㈠ 从0搭建React+antd+TypeScript+Umi Hooks+Mobx前端框架
因为现在公司的主要技术栈是React,所以也想着能够搭建一个好的React前端框架,方便在工作中使用;框架在打包过程也做了优化,多线程,拆包,缓存等等手段提升打包速度和质量。主要用到的库包括:
创建带TypeScript模板的react-app,推荐使用yarn,接下来我也主要以yarn做例子
然后在项目根目录创建一个 craco.config.js 用于修改默认配置。antd按需加载以及自定义主题
重新打包就可以了, 所有的主题配置在这里噢
这里利用React-router做路由,同时也会根据用户角色,做权限处理;只有当角色和路由允许的角色一致时才可以访问和展示。
新建router下新建indext.tsx 用于渲染页面
引入Router/index.tsx
新建hasPermission.ts,如果页面 roles 包括用户的角色则返回true,在渲染menu和子页面的时候就根据这个值渲染页面。
比如Home页面,渲染子页面的逻辑:
在这里 SubPages1 下面的 page1 就无法展示出来和访问,如果直接输入路由也会访问页面不存在,因为page1允许的角色 user 而我们角色是 admin 所以无法展示。
useImmer 很好的解决了ReactHooks中的赋值的性能问题,可以单独更新某个对象的某个属性。
上面的赋值方法也可以写到一起,效果是一样的:
Umi Hooks 是一个 React Hooks 库,致力提供常用且高质量的 Hooks。提供了非常多的Hooks组件,比如上面使用的 usePersistFn ,他的作用:在某些场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复 render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过 usePersistFn ,可以保证函数地址永远不会变化。Umi Hooks功能还是非常强大的,有很多功能很强大的API。大家可以去官方文档看看 https://hooks.umijs.org/zh-CN/hooks/life-cycle/use-update-effect 。
自定义 hooks 其实在我们的开发工作中,还是很常遇到的。 hooks 的好处就是可以抽离公共方法,像组件一样的随意使用,对于快节奏的开发工作还是很舒服的,比如你觉得 react hooks 或者 umi hooks 的api,不能满足自己的需求,也可以自己创新一些api。我这里举个例子,大家写 class 组件写的很多的话,会经常用的 this.setState() ,大家都知道 this.setState() 是异步执行,你无法直接拿到最新的 state 。 hooks 中的 useState 同样也是异步的,你无法直接获取到最新的 state ,所以我自己写了一个 useSetState 方法,用于在修改完状态后能够立即拿到最新的 state 。
我们在src/hooks文件夹下新建 useSetState.ts
使用的方式也很简单,基本和useState一致,只是在setState的时候提供一个回调函数。
这就完成了带回调的 useSetState hooks 的编写,不过这种写法不太推荐在 hooks 中使用,建议需要获取最新的数值都在 useEffect 或者 useUpdateEffect(umi hooks) 中去。
状态管理选择的Mobx,Mobx和Rex我都用过,不过当我习惯用Mobx后,就感觉还是Mobx更方便一些,所以更喜欢在项目中用Mobx,现在Mobx已经更新到5.0版本了,不过5.0版本并不支持ie11,所以如果想要兼容性可以选择4.0的版本,或者Rex。
这里推荐一个针对Mobx的库, mobx-react-lite :它是基于 React 16.8 和 Hooks 的 MobX 的轻量级React绑定。
这个主要影响的是调用方法的形式,对于Mobx的书写是一样的,比如写一个加减数值:
这里你的typeScirpt可能会编译不了,会报错:Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.
解决方法是在 tsconfig.json 加入配置:
完毕以后,一定要把 storeProvider 包裹所需要共享状态的页面,我这里直接放到app.tsx
剩下来就仅仅是调用的事情了:
此外axios的配置应该大家都知道,所以我这也不多说了,具体在我的源码里面也有,utils下的axios.ts
加入了打包分析 webpack-bundle-analyzer speed-measure-webpack-plugin
加入了打包进度条 webpackbar
加入了打包压缩 compression-webpack-plugin terser-webpack-plugin
还对包进行拆包
开发环境的域名代理 devServer
加快打包速度,还可以考虑删除antd-icons,单独去iconfont网站下,按需引入。不然打包会费很多时间
引入dotenv-cli
新增开发环境配置文件 .env.development 和 .env.proction 两个文件
然后修改package.json中的启动脚本:
现在 yarn start 或者 yarn build 就会根据环境配置来处理。
还有一些细节的调整,会尽力将这个框架更加完善的。
github地址: https://github.com/Benzic/React-typescript-umihooks-mobx
欢迎star 和提意见
㈡ 【React入门实践】结合Ant-Design 从0带你手把手开发包含【列表】和【搜索栏】的简单【用户管理】页面
写在前面的话: 上节我们实现了简单的是form表单,如下,今天我们来学习最常见的表单组件的使用。一步步向更复杂的页面进发。
本次页面核心知识点:(1)【search表单模块】搜索search、重置功能reset、日期选择器DatePicker、下拉框使用select;(2)【列表展示模块】时间规范化moment、tag标签的使用、省略款的Tooltip使用;
1. 看页面需求?分析:页面分为两个核心部分:【搜索栏】【列表】
2. 分析接口文档(1)留言列表接口-主要是在页面初始化时调用。pageNum pageSize为请求参数,非必须。响应参数在list中。
?(2)搜索接口 - 但搜索接口一般可以直接使用列表接口即可
?
[]()?3. 参照UI编写页面代码[]()(1) 列表部分使用封装的组件(代码后附),data={listData}表示列表中绑定listData数据,注意dataIndex需要对照接口返回的参数编写
dispatch,feedback: { listData, detailData },loading}: any) {const init = useCallback(() => {dispatch('feedback/init')}, [dispatch])useEffect(() => {init()}, [init])const columns = useMemo(() => [{title: '序号',dataIndex: '_id'},{title: '用户名',dataIndex: 'username'},{title: '反馈标题',dataIndex: 'title'},{title: '反馈内容',dataIndex: 'content',},{title: '提交时间',dataIndex: 'createdTime',},{title: '状态',dataIndex: 'status',},{title: '操作',render: (value: any) => (<>{value.status === 0 && (<a type="primary" onClick={() => toggleMessage(value.id)}>反馈留言</a>)}{value.status === 1 && (<a type="primary" onClick={() => toggleDetail(value.id)}>查看</a>)}</>)}],[toggleDetail, toggleMessage])return (<PageHeaderLayout className={'commonList'}><Search init={init} dispatch={dispatch} /><CardclassName={styles.table}style={{ marginTop: 1 }}bordered={false}bodyStyle={{ padding: Ǝpx 32px 32px 32px' }}><StandardTableloading={loading}data={listData}columns={columns}onChange={handleStandardTableChange}/></Card></PageHeaderLayout>)}(2)search查询栏部分const { validateFields } = formconst simpleForm = useMemo(() => {const { getFieldDecorator, getFieldValue } = formconst { query } = getLocation()return (<Formlayout="inline"style={{ display: 'flex' }}className={styles.commonForm}><Form.Item label="">{getFieldDecorator('username', {initialValue: query.username})(<Input placeholder="用户名" />)}</Form.Item><Form.Item label="">{getFieldDecorator('title', {initialValue: query.title})(<Input placeholder="标题" />)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeBegin', {initialValue: query.startDate || ''})(<DatePickeronChange={change1}disabledDate={disabledDate1}placeholder="请选择日期"/>)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeEnd', {initialValue: query.startDate || ''})(<DatePickeronChange={change2}disabledDate={disabledDate2}placeholder="请选择日期"/>)}</Form.Item><Form.Item>{getFieldDecorator('status', {initialValue: query.status || ''})(<Select><Option value={''}> 状态 </Option>{feedbackStatus.map((v: any) => (<Option key={v.key} value={v.key}>{v.value}</Option>))}</Select>)}</Form.Item><Button className={styles.search} onClick={handleSearch} icon="search">查询</Button><ButtonclassName={styles.reset}style={{ marginLeft: 10 }}onClick={handleFormReset}icon="undo">重置</Button></Form>)}, [form, handleFormReset, handleSearch, time1, time2])return (<Card bordered={false}><div className={'commonForm'}>{simpleForm}</div></Card>)})可以看到,在页面将search设置成组件,并在后面掉哦组件。至此已达到页面效果,但是还没有数据
4.初始化页面 - 列表数据显示在page页面的主函数中写init函数
dispatch('feedback/init')}, [dispatch])useEffect(() => {init()}, [init])在model页面中写详细的接口调用信息
const { query } = getLocation()const data = await dispatch({type: 'feedback/post',params: ['manage/feedback/page', { ...query }]})dispatch({type: 'feedback/setStore',params: [{ listData: listMOM1(data) }]})},其中我们使用到了ListMOM1组件来帮助页面数据回显,其主要功能是,添加页面的数据编号和实现分页功能,详细的封装代码后附。 其中type: 'feedback/setStore',表示把数据存储在缓存中,可以通过组件传递数据,因此在StandardTable中需要绑定data={listData},从而实现数据初始化显示。
export const listMOM1 = (resData: any) => {const {content,// total,totalCount,currPage: currPage = 1,pageSize: pageSize = 40,data,list} = resDataconst list1 = content || data || list//添加序号if (list) {list.forEach((element: any, index: number) => {element._id = index + 1 + (currPage - 1) * pageSizeelement._symbol = element._id})}//分页const pagination = {total: totalCount,pageSize: Number(pageSize) || 40,current: Number(currPage)}return {list,pagination: totalCount && { ...listData.pagination, ...pagination }}}5.列表数据按需渲染render可以看到后台返回的数据如下,
但是,(1)【标题和内容】数据过长,我们需要将其展示前5个字符后省略号(比如这样就。。。).(2)对于【日期】数据只需要显示到具体某天,不需要时分秒,(3)对于【状态】数据显示为0,1但是前端也需要整理为‘未回答’‘已回答’
(1)数据截取缩略显示,hover后显示全部---使用Tooltip组件
title: '反馈内容',dataIndex: 'content',render: (text: any, record: any, index: any) => {if (record.content.length <= 5) {return record.content} else {return (<Tooltip title={record.content}>{record.content.substring(0, 5) + '...'}</Tooltip>)}}},(2)日期截取同理
title: '提交时间',dataIndex: 'createdTime',render: (text: any, record: any, index: any) => {return record.createdTime.substring(0, 10)}},(3)对于接口返回的0,1,2状态数值数据需要分情况渲染
title: '状态',dataIndex: 'status',render: (value: any) => (<>{value == 0 && <Tag color="red">未回答</Tag>}{value == 1 && <Tag color="green">已回答</Tag>}</>)},6. 查询功能实现validateFields((err: any, data: any) => {// pushPath({// query: data// })// init()const res = { ...data }if (data.username !== undefined) {res.username = data.username.replace(/s*/g, '')}res.pageNum = 1dispatch({type: 'feedback/search',params: [res]})})}, [dispatch, validateFields])对于按照日期的查询需要做特殊处理。
(1)结束时间需要晚于开始时间
因此DataPicker组件中加入disableddata。
onChange={change1} disabledDate={disabledDate1} placeholder="请选择日期"/>const { validateFields } = formconst simpleForm = useMemo(() => {const { getFieldDecorator, getFieldValue } = formconst { query } = getLocation()return (<Formlayout="inline"style={{ display: 'flex' }}className={styles.commonForm}><Form.Item label="">{getFieldDecorator('username', {initialValue: query.username})(<Input placeholder="用户名" />)}</Form.Item><Form.Item label="">{getFieldDecorator('title', {initialValue: query.title})(<Input placeholder="标题" />)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeBegin', {initialValue: query.startDate || ''})(<DatePickeronChange={change1}disabledDate={disabledDate1}placeholder="请选择日期"/>)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeEnd', {initialValue: query.startDate || ''})(<DatePickeronChange={change2}disabledDate={disabledDate2}placeholder="请选择日期"/>)}</Form.Item><Form.Item>{getFieldDecorator('status', {initialValue: query.status || ''})(<Select><Option value={''}> 状态 </Option>{feedbackStatus.map((v: any) => (<Option key={v.key} value={v.key}>{v.value}</Option>))}</Select>)}</Form.Item><Button className={styles.search} onClick={handleSearch} icon="search">查询</Button><ButtonclassName={styles.reset}style={{ marginLeft: 10 }}onClick={handleFormReset}icon="undo">重置</Button></Form>)}, [form, handleFormReset, handleSearch, time1, time2])return (<Card bordered={false}><div className={'commonForm'}>{simpleForm}</div></Card>)})0(2)提交时间格式处理
const { validateFields } = formconst simpleForm = useMemo(() => {const { getFieldDecorator, getFieldValue } = formconst { query } = getLocation()return (<Formlayout="inline"style={{ display: 'flex' }}className={styles.commonForm}><Form.Item label="">{getFieldDecorator('username', {initialValue: query.username})(<Input placeholder="用户名" />)}</Form.Item><Form.Item label="">{getFieldDecorator('title', {initialValue: query.title})(<Input placeholder="标题" />)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeBegin', {initialValue: query.startDate || ''})(<DatePickeronChange={change1}disabledDate={disabledDate1}placeholder="请选择日期"/>)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeEnd', {initialValue: query.startDate || ''})(<DatePickeronChange={change2}disabledDate={disabledDate2}placeholder="请选择日期"/>)}</Form.Item><Form.Item>{getFieldDecorator('status', {initialValue: query.status || ''})(<Select><Option value={''}> 状态 </Option>{feedbackStatus.map((v: any) => (<Option key={v.key} value={v.key}>{v.value}</Option>))}</Select>)}</Form.Item><Button className={styles.search} onClick={handleSearch} icon="search">查询</Button><ButtonclassName={styles.reset}style={{ marginLeft: 10 }}onClick={handleFormReset}icon="undo">重置</Button></Form>)}, [form, handleFormReset, handleSearch, time1, time2])return (<Card bordered={false}><div className={'commonForm'}>{simpleForm}</div></Card>)})1并在查询函数中添加time时间格式处理
const { validateFields } = formconst simpleForm = useMemo(() => {const { getFieldDecorator, getFieldValue } = formconst { query } = getLocation()return (<Formlayout="inline"style={{ display: 'flex' }}className={styles.commonForm}><Form.Item label="">{getFieldDecorator('username', {initialValue: query.username})(<Input placeholder="用户名" />)}</Form.Item><Form.Item label="">{getFieldDecorator('title', {initialValue: query.title})(<Input placeholder="标题" />)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeBegin', {initialValue: query.startDate || ''})(<DatePickeronChange={change1}disabledDate={disabledDate1}placeholder="请选择日期"/>)}</Form.Item><Form.Item label="">{getFieldDecorator('submitTimeEnd', {initialValue: query.startDate || ''})(<DatePickeronChange={change2}disabledDate={disabledDate2}placeholder="请选择日期"/>)}</Form.Item><Form.Item>{getFieldDecorator('status', {initialValue: query.status || ''})(<Select><Option value={''}