开头
这开头呢,我只想说一句 如何过好这一生。
关于React实际上早在很久很久之前写过一篇基于Dva方案的文章,如果不考虑typecript和版本的话其实一切用起来至今也倒尚好。
可能typecript自己至今尚未学好,关于react、redux、typescript三者的结合,一直碎碎念,打磨不好。
这其中,有redux写起来繁琐的缘故、有typescript不到家的缘故。
直到几个月前看到github上react-redux-typescript-boilerplate,终于觉得这是自己想要的方案,拖延好几个月,这里就简单整理一下,顺便把之前的Dva方案升级一下。
方案简析
Redux
这里redux,有很多概念,譬如Action/Reducer/Store/Middleware。在项目里面有对应的actions/reducers/store/middleware目录对应。这里不是redux的安利文所以不做特别说明。
关于方案的优势这里必须提一下其使用上的便利。一般情况下,我们使用redux时候dispatch一个状态都需要经过引用,然后在mapDispatchToProps里面注册,最后再this.props.dispatch({type: Action, {payload: {}}})
进行状态触发。
在本方案中,redux和react的连接部分 代码是这样的:
1 2 3 4 5 6 7 8 9 10
| @connect( (state: RootState, ownProps): Pick<App.Props, 'todos' | 'filter'> => { const hash = ownProps.location && ownProps.location.hash.replace('#', ''); const filter = FILTER_VALUES.find((value) => value === hash) || TodoModel.Filter.SHOW_ALL; return { todos: state.todos, filter }; }, (dispatch: Dispatch): Pick<App.Props, 'actions'> => ({ actions: bindActionCreators(omit(TodoActions, 'Type'), dispatch) }) )
|
在这个小结,这里仅仅注意到bindActionCreators这个API的便利性即可。
Typescript
谈到ts,这里感觉巧妙的地方有些多。
比如actions/todo.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { createAction } from 'redux-actions'; import { TodoModel } from 'app/models';
export namespace TodoActions { export enum Type { ADD_TODO = 'ADD_TODO', EDIT_TODO = 'EDIT_TODO', DELETE_TODO = 'DELETE_TODO', COMPLETE_TODO = 'COMPLETE_TODO', COMPLETE_ALL = 'COMPLETE_ALL', CLEAR_COMPLETED = 'CLEAR_COMPLETED' }
export const addTodo = createAction<PartialPick<TodoModel, 'text'>>(Type.ADD_TODO); export const editTodo = createAction<PartialPick<TodoModel, 'id'>>(Type.EDIT_TODO); export const deleteTodo = createAction<TodoModel['id']>(Type.DELETE_TODO); export const completeTodo = createAction<TodoModel['id']>(Type.COMPLETE_TODO); export const completeAll = createAction(Type.COMPLETE_ALL); export const clearCompleted = createAction(Type.CLEAR_COMPLETED); }
export type TodoActions = Omit<typeof TodoActions, 'Type'>;
|
这里有一个namespace和一个type。其中type这里使用了Omit = Pick<T, Exclude<keyof T, K>>
这个高级类型,将namespace中的enum Type进行了排除。它的作用是给Container组件设置props类型的接口。
而namespace里面除了addTodo
这些的引用,因为可以访问到Type,还可以在Reducer里面获得引用。
然后我们看看namespace这里面的addTodo这些。
1
| export const addTodo = createAction<PartialPick<TodoModel, "text">>(actionType: string): ActionFunction1<PartialPick<TodoModel, "text">, Action<PartialPick<TodoModel, "text">>>
|
这里PartialPick<TodoModel, "text">
用来约束后面dispatch
事件时候的传值。其中PartialPick是 Partial<T> & Pick<T, K>
组成的联合类型。这里的意义是将TodoModel里面除了‘text’之外的属性改成选填状态。
然后是App/index.js文件。这里摘录一些关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export namespace App { export interface Props extends RouteComponentProps<void> { todos: RootState.TodoState; actions: TodoActions; filter: TodoModel.Filter; } } @connect( (state: RootState, ownProps): Pick<App.Props, 'todos' | 'filter'> => { const hash = ownProps.location && ownProps.location.hash.replace('#', ''); const filter = FILTER_VALUES.find((value) => value === hash) || TodoModel.Filter.SHOW_ALL; return { todos: state.todos, filter }; }, (dispatch: Dispatch): Pick<App.Props, 'actions'> => ({ actions: bindActionCreators(omit(TodoActions, 'Type'), dispatch) }) ) export class App extends React.Component<App.Props> {}
|
这其中,type TodoActions
用在了interface Props
中。
然后mapDispatchToProps函数中则是使用了bindActionCreators这个API并根据之前提到的Omit构建了一个函数排除‘Type’生成了我们常见的dispatch到props的注册。
小结
实际上在这个方案中,并没有太多特别的地方。只不过,在这里有几个地方处理得很好。
- typescript的namespace 和 type用得巧妙,namespace内部的enum这个也用的很巧妙
- 配合bindActionCreators方便了mapDispatchToProps函数的恼人操作
改造
Antd按需加载
官方推荐的按需加载用的是babel-import-plugin
,不过这里因为用了Typescript,所以不推荐用它了,转而推荐其Ts版本的ts-import-plugin
。
另外,按需加载依赖less。所以也需要配置一下less-loader这些。
最后就是extract-text-webpack-plugin
在webpack4这里无法使用,所以转而使用了mini-css-extract-plugin
,这里已经自带,不过注意不要将其和style-loader
一同使用,这是冲突操作。
首先是依赖:
1
| npm i ts-import-plugin less less-loader postcss-less antd --save
|
其次修改webpack配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| ... { test: /\.tsx?$/, use: [ !isProduction && { loader: 'babel-loader', options: { plugins: [ 'react-hot-loader/babel' ] } }, { loader: 'ts-loader', options: { transpileOnly: true, getCustomTransformers: () => ({ before: [ tsImportPluginFactory({ libraryName: 'antd', libraryDirectory: 'es', style: true, }) ] }), compilerOptions: { module: 'es2015' } } } ].filter(Boolean) }, ... { test: /\.less$/, use: [ MiniCssExtractPlugin.loader, `css-loader`, {loader: 'postcss-loader', options: postcssOptions}, {loader: 'less-loader', options: { javascriptEnabled: true }}, ], include: [/[\\/]node_modules[\\/].*antd/], }, { test: /\.less$/, use: [ MiniCssExtractPlugin.loader, `css-loader?modules&importLoaders=1&localIdentName=[local]_[hash:base64:5]&-autoprefixer`, {loader: 'postcss-loader', options: postcssOptions}, {loader: 'less-loader', options: { javascriptEnabled: true }}, ], include: [projectPath], },
|
这里仅仅标出代码,详细可以直接看相关代码。
添加异步库
这里使用redux-sagas。详细直接看代码。不作详细解说。
API请求处理
这里根据之前代码简单封装whatwg-fetch。
不过这个请求库一般需要根据自己业务自行整体处理。这里聊作参考。
另外mock服务这里暂时不做了,如果考虑要做mock实质上还是推荐直接使用devServer.proxy
来直接对请求进行反向代理,然后是如果远程rap方案的mock这里就不消说,如果是预备本地代理那就找个mockServer配合nodemon的watch和自动重启来做(不过mockServer也可能自带这个功能)。
代码生成
这块实质上是demo完成后补上的。
用意是虽然在Typescript体验上ok了,但是每次手动写那么多代码 建立那么多文件还是过于繁琐,于是参考别的项目使用plop
做了若干配置。这里运行npm run g
之后会有询问框,可以自行生成component && container代码,component支持es6和stateless模块。
这里代码生成这里最麻烦的是container这块的redux相关配置,除了script/generators里面的配置、模版外,需要注意不要删除各个文件里面的/* PREPEND IMPORT HERE */、/* PREPEND ATTR HERE */之类,这里是插入相关引用必须的插入点。
按需加载
临push时候想起还没这个生产级别的基础配置,这里简单配置了一下。主要是loadable-components && @babel/plugin-syntax-dynamic-import && @babel/react
。
这里集成到plop模版了,默认开启,使用npm run g
创建container时候会默认开启这个功能。
代码
1 2 3 4 5 6 7 8
| // 推荐 方便看master代码 git clone https://github.com/que01/react-redux-typescript-boilerplate cd react-redux-typescript-boilerplate git checkout -b demo git branch --set-upstream-to=origin/demo demo git pull // 直接 git clone -b demo https://github.com/que01/react-redux-typescript-boilerplate
|