simker

Life is too short, just make it.


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 音乐

  • 搜索

记一次用vite搭建react的经历

发表于 2021-10-30 分类于 typescript , javascript 阅读次数: Disqus:
本文字数: 9.4k 阅读时长 ≈ 9 分钟

初衷

本篇文章只为记录一次用 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

然后安装依赖:

1
➜  my-react-ts-app yarn

然后我们可以运行yarn dev,这样一个ts-react就起来了。

加入装饰器

其实一般的项目到这里也就结束了,感觉也没什么好说的。
但是你要上ts肯定就要用上装饰器的。
而且我的大部分时间也是卡在了这里,下面我们就用一个例子来说明。
首先,准备一个class组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./src/MyClassComp.tsx
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
// ./src/MyHOC.tsx
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
// ./src/main.tsx
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
// ./src/MyClassComp.tsx
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
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
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
// ./src/mobx/testStore.ts
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
// ./src/util.ts
export function delay(time: number) {
return new Promise(resolve => {
setTimeout(resolve, time);
})
}

准备好reduce,这一步你可能不需要,因为我是从redux先学的,习惯放一个reduce

1
2
3
4
5
6
7
8
// ./src/mobx/reduce.ts
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
// ./src/main.tsx
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
// ./src/MyClassComp.tsx
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
}

// 这里注意:inject在前,observer在后,否则你会抓狂

@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
// ./src/mobx/testStore.ts
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
// ./src/mobx/testStore.ts
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
// ./src/MyFunctionComp.tsx
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
// ./src/mobx/useStores.ts
import { MobXProviderContext } from 'mobx-react'
import React from "react";
import reduce from "./reduce";

type IRootStores = typeof reduce

export 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
// ./src/MyFunctionComp.tsx
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也支持了。

共勉~

Cai xian 微信支付

微信支付

Cai xian 支付宝

支付宝

# react # vite # mobx
如何用css隐藏scrollbar
  • 文章目录
  • 站点概览
Cai xian

Cai xian

A super nice guy!
24 日志
12 分类
15 标签
  1. 1. 开始搭建
  2. 2. 加入装饰器
  3. 3. 加入Mobx
  4. 4. mobx的hooks版本
© 2019 – 2021 Cai xian | 70k | 1:04
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Pisces v7.3.0
|