背景
因为现在公司的主要技术栈是React,所以也想着能够搭建一个好的React前端框架,方便在工作中使用;框架在打包过程也做了优化,多线程,拆包,缓存等等手段提升打包速度和质量。主要用到的库包括:
UI antd
-
mobx-react-lite
它是基于 React 16.8 和 Hooks 的 MobX 的轻量级React绑定。 TypeScript
- Umi Hooks 砖家出品的Hooks库
Axios
React-router
- Use Immer 用于替代useState做数据的存储
PostCss
创建项目
创建带TypeScript模板的react-app,推荐使用yarn,接下来我也主要以yarn做例子
yarn create react-app react-cli --template typescript
OR
npx create-react-app react-cli --typescript
目录结构
- api 存放接口
- assets 存放静态文件、less、iconfont等文件
- components 存放组件
- hooks 自定义hooks组件
- interfaces ts types
- layout 布局组件
- router 路由
- stores mobx状态管理
- utils 公共方法函数
- views 页面
引入Antd
yarn add antd
引入craco
yarn add @craco/craco
yarn add carco-antd
/* package.json */
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
}
然后在项目根目录创建一个 craco.config.js 用于修改默认配置。antd按需加载以及自定义主题
/* craco.config.js */
const CracoLessPlugin = require('craco-less');
const CracoAntDesignPlugin = require("craco-antd");
const path = require("path");
module.exports = {
plugins: [
// antd 按需加载 less等配置
{
plugin: CracoAntDesignPlugin,
options: {
// 自定义主题
customizeThemeLessPath: path.join(__dirname, "src/assets/styles/global.less")
}
}
],
}
/* assest/styles/global.less */
@global-text-color: #499AF2; // 公共字体颜色
@primary-color : @global-text-color; // 全局主色
重新打包就可以了,所有的主题配置在这里噢
React-router
这里利用React-router做路由,同时也会根据用户角色,做权限处理;只有当角色和路由允许的角色一致时才可以访问和展示。
yarn add react-dom-router
/* router/routes.ts */
import LoginIndex from '@/views/Login'
import HomeIndex from '@/views/Home'
import SubPages11 from '@/views/SubPages1/Page1'
import SubPages12 from '@/views/SubPages1/Page2'
import SubPages21 from '@/views/SubPages2/Page1'
import SubPages22 from '@/views/SubPages2/Page2'
import SubPages31 from '@/views/SubPages3/Page1'
import SubPages32 from '@/views/SubPages3/Page2'
import NotFound from '@/views/NotFound'
import { AndroidOutlined, AppleOutlined, DingdingOutlined, IeOutlined, ChromeOutlined, GithubOutlined, AlipayCircleOutlined, ZhihuOutlined } from '@ant-design/icons'
import { routeTypes } from '@/interfaces/routes'
const routes: routeTypes[] = [
{
path: '/',
exact: true,
component: Index,
requiresAuth: false,
},
{
path: '/pages',
component: HomeIndex,
requiresAuth: true,
children: [{
path: '/pages/sub1',
name: 'SubPages1',
icon: AndroidOutlined,
children: [{
path: "/pages/sub1/page1",
component: SubPages11,
name: 'SubPage1',
icon: AppleOutlined,
meta: {
roles: ['user']
}
},
{
path: "/pages/sub1/page2",
component: SubPages12,
name: 'SubPage2',
icon: DingdingOutlined,
meta: {
roles: ['admin']
}
}]
}, {
path: '/pages/sub2',
name: 'SubPages2',
icon: IeOutlined,
children: [...]
}, {s
path: '/pages/sub3',
name: 'SubPages3',
icon: GithubOutlined,
children: [...]
},]
},
{
path: '/login',
component: LoginIndex,
requiresAuth: false,
},
{
path: '*',
exact: true,
component: NotFound,
requiresAuth: false,
}
]
export default routes
新建router下新建indext.tsx 用于渲染页面
/* router/index.tsx */
import React from 'react';
import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { routeTypes } from '@/interfaces/routes'
import routesMap from '@router/routes'
const Routes: React.FC = () => {
return (
{
routesMap.map((item: routeTypes [], index: number) => {
return {
const Component: any = item.component
return
}}>
})
}
)
}
export default Routes
引入Router/index.tsx
import React from 'react';
import Routes from '@/router/index';
const App = () =>
export default App;
新建hasPermission.ts,如果页面roles
包括用户的角色则返回true,在渲染menu和子页面的时候就根据这个值渲染页面。
export const hasPermission = (roles: string[], userRole: string[]): boolean => {
if (!userRole) return false
if (!roles) return true
return userRole.some((role: string) => roles.includes(role))
}
比如Home页面,渲染子页面的逻辑:
const Home: React.FC = ((props: RouteComponentProps): JSX.Element => {
const loadFirstPage = useRef(false)
const getPermissionRoutes = usePersistFn((Routes: routeTypes[]): React.ReactNode => {
const userRole: string[] = ['admin']
return Routes.map((item: routeTypes, index: number) => {
if (item.children && item.children.length > 0) {
return getPermissionRoutes(item.children)
} else {
if (item?.meta?.roles) {
if (hasPermission(item?.meta?.roles, userRole)) {
if (!loadFirstPage.current) {
props.history.replace(item.path)
loadFirstPage.current = true
}
return
} else {
return null
}
} else {
return
}
}
})
})
return
{getPermissionRoutes(Routes[1].children as routeTypes[])}
})
export default withRouter(Home)
在这里SubPages1
下面的page1
就无法展示出来和访问,如果直接输入路由也会访问页面不存在,因为page1允许的角色user
而我们角色是admin
所以无法展示。
Use Immer
yarn add use-immer
useImmer
很好的解决了ReactHooks中的赋值的性能问题,可以单独更新某个对象的某个属性。
const [state, setState] = useImmer({
menuMode: "inline",
list: {
test: "test",
otherKey: "otherKey"
}
})
const onChangeCollapse = usePersistFn((val: boolean) : void => {
setState(state => {
state.menuMode = !val ? 'inline' : 'horizontal' //只更新state.menuMode属性
})
setState(state => {
state.list.test = 'test update' //只更新state.list.test属性
})
})
上面的赋值方法也可以写到一起,效果是一样的:
setState(state => {
state.menuMode = !val ? 'inline' : 'horizontal'; //只更新state.menuMode属性
state.list.test = 'test update' //只更新state.list.test属性
})
Umi Hooks
yarn add ahooks
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
其实在我们的开发工作中,还是很常遇到的。hooks
的好处就是可以抽离公共方法,像组件一样的随意使用,对于快节奏的开发工作还是很舒服的,比如你觉得react hooks
或者 umi hooks
的api,不能满足自己的需求,也可以自己创新一些api。我这里举个例子,大家写class
组件写的很多的话,会经常用的this.setState()
,大家都知道this.setState()
是异步执行,你无法直接拿到最新的state
。hooks
中的useState
同样也是异步的,你无法直接获取到最新的state
,所以我自己写了一个useSetState
方法,用于在修改完状态后能够立即拿到最新的state
。
我们在src/hooks文件夹下新建useSetState.ts
/* hooks/useSetState.ts */
import { useState, useEffect, useRef, useCallback } from 'react'
export const useSetState = (
initialState: T = {} as T,
): [T, (patch: Partial | ((prevState: T) => Partial), cb?: Function) => void] => {
const [state, setState] = useState(initialState);
const callBack = useRef(null)
const setMergeState = useCallback(
(patch, cb) => {
callBack.current = cb;
setState((prevState) => {
if (Object.prototype.toString.call(patch).slice(8, -1) === 'Object') {
return Object.assign({}, prevState, patch)
} else {
return patch
}
});
},
[setState],
);
useEffect(() => {
callBack.current && callBack.current(state)
}, [state])
return [state, setMergeState];
};
export default useSetState
使用的方式也很简单,基本和useState一致,只是在setState的时候提供一个回调函数。
import { useSetState } from '@/hooks/useSetState' //引入
const [state, setState] = useSetState(12)
useUpdateEffect(() => {
console.log("counter change:" + counter)
setState(333, (newState: any) => {
console.log("setState的回调:", newState)
})
console.log("修改完毕后的当前数值:", state)
}, [counter])
useEffect(() => {
console.log('useEffect监听数值变化:', state)
}, [state])
这就完成了带回调的useSetState hooks
的编写,不过这种写法不太推荐在hooks
中使用,建议需要获取最新的数值都在useEffect
或者 useUpdateEffect(umi hooks)
中去。
Mobx
状态管理选择的Mobx,Mobx和Redux我都用过,不过当我习惯用Mobx后,就感觉还是Mobx更方便一些,所以更喜欢在项目中用Mobx,现在Mobx已经更新到5.0版本了,不过5.0版本并不支持ie11,所以如果想要兼容性可以选择4.0的版本,或者Redux。
这里推荐一个针对Mobx的库,mobx-react-lite
:它是基于 React 16.8 和 Hooks 的 MobX 的轻量级React绑定。
yarn add mobx mobx-react-lite
这个主要影响的是调用方法的形式,对于Mobx的书写是一样的,比如写一个加减数值:
/* stores/test/index.ts */
import { observable, action } from 'mobx'
export type CountStoreType = {
counter: number,
onIncrement: () => void,
onDecrement: () => void
};
// 观察者方式
class counterStoreClass {
@observable counter: number = 0
@action.bound
onIncrement() {
this.counter++;
}
onDecrement = () => {
this.counter--;
}
}
const counterStore: CountStoreType = new counterStoreClass();
export default counterStore;
这里你的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
加入配置:
"compilerOptions": {
...
"experimentalDecorators": true,
...
}
/* stores/index.tsx */
import { useLocalStore } from 'mobx-react-lite';
import * as React from 'react';
import { createStore, TStore } from './config';
const storeContext = React.createContext(null);
export const StoreProvider = ({ children }: any) => {
const store = useLocalStore(createStore);
return {children} ;
};
export const useStore = () => {
const store = React.useContext(storeContext);
if (!store) {
throw new Error('You have forgot to use StoreProvider.');
}
return store;
};
完毕以后,一定要把storeProvider
包裹所需要共享状态的页面,我这里直接放到app.tsx
/* app.tsx */
import { StoreProvider } from '@stores/index';
const App = () =>
<>
...
...
>
export default App;
剩下来就仅仅是调用的事情了:
import React from 'react'
import { useDebounceEffect, useMount, useUnmount, useUpdateEffect } from 'ahooks'
import { Button } from 'antd'
import { observer } from 'mobx-react-lite'
import { useStore } from '@/stores';
import { getTestApi } from '@/api/testApi'
import ButtonCom from '@/components/Button'
interface IProps { }
const SubPage: React.FC = ((): JSX.Element => {
const { counterStore } = useStore(); //引入store对象
const { counter, onIncrement, onDecrement } = counterStore // 获取属性和方法
useMount(() => {
console.log("执行了页面加载")
})
useUnmount(() => {
console.log("执行了页面卸载")
})
useUpdateEffect(() => {
console.log("counter change:" + counter)
}, [counter])
useDebounceEffect(() => {
console.log("counter debounce:" + counter)
}, [counter])
return
这是SubPages-1
count:{counter}
})
export default observer(SubPage) //observer组件
此外axios的配置应该大家都知道,所以我这也不多说了,具体在我的源码里面也有,utils下的axios.ts
拆包、多线程打包、缓存等等打包优化
加入了打包分析 webpack-bundle-analyzer
speed-measure-webpack-plugin
加入了打包进度条 webpackbar
加入了打包压缩 compression-webpack-plugin
terser-webpack-plugin
还对包进行拆包
开发环境的域名代理 devServer
加快打包速度,还可以考虑删除antd-icons,单独去iconfont网站下,按需引入。不然打包会费很多时间
/* craco.config.js */
const { POSTCSS_MODES, whenProd } = require("@craco/craco");
const CracoAliasPlugin = require("craco-alias");
const CracoAntDesignPlugin = require("craco-antd");
// 打包信息配置
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
// webpack 进度条
const WebpackBar = require('webpackbar');
// 开启gzip
const CompressionWebpackPlugin = require('compression-webpack-plugin');
// 压缩js
const TerserPlugin = require('terser-webpack-plugin');
// 分析打包时间
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const threadLoader = require('thread-loader');
const path = require("path");
const resolve = dir => path.join(__dirname, '..', dir);
const jsWorkerPool = {
workers: 2,
poolTimeout: 2000
};
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
// 打包取消sourceMap
process.env.GENERATE_SOURCEMAP = "false";
// 覆盖默认配置
module.exports = {
webpack: smp.wrap({
configure: {
/*在这里添加任何webpack配置选项: https://webpack.js.org/configuration */
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: jsWorkerPool
},
'babel-loader?cacheDirectory'
]
}]
},
resolve: {
modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
resolve('src'),
resolve('node_modules'),
],
alias: {
"@": resolve("src") // 缓存src目录为@符号,避免重复寻址
}
},
optimization: {
// 开发环境不压缩
minimize: process.env.REACT_APP_ENV !== 'development' ? true : false,
splitChunks: {
chunks: 'all', // initial、async和all
minSize: 30000, // 形成一个新代码块最小的体积
maxAsyncRequests: 5, // 按需加载时候最大的并行请求数
maxInitialRequests: 3, // 最大初始化请求数
automaticNameDelimiter: '~', // 打包分割符
name: true,
cacheGroups: {
vendors: { // 基本框架
chunks: 'all',
test: /(react|react-dom|react-dom-router|babel-polyfill|mobx)/,
priority: 100,
name: 'vendors',
},
'async-commons': { // 其余异步加载包
chunks: 'async',
minChunks: 2,
name: 'async-commons',
priority: 90,
},
commons: { // 其余同步加载包
chunks: 'all',
minChunks: 2,
name: 'commons',
priority: 80,
}
}
}
},
},
plugins: [
// webpack进度条
new WebpackBar({ color: 'green', profile: true }),
// 打包时,启动插件
...whenProd(() => [
// 压缩js 同时删除console debug等
new TerserPlugin({
parallel: true, // 多线程
terserOptions: {
ie8: true,
// 删除注释
output: {
comments: false
},
//删除console 和 debugger 删除警告
compress: {
drop_debugger: true,
drop_console: true
}
}
}),
// 开启gzip
new CompressionWebpackPlugin({
// 是否删除源文件,默认: false
deleteOriginalAssets: false
}),
// 打包分析
new BundleAnalyzerPlugin()
], [])
]
}),
style: {
// 自适应方案
postcss: {
mode: POSTCSS_MODES.file
}
},
plugins: [
// antd 按需加载 less等配置
{
plugin: CracoAntDesignPlugin,
options: {
// 自定义主题
customizeThemeLessPath: path.join(__dirname, "src/assets/styles/global.less")
}
},
// 插件方式,设置别名
{
plugin: CracoAliasPlugin,
options: {
source: "tsconfig",
tsConfigPath: "tsconfig.paths.json"
}
},
],
devServer: {
proxy: {
'/': {
target: 'www.test.com', // 开发路由代理
ws: false, // websocket
changeOrigin: true, //是否跨域
secure: false, // 如果是https接口,需要配置这个参数
pathRewrite: {}
}
}
}
};
环境配置
引入dotenv-cli
yarn add dotenv-cli
新增开发环境配置文件.env.development
和 .env.production
两个文件
/* .env.development */
# 方便打包不同接口环境
# 开发环境
# 自定义变量 必须以 REACT_APP_ 开头
PORT = 3000
NODE_ENV= development
REACT_APP_ENV = development
REACT_APP_BASE_API = ""
/* .env.production*/
# 生产环境
# 自定义变量 必须以 REACT_APP_ 开头
NODE_ENV= production
REACT_APP_ENV = production
REACT_APP_BASE_API = ""
然后修改package.json中的启动脚本:
现在yarn start
或者 yarn build
就会根据环境配置来处理。
Package.json:
还有一些细节的调整,会尽力将这个框架更加完善的。
github地址:https://github.com/Benzic/React-typescript-umihooks-mobx
欢迎star 和提意见