该基础框架采用第三方依赖包都是目前最新版本,结合antd的UI框架,实现主题定制,webpack自主配置,动态菜单路由设计,redux数据管理,国际化多语言,错误统一处理,本地mock服务等功能。
设计原则:
1)开放性:秉承自由开放精神,绝对不会给大家造任何的黑盒子(啥叫黑盒子,就是它规定你怎么写,你就得怎么写,它不更新,你也休想正常使用,当然除了react,antd等第三方框架),项目中数据管理(伪dva),webpack配置,动态路由设计等,都是采用自由开放思想,大家可根据自己实际项目需要自行修改。
2)多选择性:该框架有目前存在多个版本供大家选择
1)test分支(min版本,里面只有静态菜单路由,数据管理,按需加载几个简单功能,适合小型项目)
2)master分支(默认,稳定版本)
3)develop分支(开发版本,最新的版本)
4)pre分支(健全版本,由min版本升级后的第一个版本,功能相对健全)
功能说明:
$ npm i generator-antd-custom -g
$ cfe init react
$ git init
$ git clone https://github.com/ctq123/antd-custom.git
$ git checkout N
N = test || develop || pre
$ npm start
or
$ npm run dev
$ npm run build:test
or
$ npm run build:prod
1.针对tabs业务模块,不再使用文件名称代表某个业务模块,建议使用目录名称替代。比如主页模块,目录名称home就代表home业务模块,home目录下采用index.js作为模块入口
2.由于react已经采用react16.9版本,切勿继续使用componentWillMount和componentWillReceiveProps等即将要废弃的生命周期,免得要在react17版本中增加维护成本
...
新增一个菜单需要两个步骤:
1)在menus中的menu.data.js文件中添加新菜单,其中key和path都是唯一值(其中permKey是权限字段,权限控制中会介绍)
2)在对应的tabs模块下添加index.menux.js文件配置,其中key和path要与步骤1中保持一致(其中routeProps是路由属性,动态路由中会介绍)
涉及范围:
menus/menu.data.js
tabs/../index.route.js
项目中采用react-router4管理路由,路由又分为静态路由和动态路由
1)静态路由:src/App.js中转跳app页面和login页面采用的是静态路由设计,它属于页面层级,并不会因为用户权限的差异而不同
2)动态路由:pages/app/index.js中采用的是动态生成路由,它会因为用户权限的不同而存在差异
动态生成路由逻辑:
1)先去pages/tabs业务目录下查找出所有业务模块index.route.js文件
2)根据index.route.js查找routeProps属性生成Route
3)根据传入用户菜单列表的menuList查找父级菜单下的第一个子菜单生成Redirect(业务需求:点击父级菜单路由会转跳至第一个有效子菜单)
详情:menus/menu.route.js
涉及范围:
pages/app/index.js
menus/menu.route.js
tabs/../index.route.js
app内菜单处理技术方案:
1)路由转跳方案:点击菜单加载tab页面文件,转跳到对应的路由并渲染页面,本项目采用的是路由转跳处理方案
2)新增tab方案:点击菜单加载tab页面文件,新增tab渲染页面,若采用该技术实现方案,可将上述涉及范围删除
用户登陆成功后,会向后端获取用户权限列表,是一个权限key值的列表,分为菜单权限和功能权限
1)菜单权限: 在menus/menu.data.js路由中配置permKey值,获取到用户权限列表后,重新生成新的菜单列表
2)路由权限: 在index.route.js路由中配置permKey值,获取到用户权限列表后,重新生成新的路由列表
3)功能权限: 若页面中存在某个按钮只有某些角色(权限)才能看到,根据用户权限列表判断是否需要显示该按钮,如tabs/example模块
涉及范围:
pages/app/index.js
menus/menu.data.js
menus/menu.permission.js
utils/permission.js
tabs/../index.route.js
更多权限技术方案:
1)前后端结合方案:用户登陆后,获取用户权限列表,前端根据用户权限列表生成相应的菜单,但需要前端配置菜单权限key值;
优势:路由权限和功能权限可以一次性获取
2)后端判断方案:用户登陆后,后端判断该用户权限,直接返回菜单列表,前端直接显示
优势:前端不需要重新生成菜单,也不需要配置权限key值 劣势:若有菜单内颗粒度更小的按钮功能等权限时,还需要再次向后端获取权限列表值,判断是否需要显示
数据管理采用的是redux+redux-saga方案,这里参考dva的部分设计思想,自己重新生成一套数据管理方案,使用方法与dva非常相似
如果你还不熟悉redux和redux-saga,请自行学习和了解
如果你不想了解它们,那也没关系,按照以下两个步骤使用即可,不过我还是建议你抽空学习并掌握它们
新增一个菜单数据管理需要两个步骤:
以tabs/home模块为例
1)在home模块下创建一个index.model.js文件,其包含四个属性name, state, reducers, effects。
name: model名称,model中的name需要保持唯一,它是挂在store下state对应的key值
state: 初始state状态
reducers: reducer纯函数对象
effects: redux-saga处理异步请求的副作用对象
2)在home/index.js文件中使用connect()引入该模块的state属性,即步骤1中的name,它对全局有效
说明:reducers和effects函数的key,最好以步骤1中的name开头,调用时业务清晰明了,同时方便后期全局的搜索和维护
涉及范围:
pages/../index.model.js
pages/../index.js
redux/*
核心设计思想:
1)遍历pages/*目录下index.model.js找出{name, state, reducers},并将其重新整合生成redux的reducer
2)遍历pages/*目录下index.model.js找出effects,将其重新整合生成redux-saga
更多详情请看redux最佳实践的前世今生2
redux建议使用场景
1.兄弟组件之间通信,不建议在直接父子组件中滥用redux
2.跨多层父子组件之间通信,比如爷爷和孙子,曾祖父和曾孙等。redux原理之一就是基于这个来做的。
理论上,在index.js中直接使用axios处理异步已满足百分之九十业务场景了,比如user模块;
只有一些跨兄弟组件通信或者跨多层父子组件才需要使用redux,比如login模块;
home模块中的示例只是为了方便介绍redux的使用方法,故意而为之
按需加载处理方案:
1)采用require.ensure()处理
2)采用react.lazy()处理,react16.6引入,利用import()原理处理懒加载,暂时还不支持服务器渲染
3)采用loadable-components处理,支持服务器渲染,也是react官方推荐处理服务器渲染方案。
后面两种方案都是采用import()原理进行代码切割,并使用promise进行异步加载。
由于本项目不涉及服务器渲染,采用第二种技术方案,更重要的是它是react原生官方的,是亲生儿子,比任何第三方插件都可靠。
目前处理国际化语言最流行的两种解决方案是react-intl和i18n
本项目采用的是antd官方使用的一套国际化插件,react-intl3,也是非常流行的一套解决方案,但目前网上的文章多是react-intl2的,react-intl3配置和使用方法均出现了较大的变化,具体可看官网。
react-intl有两种使用方法:
1)dom场景:直接生成HTML对应的翻译文本dom,如home模块(home/index.js)
2)字符串场景:直接生成string字符串翻译文本字符串,如user模块(user/index.js)
实际使用场景中需要在locales/目录下配置对应的key-value值,通过引用key来显示对应的文字
涉及范围:
src/components/react-intl/*
locales/*
pages/../index.js
src/components/*
这里采用的是一个外部插件cf-mock-server作为本地mock服务,具体配置可在webpack中配置
webpack.config.js
module.exports = {
//...
devServer{
//...
after: (app, server) => {
app.use(mock({
config: path.join(__dirname, './mock-server/config.js')
}))
},
}
}
然后创建mock-server文件夹及其配置文件config.js,最后在config.js配置对应的接口API以及对应json文件数据即可,具体可看home模块的例子
涉及范围:
mock-server/*
通过创建import或require的别名,来确保模块的引入变得更简单。
webpack.config.js
module.exports = {
//...
resolve: {
alias: {
'@assets': path.join(__dirname, 'assets'),
'@src': path.join(__dirname, 'src'),
'@components': path.join(__dirname, 'src/components'),
'@utils': path.join(__dirname, 'src/utils'),
'@menus': path.join(__dirname, 'src/menus'),
'@locales': path.join(__dirname, 'src/locales'),
}
}
}
通常我们使用的编辑器是vscode,上述只对webpack有效,vscode编辑器它并不知道,command+点击并不会发生转跳,因此需要添加jsconfig.json配置
jsconfig.json
{
"compilerOptions": {
"checkJs": false,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["assets/*"],
"@src/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@menus/*": ["src/menus/*"],
"@locales/*": ["src/locales/*"],
}
},
}
我们采用axios作为通信处理方式,在应用页面初始化时,设置axios,并对url返回进行拦截处理,若出现错误异常,会出现两种场景,一种是网络错误,一种是业务错误
1)网络错误:网络请求状态为401,404,503等错误,并提示对应的信息。
2)业务错误:这种没有网络异常(即返回状态为200),通常后端返回的数据都会经过一层包装,若数据中存在success的状态,若为false,即发生了业务异常,需要对其进行特殊处理。本项目中业务异常返回到各模块中进行处理,在拦截层做统一处理,若成功直接返回真正可用的data数据;若失败,先提取后端返回的错误信息,并与原始数据一起直接返回给view层,至于如何处理错误,由view层自己决定
涉及范围:
utils/handleAxios.js
安全方面,我们采用token验证的方式解决
我们通过设置withCredentials携带cookie,前端登陆成功后,我们会从cookie中获取token值,并在axios请求的header中统一设置Authorization字段为token值,以后所有的请求的头部都会带上该Authorization字段,后端根据该字段与后端保存的token进行验证判断该请求是否合法
涉及范围:
utils/handleAxios.js
采用xlsx依赖包,它比file-saver更出色,提供多种导出文件格式并且提供样式导出,比如合并单元格等
对antd的columns进行解析和封装,使用时直接调用方法即可,优雅简单,不需要其他配置
涉及范围:
utils/exportTableData.js
utils/download-file.js