Springboot + mybatis + React+redux+React-router+antd+Typescript(二): React+Typescrip项目的搭建
前言:
后台搭建完以后开始搭建前端,使用create-react-app搭建项目非常方便。
前端主要是如何向后台请求数据,以及如何使用redux管理state,路由的配置.
前端github地址: https://github.com/www2388258980/rty-web
后台github地址: https://github.com/www2388258980/rty-service
项目访问地址: http://106.13.61.216:5000/ 账号/密码: root/root
准备工作:
1.需要安装node.js;
2.安装create-react-app脚手架;
准备知识:
1.React
2.Typescript
3.Redux
4.React-Router
5.adtd
以上点击都可以打开对应的文档.
正文:
1.使用 TypeScript 启动新的 Create React App 项目:
命令行:
>npx create-react-app 项目名 --typescript
or
>yarn create react-app 项目名 --typescript
然后运行:
>cd 项目名
>npm start
or
>yarn start
此时浏览器会访问 http://localhost:3000/ ,看到 Welcome to React
的界面就算成功了。
2. 按需引入antd组件库
[1]: 此时我们需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)。
>cd 项目名
>yarn add react-app-rewired
customize-cra
接着修改package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
}
[2]: 使用 babel-plugin-import,'是一个用于按需加载组件代码和样式的 babel 插件,现在我们尝试安装它并修改
config-overrides.js
文件。'
>yarn add babel-plugin-import
然后在项目根目录创建一个
config-overrides.js
用于修改默认配置.
const {override, fixBabelImports} = require('customize-cra'); module.exports = override( fixBabelImports('import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css', }), );
[3]: 引入antd:
> yarn add antd
3. 封装ajax向后台请求数据:
[1]: > yarn add isomorphic-fetch
[2]: 在src下新建api目录,新建api.js和index.js
import fetch from 'isomorphic-fetch';//考虑使用fetch class _Api { constructor(opts) { this.opts = opts || {}; if (!this.opts.baseURI) throw new Error('baseURI option is required'); } request = (path, method = 'post', params, data, callback, urlType) => { return new Promise((resolve, reject) => { let url = this.opts.baseURI + path; if (urlType) { url = this.opts[urlType + 'BaseURI'] + path; } if (path.indexOf('http://') == 0) { url = path; } const opts = { method: method, }; if (this.opts.headers) { opts.headers = this.opts.headers; } if (data) { opts.headers['Content-Type'] = 'application/json; charset=utf-8'; opts.body = JSON.stringify(data); } if (params) { opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; let queryString = ''; for (const param in params) { const value = params[param]; if (value == null || value == undefined) { continue; } queryString += (param + '=' + value + '&'); } if (opts.method == 'get') { if (url.indexOf('?') != -1) { url += ('&' + queryString); } else { url += ('?' + queryString); } } else { opts.body = queryString; } } fetch(url, opts).then(function (response) { if (response.status >= 400) { throw new Error("Bad response from server"); } return response.json().then(function (json) { // callback(); return resolve(json); }) }).catch(function (error) { console.log(error); }); }) } } const Api = _Api; export default Api
import Api from './api'; const api = new Api({ baseURI: 'http://106.13.61.216:8888', // baseURI: 'http://127.0.0.1:8888', headers: { 'Accept': '*/*', 'Content-Type': 'application/json; charset=utf-8' } }); export default api;
4.接下来举个计时器的例子引入说明Redux,React-router.
[1]: 在react+typescript下引入Redux 管理状态:
> yarn add redux @types/redux
> yarn add react-redux @types/react-redux
[2]: 在src下新建containers目录,在containers目录下新建test目录,接着在此目录下新建action.tsx和index.tsx;
action.tsx:
const namespace = 'test'; // 增加 state 次数的方法 export function increment() { console.log("export function increment"); return { type: 'INCREMENT', isSpecial: true, namespace, } } // 减少 state 次数的方法 export const decrement = () => ({ type: 'DECREMENT', isSpecial: true, namespace })
index.tsx:
import * as React from 'react'; import {connect} from 'react-redux'; import {Dispatch, bindActionCreators} from 'redux'; import {decrement, increment} from '../test/action'; // 创建类型接口 export interface IProps { value: number; onIncrement: any; onDecrement: any; } // 使用接口代替 PropTypes 进行类型校验 class Counter extends React.PureComponent{ public render() { const {value, onIncrement, onDecrement} = this.props; console.log("value: " + value); console.log('onIncrement: ' + typeof onIncrement) return ( Clicked: {value} times
) } } // 将 reducer 中的状态插入到组件的 props 中 const mapStateToProps = (state: { counterReducer: any }) => ({ value: state.counterReducer.count }) // 将对应action 插入到组件的 props 中 const mapDispatchToProps = (dispatch: Dispatch) => ({ onDecrement: bindActionCreators(decrement, dispatch), onIncrement: bindActionCreators(increment, dispatch), }) // 使用 connect 高阶组件对 Counter 进行包裹 const CounterApp = connect( mapStateToProps, mapDispatchToProps )(Counter); export default CounterApp;
[3]: src下新建utils目录,新建promise.js,
export function isPromise(value) { if (value !== null && typeof value === 'object') { return value.promise && typeof value.promise.then === 'function'; } }
安装中间件:
>yarn add redux-thunk
在src下创建middlewares目录,新建promise-middleware.js(中间件),
import {isPromise} from '../utils/promise'; const defaultTypes = ['PENDING', 'FULFILLED', 'REJECTED']; export default function promiseMiddleware(config = {}) { const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypes; return (_ref) => { const dispatch = _ref.dispatch; return next => action => { if(!isPromise(action.payload)){ let originType = action.originType?action.originType:action.type; let type = action.originType?action.type:action.namespace?`${action.namespace}_${action.type}`:`${action.type}`; return next({ ...action, originType, type }); } const {type, payload, meta,isSpecial, resultType,namespace} = action; const {promise, data} = payload; const [ PENDING, FULFILLED, REJECTED ] = (meta || {}).promiseTypeSuffixes || promiseTypeSuffixes; /** * Dispatch the first async handler. This tells the * reducers that an async action has been dispatched. */ next({ originType:type, type: namespace?`${namespace}_${type}_${PENDING}`:`${type}_${PENDING}`, ...!!data ? {payload: data} : {}, ...!!meta ? {meta} : {}, isSpecial, resultType, namespace }); const isAction = resolved => resolved && (resolved.meta || resolved.payload); const isThunk = resolved => typeof resolved === 'function'; const getResolveAction = isError => ({ originType:type, type: namespace?`${namespace}_${type}_${isError ? REJECTED : FULFILLED}`:`${type}_${isError ? REJECTED : FULFILLED}`, ...!!meta ? {meta} : {}, ...!!isError ? {error: true} : {}, isSpecial, resultType, namespace }); /** * Re-dispatch one of: * 1. a thunk, bound to a resolved/rejected object containing ?meta and type * 2. the resolved/rejected object, if it looks like an action, merged into action * 3. a resolve/rejected action with the resolve/rejected object as a payload */ action.payload.promise = promise.then( (resolved = {}) => { const resolveAction = getResolveAction(); return dispatch(isThunk(resolved) ? resolved.bind(null, resolveAction) : { ...resolveAction, ...isAction(resolved) ? resolved : { ...!!resolved && {payload: resolved} } }); }, (rejected = {}) => { const resolveAction = getResolveAction(true); return dispatch(isThunk(rejected) ? rejected.bind(null, resolveAction) : { ...resolveAction, ...isAction(rejected) ? rejected : { ...!!rejected && {payload: rejected} } }); }, ); return action; }; }; }
[4]:
(1)src下新建store目录,在此新建base-reducer.js,
const initialState = {}; export default function baseReducer(base, state = initialState, action = {}) { switch (action.type) { case `${base}_${action.originType}_PENDING`: return { ...state, [`${action.originType}Result`]: state[`${action.originType}Result`] ? state[`${action.originType}Result`] : null, [`${action.originType}Loading`]: true, [`${action.originType}Meta`]: action.meta, }; case `${base}_${action.originType}_SUCCESS`: return { ...state, [`${action.originType}Result`]: action.resultType ? action.payload[action.resultType] : action.payload, [`${action.originType}Loading`]: false, [`${action.originType}Meta`]: action.meta, }; case `${base}_${action.originType}_ERROR`: return { ...state, [`${action.originType}Error`]: action.payload.errorMsg, [`${action.originType}Loading`]: false }; case `${base}_${action.originType}`: return {...state, [action.originType]: action.data, [`${action.originType}Meta`]: action.meta}; default: return {...state}; } }
(2) 在store目录下新建reducers目录,接着新建test.tsx文件,
import baseReducer from '../base-reducer'; const namespace = 'test'; // 处理并返回 state export default function CounterReducer(state = {count: 0}, action: { type: string, isSpecial?: boolean }) { if (!action.isSpecial) { return baseReducer(namespace, state, action); } switch (action.type) { case namespace + '_INCREMENT': console.log('INCREMENT'); return Object.assign({}, state, { count: state.count + 1 //计数器加一 }); case namespace + '_DECREMENT': console.log('DECREMENT'); return Object.assign({}, state, { count: state.count - 1 //计数器减一 }); default: return state; } }
(3) 在store目录下新建configure.store.tsx文件,
import {createStore, applyMiddleware, combineReducers, compose} from 'redux'; import thunkMiddleware from 'redux-thunk'; import promiseMiddleware from '../middlewares/promise-middleware'; import CounterReducer from './reducers/test'; const reducer = combineReducers({ counterReducer: CounterReducer, }) const enhancer = compose( //你要使用的中间件,放在前面 applyMiddleware( thunkMiddleware, promiseMiddleware({promiseTypeSuffixes: ['PENDING', 'SUCCESS', 'ERROR']}) ), ); export default function configureStore(initialState = {}) { return createStore( reducer, initialState, enhancer ); }
[5]: 更改App.tsx中的内容:
import React from 'react'; // import './App.css'; import {Link} from 'react-router-dom'; import {Layout, Menu, Icon} from 'antd'; const {SubMenu} = Menu; const {Header, Content, Sider} = Layout; export interface AppProps { } export interface AppState { } class App extends React.Component{ rootSubmenuKeys = ['拨入', '审计', '测试']; state = { collapsed: false, openKeys: ['拨入'], }; componentDidMount(): void { } toggle = () => { this.setState({ collapsed: !this.state.collapsed, }); }; onOpenChange = (openKeys: Array ) => { let latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1); latestOpenKey = latestOpenKey ? latestOpenKey : ''; if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) { this.setState({openKeys}); } else { this.setState({ openKeys: latestOpenKey ? [latestOpenKey] : [], }); } }; render() { const mainSvg = () => ( ); const boruSvg = () => ( ); const testSVg = () => ( ) const MainIcon = (props: any) => ; const BoruIcon = (props: any) => ; const TestIcon = (props: any) => ; return ( ); } } export default App;this.state.collapsed} trigger={null} width={250}> style={{ height: 32, background: 'rgba(255, 255, 255, 0.2)', margin: 16, fontSize: 20, color: "orange" }}> rty<Menu theme="dark" mode="inline" defaultSelectedKeys={['1']} defaultOpenKeys={['审计']} style={{borderRight: 0,height: 950}} openKeys={this.state.openKeys} onOpenChange={this.onOpenChange} > <SubMenu key="拨入" title={拨入} > 新增拨入记录 查询拨入记录 拨入人员列表 查询拨入人员列表 账号变更查询 <SubMenu key="审计" title={审计} > 添加OA账号 账号查询 账号变更查询 <SubMenu key="测试" title={测试} > 计时器测试 <Icon className="trigger" type={this.state.collapsed ? 'menu-unfold' : 'menu-fold'} onClick={this.toggle} /> <Content style={{ margin: '6px 4px', padding: 6, background: '#ffffff', minHeight: 280, }} > {this.props.children}
[6]: > yarn add react-router-dom @types/react-router-dom
然后在src下新建MyRouer.tsx,
/* 定义组件路由 * @author: yj * 时间: 2019-12-18 */ import React from 'react'; import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; import CounterApp from './containers/test/index';
import App from "./App"; const MyRouter = () => (); export default MyRouter; }/>
[7]: 更改src/index.tsx中的内容,
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import * as serviceWorker from './serviceWorker'; import {Provider} from 'react-redux'; import configureStore from './store/configure-store'; import MyRouterApp from './MyRouter'; const store = configureStore(); ReactDOM.render( //关联store, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
-------------------------------
然后启动项目即可.总结一下工作流程以及容易误解的地方:
(1): 点击 '+'号,触发onIncrement函数,onIncrement通过'bindActionCreators'绑定了action的'increment'方法,本次action描述'计时器'要加1,派发action会交给reduce处理,reducer会改变state,
即state.counterReducer.count值加1,然后计时器重新渲染,值变成1.
(2): 本次的中间件会让api请求后台数据变成异步;
(3): 当action.isSpecial为false的时候,base-reducer.js是用来统一处理action的reducer,
import api from '../../api/index';
import {rtyDialOAPersonReq, rtyDialOAPerson} from './data'
const namespace = 'shenji';
export function getRtyOADialPersonsByFirstChar(firstChar?: string) {
let path = '/rtyOADialPersons/getRtyOADialPersonsByFirstChar';
return {
type: 'rtyOADialPersonsByFirstCharSource',
payload: {
promise: api.request(path, 'post', {firstChar})
},
resultType: 'data',
namespace
}
}
例如这个action,它会向 /rtyOADialPersons/getRtyOADialPersonsByFirstChar 接口请求数据, 拿到的json数据为:
{"JSON":{"status":"success","errorCode":null,"message":"查询成功.","data":[{"dialPersonId":"6","firstName":"美羊羊","telecomNumber":"12341231234","description":"羊村之美羊羊-臭美","firstChar":"meiyangyang","departmentId":"1004","status":"是","createdBy":"root","modifiedBy":"root","billId":"DSFsdf34543fds","modifiedBillId":"7777777777","opType":"更新","effectiveDate":"2020-02-11 13:17:10","lastUpdatedStamp":"2020-02-21 10:05:23","createdStamp":"2020-02-11 13:17:28"}],"total":0,"success":true},
'resultType'表示从后台返回的json对象拿属性为data的数据,
[{"dialPersonId":"6","firstName":"美羊羊","telecomNumber":"12341231234",......,"createdStamp":"2020-02-11 13:17:28"}
'type'表示经过base-reducer.js处理后,把上面的数据封装到 rtyOADialPersonsByFirstCharSourceResult 里面.rtyOADialPersonsByFirstCharSourceLoding一般和antd组件的loading属性绑定,
当数据加载的时候antd组件会转圈圈,加载成功以后转圈圈消失.
--------------------
本人照着以上流程走一遍,可以运行。