初衷
本篇文章只为记录一次用 vite 搭建react的经历与踩坑。
背景
公司要开发一个新的项目,需要和别的部门合作一起开发,但是我们(当时)目前的技术栈是用CRA搭建的,主要是react管dom,mobx管数据,react-router管路由等,而且大部分组件都还在class组件。算是比较老了。 而对方那边的项目则是用vite构建的,而且上了ts。虽然自己当时已经有使用CRA搭建react-ts的经验了,但是还是在路上踩了不少坑,以为方便。
开始搭建 首先我们用vite创建项目:
1 yarn create vite my-react-ts-app --template react-ts
然后我们进入目录,目录结构是长这样的:
1 2 3 ➜ ~ cd my-react-ts-app ➜ my-react-ts-app ls index.html package.json src tsconfig.json vite.config.ts
然后安装依赖:
然后我们可以运行yarn dev,这样一个ts-react就起来了。
加入装饰器 其实一般的项目到这里也就结束了,感觉也没什么好说的。 但是你要上ts肯定就要用上装饰器的。 而且我的大部分时间也是卡在了这里,下面我们就用一个例子来说明。 首先,准备一个class组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , { Component } from 'react' ;class MyClassComp extends Component <{}, {}> { render ( ) { return ( <div > My Class Comp </div > ); } } export default MyClassComp ;
然后,我们来写一个高阶组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { ComponentType , useEffect } from "react" ;const MyHOC = <P extends {}>(Comp : ComponentType <P> ) => { return (props : P ) => { useEffect (() => { console .log (1 ); }, []) return <Comp {...props } /> } } export default MyHOC ;
上面我们写了一个简单的HOC,用来增强组件,即每次成功渲染之后都打印一个1。
基本的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import MyClassComp from './MyClassComp' import MyHOC from "./MyHOC" ;const HOCApp = MyHOC (MyClassComp );ReactDOM .render ( <React.StrictMode > <HOCApp /> </React.StrictMode > , document .getElementById ('root' ) )
渲染页面看到打印1就是成功使用了,但是我们不想要这样,因为如果参数过多的话,代码就不是很好看了,再者,从逻辑上说,增强组件的功能在组件内部也有提现,比如像withRouter这样的高阶组件。所以我们还是需要装饰器的写法更为优雅。
ok, 我们把./src/main.tsx 还原,在./src/MyClassComp.tsx用装饰器试试.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { Component } from 'react' ;import MyHOC from "./MyHOC" ;@MyHOC class MyClassComp extends Component <{}, {}> { render ( ) { return ( <div > My Class Comp </div > ); } } export default MyClassComp ;
然后我们运行会发现有一个报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [vite] Internal server error: ~/my-react-ts-app/src/App.tsx: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (6:1): 4 | import MyHOC from "./MyHOC"; 5 | > 6 | @MyHOC | ^ 7 | function App() { 8 | const [count, setCount] = useState(0) 9 | Plugin: vite:react-babel File: ~/my-react-ts-app/src/App.tsx 3 | import './App.css' 4 | import MyHOC from "./MyHOC"; 5 | | ^ 6 | @MyHOC 7 | function App() {
这里是说我们没有装饰器的插件decorators-legacy,而且报错标注了是react-babel的。我们去vite.config.ts配置一下。
1 2 3 4 5 6 7 8 9 10 11 12 import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig ({ plugins : [react ({ babel : { parserOpts : { plugins : ['decorators-legacy' , 'classProperties' ]} } })] })
可以看到这里与CRA不同的点,cra需要你自己安装babel插件来转义,我们这里没有用任何的插件,因为vite内置了,这也是对开发者友好的一个点。
配置完成之后,你可以看到页面完美运行,且高阶组件也生效了。
加入Mobx 因为公司这边是用mobx管理状态的,这边我也花了一点时间在这上面,这里我也记下。
首先我们准备好mobx。
安装必要的库。
1 yarn add mobx mobx-react
准备好一个测试的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 25 26 27 28 29 30 31 32 33 34 import { observable, action } from "mobx" ;import { delay } from "../util" ;class TestStore { @observable num = 0 ; @observable loading = false ; constructor ( ) { this .reset () } @action .bound add ( ) { this .num ++ } @action .bound async asyncAdd () { if (this .loading ) return ; this .loading = true ; await delay (3000 ) this .num ++ this .loading = false } @action .bound reset ( ) { this .num = 0 ; this .loading = false ; } } export default new TestStore ();
用一个工具文件来存储常用函数:
1 2 3 4 5 6 export function delay (time : number ) { return new Promise (resolve => { setTimeout (resolve, time); }) }
准备好reduce,这一步你可能不需要,因为我是从redux先学的,习惯放一个reduce
1 2 3 4 5 6 7 8 import testStore from "./testStore" ;const store = { testStore } export default store
把store应用到react上面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' import { Provider } from 'mobx-react' import store from "./mobx/reduce" ;ReactDOM .render ( <React.StrictMode > <Provider {...store }> <App /> </Provider > </React.StrictMode > , document .getElementById ('root' ) )
以上这些准备工作,你觉得不做过多的解释,需要了解的可以去Mobx.js官网 查看
OK,准备工作做完了,你打算在组件里面运用上mobx的管理。
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 27 28 29 30 31 32 33 import React , { Component } from 'react' ;import MyHOC from "./MyHOC" ;import { inject, observer } from "mobx-react" ;import testStore from "./mobx/testStore" ;interface IProps { testStore ?: typeof testStore } @MyHOC @inject ('testStore' ) @observer class MyClassComp extends Component <IProps , {}> { render ( ) { const {num, loading, add, asyncAdd, reset} = this .props .testStore ; return ( <div > My Class Comp <p > {num}</p > <p > {loading ? 'loading': null}</p > <p > <button onClick ={() => add()}>add 1</button > </p > <p > <button onClick ={() => asyncAdd()}>async add 1</button > </p > <p > <button onClick ={() => reset()}>reset num</button > </p > </div > ); } } export default MyClassComp ;
如果你这时候运行的话,页面渲染看起来是没有什么问题的,为什么是看起来没有问题,因为你点击一下add 1 按钮,你就会发现,控制台报错。我能料想到,差不多是这样的:
1 2 3 4 5 6 7 8 9 10 11 action.ts:68 Uncaught TypeError: Cannot read properties of undefined (reading 'num') at add (testStore.ts:18) at executeAction (action.ts:65) at add (action.ts:46) at onClick (MyClassComp.tsx:22) at HTMLUnknownElement.callCallback2 (react-dom.development.js:3945) at Object.invokeGuardedCallbackDev (react-dom.development.js:3994) at invokeGuardedCallback (react-dom.development.js:4056) at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4070) at executeDispatch (react-dom.development.js:8243) at processDispatchQueueItemsInOrder (react-dom.development.js:8275)
你通过了解报错信息发现是读取num这个属性引发的,因为读取的对象是undefined,结合我们点击add 1按钮来看,你觉得应该是15行这里出错了:
1 2 3 4 13 @action.bound 14 add() { ^15 this.num++ 16 }
这里的this取不到值,那我们就换成箭头函数,让this不用指向上下文,而是指向store本身。
换完之后,你再点击的时候就没有报错了。终于正常了,到这里似乎一切还顺利,但是你很快就发现,无论你怎么点击,页面上渲染的值还是0,没有变化。
不对啊,我们明明在组件上进入了观察者模式才对啊。
这里需要另外一个api来修复这个了。我们在store的constructor里面加入一句话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { observable, action, makeAutoObservable } from "mobx" ;import { delay } from "../util" ;class TestStore { @observable num = 0 ; @observable loading = false ; constructor ( ) { makeAutoObservable (this ) this .reset () } }
并且我们可以移除所有的@action.bound。
我们在一次点击的时候你就可以看到页面上的num是正常渲染了。
但是,这时候细心的你一定还是发现了控制台的警告:
1 Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: TestStore@1.num
你发现这是点击async add引发的问题。查阅官网之后你发现,需要要用这样一个api:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { observable, makeAutoObservable } from "mobx" ;import { delay } from "../util" ;class TestStore { asyncAdd = async () => { if (this .loading ) return ; this .loading = true ; await delay (3000 ) runInAction (() => { this .num ++ this .loading = false }) } }
之后的数据管理终于没有感叹号发生了,大功告成!
mobx的hooks版本 上述的一切都是简历在class component上面的,但是与我们一起开发的还有其他的同事,怎么的也得把hooks给支持了吧
得,你又写了一个function component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { Component } from 'react' ;const MyFunctionComponent = ( ) => { const {num, loading, add, asyncAdd, reset} = testStore; return ( <div > My Class Comp <p > {num}</p > <p > {loading ? 'loading': null}</p > <p > <button onClick ={() => add()}>add 1</button > </p > <p > <button onClick ={() => asyncAdd()}>async add 1</button > </p > <p > <button onClick ={() => reset()}>reset num</button > </p > </div > ); } export default MyFunctionComponent ;
这里,你知道,运行起来肯定报错,因为testStore都没有定义。怎么拿到自己的store呢?
查阅官网之后你发现一个解决方案。
1 2 3 4 5 6 7 8 9 10 import { MobXProviderContext } from 'mobx-react' import React from "react" ;import reduce from "./reduce" ;type IRootStores = typeof reduceexport default function useStores ( ): IRootStores { return React .useContext (MobXProviderContext ) as IRootStores }
有了这个hooks,我们就可以在组件里面这样定义store了:
1 2 const {testStore} = useStores ();const {num, loading, add, asyncAdd, reset} = testStore;
运行之后,你发现点击按钮没有反应,页面渲染的num始终是0
这时候,你仔细查看代码发现没有进入观察者模式。于是你加上observer:
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 27 28 29 import React from 'react' ;import useStores from "./mobx/useStores" ;import { observer } from "mobx-react" ;const MyFunctionComponent = observer (() => { const { testStore } = useStores () const { num, loading, add, asyncAdd, reset } = testStore; return ( <div > My Class Comp <p > {testStore.num}</p > <p > {loading ? 'loading' : null}</p > <p > <button onClick ={() => add()}>add 1</button > </p > <p > <button onClick ={() => asyncAdd()}>async add 1</button > </p > <p > <button onClick ={() => reset()}>reset num</button > </p > </div > ); }) export default MyFunctionComponent ;
于此,对于hooks也支持了。
共勉~