很久没更新技术类博客,记得去年8月写过《不一样的思维去封装Ant-Design(一)》 后就不了了之。回忆去年,感觉又悲又喜,悲的是“已无独处,静下心去思考
”的时间,(我有个习惯,如果周围不是安静的状态,我基本写不出什么干货);喜的是“每天有家人的陪伴
,女儿活泼乖巧、老婆善解人意,妈妈身体健康,忙碌且幸福”。
拥有孩子后,每天都是“热热闹闹”的,想回到之前“平静”、“安逸”的生活基本不可能。感慨一下,快30岁的人看起来像20岁的大学生,拥有16岁的爱玩心态,还怀有3岁小孩的好奇心,就是现在的我。
最近在关注和学习如何做好资产配置
和什么是增额终身寿险
等,尽管自己目前并没多少资产可言,也不知道学它们对自己到底有多大帮助,但是理解和接触下这些知识总是好的。
回到前端技术本身,当今各大浏览器引擎都支持原生JavaScript模块
。 一切前端框架都依赖原生JavaScript API ,如:vue2.x通过Object.defineProperty
的set
和get
监听来实现双向绑定;vue3.x通过Proxy(代理) & Reflect(反射)
来实现双向绑定;react16使用requestIdleCallback
和requestAnimationFrame
实现Fiber调度和性能优化…
2022年的今天,和前年比,前端框架也有很多的升级。 react从16升级到最新18,react-router从5升级到最新的6,mbox从4升级到最新的6。 构建工具从原来webpack、rollup、parcel构建工具到新一代构建工具的vite、esbuild、snowpack、wmr。 升级构建工具和框架到最新的好处:拥有更好的交互体验,解决框架自身遗留问题,开发者能用新的功能特性
。 升级构建工具和框架到最新的难处:老项目很难直接升级最新,兼容性问题难处理
。
最佳解决方案是,从无到有搭建一套属于自己的UI库。(目前全用最新版本,不需要关心如何兼容老项目,后续新项目能用就行。)
UI库可以理解成脚手架+UI组件,今天的主要内容是通过vite
脚手架实现项目的基本结构。
基于最新node版本v18.4.0
环境开发
typescript:4.7.4
,最新版本
react相关:react、react-dom:18.2.0
,最新版本
路由相关:react-router、react-router-dom:6.3.0
,最新版本
状态管理相关:mobx:6.6.1
、 mobx-react-lite:3.4.0
,最新版本
UI库相关:antd:4.21.4
,最新版本
文章目录会按照以下顺序介绍和搭建:
引入Vite(脚手架)
1.Vite生产环境为什么选择Rollup做构建工具
2.Vite为什么不用Rollup的热更新
3.Vite为什么不用Webpack
4.引入最新Vite(完成项目搭建)
引入最新Mobx(状态管理)
引入最新antd(UI库)
引入最新react-router(路由)
整合它们实现简单demo
1.项目相对路径支持@
别名
2.引入antd库国际化
3.初始化react-router
4.初始化mbox,集成mobx-react-lite
5.设置路由react-router
一.引入Vite(脚手架) Vite是一个由原生ESM驱动的Web开发构建工具
。开发环境下使用原生ESM imports
,生产环境下使用Rollup打包
。
Vite可以理解成一个脚手架工具,Vite生成环境依赖的Rollup
是构建工具,类似Webpack。
1.Vite生产环境为什么选择Rollup做构建工具? Vite是一个由原生ESM驱动的Web开发构建工具。在选择构建工具的时候也最好可以选择基于ESM的工具。
Rollup是基于ES2015的JavaScript打包工具。它将小文件打包成一个大文件或者更复杂的库和应用,打包既可用于浏览器和Node.js使用。 Rollup最显著的地方就是能让打包文件体积很小
。相比其他JavaScript打包工具,Rollup总能打出更小,更快的包。因为Rollup基于ES2015模块,比Webpack和Browserify使用的CommonJS模块机制更高效。
2.Vite为什么不用Rollup的热更新? Vite开发模式单独实现了一套热更新(HMR - Hot Module Replacement),可是从Rollup Awesome中可以发现,Rollup有热更新插件nollup。为什么Vite不用Rollup的热更新呢?
从Vite的README,我们可以发现:
1 Vite was created to tackle native ESM-based HMR. When Vite was first released with working ESM-based HMR, there was no other project actively trying to bring native ESM based HMR to production.
也就是说Vite是第一个发布基于纯ESM的热更新。当时Rollup还没有纯ESM的热更新。
3.Vite为什么不用Webpack? Webpack和Rollup功能差不多,以前有种说法是应用开发用Webpack,库开发用Rollup。但是现在Webpack也支持Tree shaking,Rollup也有热更新,而且都有强大的插件开发功能。二者的功能差异越来越模糊。 二者更多的区别是在写法上。 如下是Rollup的配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // rollup .config.js import babel from 'rollup-plugin-babel' ;export default { input : './src/index.js' , output: { file: './dist/bundle.rollup.js' , format : 'cjs' }, plugins: [ babel({ presets: [ [ 'es2015' , { modules: false } ] ] }) ] }
下面是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 const path = require ('path' );const webpack = require ('webpack' );module .exports = { entry : { 'index.webpack' : path.resolve('./src/index.js' ) }, output : { libraryTarget : "umd" , filename : "bundle.webpack.js" , }, module : { rules : [ { test : /\.js$/ , exclude: /node_modules/ , loader: 'babel-loader' , query : { presets : ['es2015' ] } } ] } }
可以看出:
Rollup使用新的ESM,而Webpack用的是旧的CommonJS。
Rollup支持相对路径,webpack需要使用path模块。Rollup使用起来更简洁,并且Rollup打出更小体积的文件,所以Rollup更适合Vite
。
4.引入最新Vite(完成项目搭建)
兼容性注意Vite 需要 Node.js 版本 >= 14.18.0。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
因此先安装最新的node,当前最新node版本是v18.4.0:
node升级后,安装vite:
选择react-ts,后自动生成项目。安装node_modules,运行项目:
二、引入最新Mobx(状态管理) MobX 有两种 React 绑定方式,其中mobx-react-lite
仅支持函数组件,mobx-react
还支持基于类的组件。我选择mobx-react-lite
。
mobx6和mobx4主要区别是放弃使用装饰器@action
、@observable
、@computed
等。主要原因是装饰器语法其实已经出来很久了,但一直未纳入ES标准,出于兼容性的考虑,建议使用makeObservable
/ makeAutoObservable
代替。
三、引入最新antd(UI库)
四、引入最新react-router(路由) react-router6和react-router5主要区别是废弃老组件、hooks,使用新组件、hooks。如以前的Switch
、Redirect
、useHistory
都不能使用,新的hooks如useNavigate
。
1 npm install react- router- dom@6
五、整合它们实现简单demo 截止目前,react、ts、mobx、antd、react-router已经基本可实现项目的基本结构。
app.tsx:
1 2 3 <React.StrictMode> <App /> </React.StrictMode>
补充一下:<React.StrictMode>
包裹的组件包括其内所有的后代会被检查到,StrictMode的目的:
识别具有不安全生命周期的组件
有关旧式字符串ref用法的警告
检测意外的副作用
检测遗留 context API
1.项目相对路径支持@
别名 更改index.html,id,个性化自己的,适合当前公司的名字:
1 <div id ="bee-logistic" ></div >
更改main.tsx,渲染的dom,id:
1 ReactDOM . createRoot(document .getElementById ('root ') !)...
1 ReactDOM . createRoot(document .getElementById ('bee -logistic ') !)...
加强tsconfig.json,配置参数demo如下:http://json.schemastore.org/tsconfig :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "compilerOptions" : { "target" : "ESNext" , "useDefineForClassFields" : true , "lib" : ["DOM" , "DOM.Iterable" , "ESNext" ], "allowJs" : false , "skipLibCheck" : true , "esModuleInterop" : false , "allowSyntheticDefaultImports" : true , "strict" : true , "forceConsistentCasingInFileNames" : true , "module" : "ESNext" , "moduleResolution" : "Node" , "resolveJsonModule" : true , "isolatedModules" : true , "noEmit" : true , "jsx" : "react-jsx" }, "include" : ["src" ], "references" : [{ "path" : "./tsconfig.node.json" }] }
compilerOptions里面新增baseUrl
和paths
:
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 { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : ["src/*" ] }, "target" : "ESNext" , "useDefineForClassFields" : true , "lib" : ["DOM" , "DOM.Iterable" , "ESNext" ], "allowJs" : false , "skipLibCheck" : true , "esModuleInterop" : false , "allowSyntheticDefaultImports" : true , "strict" : true , "forceConsistentCasingInFileNames" : true , "module" : "ESNext" , "moduleResolution" : "Node" , "resolveJsonModule" : true , "isolatedModules" : true , "noEmit" : true , "jsx" : "react-jsx" }, "include" : ["src" ], "references" : [{ "path" : "./tsconfig.node.json" }] }
引入node模块类型的ts声明:
1 npm install @types/node --save-dev
引入node的path模块,更改vite.config.ts
1 2 3 4 5 6 7 import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https:// vitejs.dev/config/ export default defineConfig({ plugins: [react()] })
1 2 3 4 5 6 7 8 9 10 11 12 13 import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { join } from "path" ;export default defineConfig({ plugins: [react()], resolve: { alias: { '@' : join (__dirname, "src" ), } } })
改好后,之前需要相对路径引入文件可以用@
符号替换:
1 2 import App from './pages/App' import logo from './../assets/images/logo.svg'
1 2 import App from '@/pages/App' import logo from '@/assets/images/logo.svg'
2.引入antd库国际化 更改app.tsx:
1 2 3 <React.StrictMode> <App /> </React.StrictMode>
1 2 3 4 5 6 7 8 import { ConfigProvider } from 'antd' import zhCN from "antd/es/locale/zh_CN" <React.StrictMode> <ConfigProvider locale ={zhCN} > <App /> </ConfigProvider > </React.StrictMode>
3.初始化react-router 更改app.tsx:
1 2 3 4 5 <React.StrictMode > <ConfigProvider locale ={zhCN} > <App /> </ConfigProvider > </React.StrictMode >
1 2 3 4 5 6 7 8 9 import { BrowserRouter } from "react-router-dom" <React.StrictMode> <BrowserRouter> <ConfigProvider locale={zhCN}> <App /> </ConfigProvider> </BrowserRouter> </React.StrictMode>
4.初始化mbox,集成mobx-react-lite 选择集成轻量化的mobx-react-lite
而非mobx-react是因为打算只用函数式组件去写代码,并不会用到类组件。 我相信未来也会是这个趋势,函数组件基本会替代类组件。
1.更改app.tsx:
1 2 3 4 5 6 7 <React.StrictMode > <BrowserRouter > <ConfigProvider locale ={zhCN} > <App /> </ConfigProvider > </BrowserRouter > </React.StrictMode >
1 2 3 4 5 6 7 8 9 10 11 import { StoreProvider } from "@/store/index" <React.StrictMode> <StoreProvider> <BrowserRouter> <ConfigProvider locale={zhCN}> <App /> </ConfigProvider> </BrowserRouter> </StoreProvider> </React.StrictMode>
2.实现StoreProvider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React, { createContext, useContext } from "react" import { useLocalObservable, } from "mobx-react-lite" import createStore from "./store" type StoreType = ReturnType<typeof createStore>const storeContext = createContext<StoreType>(null )export const StoreProvider = ({ children }: any ) => { const { Provider } = storeContext const store = useLocalObservable(createStore) return <Provider value ={store} > {children}</Provider > } export const useStore = () => { const store = useContext(storeContext) if (!store) { return useContext(storeContext) } return store }
3.声明总store:
1 2 3 4 5 6 7 import testStore from "./workbench/test" export default function createStore ( ) { return { testStore, } }
4.声明子store:
1 2 3 4 5 6 7 8 9 10 11 import { makeAutoObservable } from "mobx" class TestStore { constructor ( ) { makeAutoObservable(this ) } count = 0 increase ( ) {this .count += 1 } } export default new TestStore()
5.在子组件使用store:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { observer, } from "mobx-react-lite" import { useStore } from '@/store' function Header ( ) { const { testStore } = useStore() return ( <header className ="App-header" > <button type ="button" onClick ={() => testStore.increase()}> count is: {testStore.count} </button > </header > ) }
6.认识mbox核心:useLocalObservable
及makeAutoObservable
API
1 2 useLocalObservable 等价于 const [store] = useState(() => observable({ }))
makeAutoObservable 好处:自动注入注解,无需重复声明action、observable等。 不足:该类不能继承父类。 推断规则: 所有 自有属性
都成为 observable
。 所有 getters
都成为 computed
。 所有 setters
都成为 action
。 所有 prototype 中的 functions
都成为 autoAction
。 所有 prototype 中的 generator functions
都成为 flow
。 在 overrides
参数中标记为 false
的成员将不会被添加注解。例如,将其用于像标识符这样的只读字段。
1 2 3 4 class 类里面构造器加makeAutoObservableconstructor ()
5.设置路由react-router 最外层,已经设置过BrowserRouter。 1.在App.tsx中设置主路由和404
1 2 3 4 5 6 7 8 9 10 11 12 import { Routes, Route, } from 'react-router-dom' import Header from '@/pages/Header' import NotFound from '@/pages/layout/404' function App ( ) { return ( <Routes > <Route path ="/" element ={ <div className ="App" > <Header /> </div > } /> <Route path ="*" element ={ <NotFound /> } /> </Routes > ) }
2.通过React.lazy
和Suspense
配合一起用,能够实现动态加载组件的效果
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 import React, { lazy, Suspense } from 'react' import { Spin } from 'antd' const Header = lazy(() => import ('@/pages/Header' ))function BSuspense ({ children } ) { return ( <Suspense fallback ={ <div className ="ui-layout-loading" > <Spin tip ={ "加载中... "} /> </div > }> {children} </Suspense > ) } function App ( ) { return ( <Routes > <Route path ="/" element ={ <BSuspense > <Header /> </BSuspense > } /> <Route path ="*" element ={ <NotFound /> } /> </Routes > ) }
效果如下:
3.结合store实现登录和工作台之间的切换(仿登录逻辑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { observer } from "mobx-react-lite" import { useStore } from '@/store' const Header = lazy(() => import ('@/pages/Header' ))const Workbench = lazy(() => import ('@/pages/Workbench' ))function App ( ) { const { authStore } = useStore() return ( <Routes > <Route path ="/" element ={ <BSuspense > {authStore.isLogin ? <Workbench /> : <Header /> }</BSuspense > } /> <Route path ="*" element ={ <NotFound /> } /> </Routes > ) }
声明的auth.ts:
1 2 3 4 5 6 7 8 9 10 11 import { makeAutoObservable } from "mobx" class AuthStore { constructor ( ) { makeAutoObservable(this ) } isLogin = false setLogin (login ) {this .isLogin = login} } export const authStore = new AuthStore()
声明的Header.tsx:(一个页面使用多个store的情况)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import logo from '@/assets/images/logo.svg' import { observer, } from "mobx-react-lite" import { useStore } from '@/store' function Header ( ) { const { testStore, authStore, } = useStore() return ( <div className ="App" > <header className ="App-header" > <img src ={logo} className ="App-logo" alt ="logo" /> <p > Hello Vite + React!</p > <p > <button type ="button" onClick ={() => testStore.increase()}> count is: {testStore.count} </button > </p > <p > <button type ="button" onClick ={() => authStore.setLogin(true)}>login</button > </p > </header > </div > ) } export default observer(Header)
声明的Workbench.tsx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import logo from '@/assets/images/logo.svg' import { observer, } from "mobx-react-lite" import { useStore } from '@/store' function Workbench ( ) { const { authStore, } = useStore() return ( <header className ="App-header" > <img src ={logo} className ="App-logo" alt ="logo" /> <p > Workbench</p > <p > <button type ="button" onClick ={() => authStore.setLogin(false)}>back to home</button > </p > </header > ) } export default observer(Workbench)
效果如下: