很久没更新技术类博客,记得去年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
- 1.项目相对路径支持
一.引入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,我们可以发现:
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的配置文件:
// 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的配置文件:
// webpack.config.js
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:
nvm install v18.4.0
node升级后,安装vite:
npm create vite@latest
选择react-ts,后自动生成项目。安装node_modules,运行项目:
npm i
npm run dev
二、引入最新Mobx(状态管理)
MobX 有两种 React 绑定方式,其中mobx-react-lite
仅支持函数组件,mobx-react
还支持基于类的组件。我选择mobx-react-lite
。
mobx6和mobx4主要区别是放弃使用装饰器@action
、@observable
、@computed
等。主要原因是装饰器语法其实已经出来很久了,但一直未纳入ES标准,出于兼容性的考虑,建议使用makeObservable
/ makeAutoObservable
代替。
npm install --save mobx mobx-react-lite
三、引入最新antd(UI库)
npm install --save antd
四、引入最新react-router(路由)
react-router6和react-router5主要区别是废弃老组件、hooks,使用新组件、hooks。如以前的Switch
、Redirect
、useHistory
都不能使用,新的hooks如useNavigate
。
npm install react-router-dom@6 --save
五、整合它们实现简单demo
截止目前,react、ts、mobx、antd、react-router已经基本可实现项目的基本结构。
app.tsx:
<React.StrictMode>
<App />
</React.StrictMode>
补充一下:<React.StrictMode>
包裹的组件包括其内所有的后代会被检查到,StrictMode的目的:
- 识别具有不安全生命周期的组件
- 有关旧式字符串ref用法的警告
- 检测意外的副作用
- 检测遗留 context API
1.项目相对路径支持@
别名
更改index.html,id,个性化自己的,适合当前公司的名字:
<div id="root"></div>
<div id="bee-logistic"></div>
更改main.tsx,渲染的dom,id:
ReactDOM.createRoot(document.getElementById('root')!)...
ReactDOM.createRoot(document.getElementById('bee-logistic')!)...
加强tsconfig.json,配置参数demo如下:http://json.schemastore.org/tsconfig:
{
"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
:
{
"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声明:
npm install @types/node --save-dev
引入node的path模块,更改vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { join } from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': join(__dirname, "src"),
}
}
})
改好后,之前需要相对路径引入文件可以用@
符号替换:
import App from './pages/App'
import logo from './../assets/images/logo.svg'
import App from '@/pages/App'
import logo from '@/assets/images/logo.svg'
2.引入antd库国际化
更改app.tsx:
<React.StrictMode>
<App />
</React.StrictMode>
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:
<React.StrictMode>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</React.StrictMode>
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:
<React.StrictMode>
<BrowserRouter>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</BrowserRouter>
</React.StrictMode>
import { StoreProvider } from "@/store/index"
<React.StrictMode>
<StoreProvider>
<BrowserRouter>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</BrowserRouter>
</StoreProvider>
</React.StrictMode>
2.实现StoreProvider:
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:
import testStore from "./workbench/test"
export default function createStore() {
return {
testStore,
}
}
4.声明子store:
import { makeAutoObservable } from "mobx"
class TestStore{
constructor() {
makeAutoObservable(this)
}
count = 0
increase(){this.count += 1}
}
export default new TestStore()
5.在子组件使用store:
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
useLocalObservable 等价于
const [store] = useState(() => observable({ /* something */}))
makeAutoObservable
好处:自动注入注解,无需重复声明action、observable等。
不足:该类不能继承父类。
推断规则:
所有 自有属性
都成为 observable
。
所有 getters
都成为 computed
。
所有 setters
都成为 action
。
所有 prototype 中的 functions
都成为 autoAction
。
所有 prototype 中的 generator functions
都成为 flow
。
在 overrides
参数中标记为 false
的成员将不会被添加注解。例如,将其用于像标识符这样的只读字段。
class类里面构造器加makeAutoObservable
constructor() {
makeAutoObservable(this)
}
5.设置路由react-router
最外层,已经设置过BrowserRouter。 1.在App.tsx中设置主路由和404
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
配合一起用,能够实现动态加载组件的效果
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实现登录和工作台之间的切换(仿登录逻辑)
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:
import { makeAutoObservable } from "mobx"
class AuthStore{
constructor() {
makeAutoObservable(this)
}
isLogin = false
setLogin(login){this.isLogin = login}
}
export const authStore = new AuthStore()
声明的Header.tsx:(一个页面使用多个store的情况)
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:
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)
效果如下: