React 从 CRA 到 Vite 迁移笔记

如果你还没听说过 Vite.js ,那你应该去试一试。

Vite 提供了一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)

在开始改造之前,先来看一下本项目用了哪些东西:

  • Create React App (CRA) 创建项目
  • SASS
  • react-app-rewired 启动
  • customize-cra 自定义 CRA 配置,其中包括别名定义
  • .js 后缀的组件
  • Mobx,且用到了 @ 装饰器的方式来使用
  • antd v4.17.3
  • CRA 的 .env 文件配置了 PUBLIC_URL
  • process.env.NODE_ENV

下面是迁移步骤:

  1. 安装 Vite
  2. 更改文件
  3. SASS
  4. Mobx 改造
  5. vite.config.js 配置
  6. 变量使用

1 安装 Vite

打开通过 CRA 创建的项目并进入命令行,执行命令,安装 vite@vitejs/plugin-reactdevDependencies

# npm
$ npm install -D vite @vitejs/plugin-react

# yarn
$ yarn add -D vite @vitejs/plugin-react

更新 package.json 中的 scripts

  "scripts": {
-    "start": "react-app-rewired start",
-    "build": "react-app-rewired build",
-    "test": "react-app-rewired test",
-    "eject": "react-scripts eject"
+    "start": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
  },

如果需要使用 Typescript 则可以配置 "build": "tsc && vite build"

2. 更改文件

index.html 文件

文件 index.html 移动到根目录,并且移除 index.html 中的所有 %PUBLIC_URL%

-  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+  <link rel="icon" href="/favicon.ico" />
-  <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+  <link rel="apple-touch-icon" href="/logo192.png" />
-  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+  <link rel="manifest" href="/manifest.json" />

并且在 body 标签内追加代码

   <noscript>You need to enable JavaScript to run this app.</noscript>
   <div id="root"></div>
+  <script type="module" src="/src/index.jsx"></script>

如果是 Typescript 则使用 <script type="module" src="/src/index.tsx"></script>

更改 .js 为 .jsx 后缀

在 vite 中需要将 .js 后缀的组件改成 .jsx.tsx 后缀,参考:react支持.js后缀文件 · Issue #1552 · vitejs/vite (github.com)

3. SASS

如果项目使用了 sass,则需要执行命令进行安装:

# npm
$ npm install -D sass

# yarn
$ yarn add -D sass

如果 scss 文件里面引入了一些 node_modules 的 css 是使用 ~ 符号的,可以做出调整:

@import '~antd/dist/antd.css';
@import '~react-perfect-scrollbar/dist/css/styles.css';

调整为

@import 'antd/dist/antd.css';
@import 'react-perfect-scrollbar/dist/css/styles.css';

可以参考 issue - Cannot import CSS from node_modules using "~" pattern

4. Mobx 使用方式改造

去除装饰器

由于 esbuild 本身不支持装饰器,问题参考:decorators not support in js for prebuild · Issue #2349 · vitejs/vite (github.com),而本人的 JavaScript 项目使用了 mobx 的装饰器 @,本来期望通过配置 babel 来解决但没有生效。

关于在 CRA 中启用装饰器主要是用到两个插件: @babel/plugin-proposal-decorators@babel/plugin-proposal-class-properties,这里列出参考的链接:

经过一系列尝试,最终还是决定移除装饰器,需将 Mobx 里面用到的装饰器改成函数调用的形式,mobx用法改写过程可以参考链接,最新版的 mobx(本机版本:"mobx": "^6.3.12")可以参考官方示例 - React integration · MobX,下面列出本人的调整方式。

Store 的创建方式调整

项目的 Mobx 版本如下:

"mobx": "^6.3.12",
"mobx-react": "^7.2.1",

现在需要将使用到的装饰器移除,如下:

import { observable, action } from "mobx";

class ArticleStore {
    @observable articleList = [];

    @observable articlePage = 1;
    articlePageSize = 10;
    @observable articleCount = 0;

    @action async getArticleList(page = 1) {
        // ...
    }

    @action async unshiftArticle(data) {
        // ...
    }
}
export default ArticleStore;


// 改造后
import { observable, computed, runInAction, action, makeObservable, makeAutoObservable } from "mobx"

class ArticleStore {
  articleList = []

  articlePage = 1
  articlePageSize = 10
  articleCount = 0

  constructor() {
    makeObservable(this, {
      articleList: observable,
      articlePage: observable,
      articleCount: observable,
      getArticleList: action,
      unshiftArticle: action
    })
    // makeAutoObservable(this)
  }

  async getArticleList(page = 1) {
    // ...
    const result = await ...
    // ...
    runInAction(() => {
      // ...
    })
  }

  unshiftArticle(data) {
    // ...
  }
}

export default ArticleStore

关于报错

MobX: Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed

参考链接MobX: Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed

解决方法:如果一个 action 方法使用到了 async 的话,则需要写一个 runInAction 在内部来修改 observable 属性,或者调用另外一个 action 方法修改,或者使用其他方式来实现 async actions:

import { runInAction, makeAutoObservable } from "mobx"

class AuthStoreClass {
    authUser = null

    constructor() {
        makeAutoObservable(this)
    }

    login = async (params) => {
        const { data: { data: authUser } } = await loginUser(params)
        
        runInAction(() => {
          this.authUser = authUser
        })
        // 或者使用独立方法
        this.setUser(authUser)
    }

    // 这个方法将被自动封装成`action`,因为使用了 `makeAutoObservable`
    setUser = (user) => {
        this.authUser = authUser
    }
}

组件中 Observer 和 Inject 的改造

mobx-react 文档中提到,新的编码方式已不需要使用 Provider / inject ,可以参考 mobx 官方示例或使用 React.createContext 来传递 store

Note: usually there is no need anymore to use Provider / inject in new code bases; most of its features are now covered by React.createContext.

但由于本人项目从一开始的装饰器更改过来,继续采取 Provider / inject 更便于修改,来看页面组件中的修改:

import React from 'react';
import { observer, inject } from "mobx-react";

@inject("userStore", "articleStore")
@observer
class HomePage extends React.Component {
    render() {
    }
}
export default HomePage


// 改造后
import React from 'react';
import { observer, inject } from "mobx-react";

class HomePage extends React.Component {
    render() {
    }
}

export default inject('userStore', 'articleStore')(observer(HomePage))




// 搭配使用 react-router-dom 时
export default withRouter(inject('userStore')(observer(RouterPage)))

Provider 使用方式

// 定义多个 store 示例如下
import HomeStore from "./homeStore";
import UserStore from "./userStore";
import ArticleStore from "./articleStore";
import FileStore from "./fileStore";
let homeStore = new HomeStore();
let userStore = new UserStore();
let articleStore = new ArticleStore();
let fileStore = new FileStore();
const stores = {
  homeStore, userStore, articleStore, fileStore
};
// 默认导出接口
export default stores;


// 在 `App.jsx` 中的使用示例:
import React from 'react';
import { HashRouter as Router } from "react-router-dom";


import { Provider } from "mobx-react";
import stores from "./store";

import RouterPage from './pages/RouterPage';
import './App.scss';

function App() {
  return (
    <Provider {...stores}>
      <div className="App">
        <Router><RouterPage /></Router>
      </div>
    </Provider>
  );
}

export default App;

参考文档

segmentfault - react-mobx6+使用案例

mobx-react - inject-as-function

mobx - The gist of MobX

https://dev.to/rosyshrestha/build-your-first-app-with-mobx-and-react-4896

掘金 - Mobx React 初学者入门指南

stackoverflow - How to get MobX Decorators to work with Create-React-App v2?

5. vite.config.js 配置

在项目根目录新建一个 vite.config.js 文件,内容如下:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()]
})

修改 base 路径

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  plugins: [react()]
})

配置别名

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const { resolve } = require('path') //必须要引入resolve 

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  resolve: {
    alias: {
      '@components': resolve(__dirname, 'src', 'components'),
      '@utils': resolve(__dirname, 'src', 'utils'),
      '@config': resolve(__dirname, 'src', 'config'),
    },
  },
  plugins: [react()]
})

jsxRuntime 更改

经过上面的迁移改造,项目已经能够正常启动开发,但是在 npm run build 之后执行 npm run preview 会发现报错:

Uncaught ReferenceError: React is not defined

这是因为使用到了 mobx-reactinject 导致,关于报错信息,本人在网上搜索找到类似问题的链接:

具体配置可以参考 @vitejs/plugin-react 插件的相关描述,因此接下来需要继续调整 vite.config.js 文件中的配置:


import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const { resolve } = require('path') //必须要引入resolve 

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  resolve: {
    alias: {
      '@components': resolve(__dirname, 'src', 'components'),
      '@utils': resolve(__dirname, 'src', 'utils'),
      '@config': resolve(__dirname, 'src', 'config'),
    },
  },
  plugins: [react({ jsxRuntime: 'classic' })]
})

6. 变量使用

如果使用 process ,则需要更新 process.envimport.meta.env ,比如将 NODE_ENV === 'production' 更新为 import.meta.env.PRODREACT_APP_XXX 的环境变量,则切换为 VITE_XXX,假如有一个 .env 文件:

PORT=8001
PUBLIC_URL=/awesome-project/
REACT_APP_NAME=My App

第一步,修改 vite.config.js 里面的配置支持下面两个变量:

-  PORT=8001
-  PUBLIC_URL=/awesome-project/
export default defineConfig({
  base: "/awesome-project/",
  server: {
    port: 8001,
  }, ...
]);

接着修改支持 REACT_APP_ 开头的自定义变量:

# 在 .env 文件自定义 env 变量
-  REACT_APP_NAME=My App
+  VITE_NAME=My App

# 在 React 组件访问自定义 env 变量
-  process.env.REACT_APP_NAME
+  import.meta.env.VITE_NAME

还可以采用 dotenv 来加载 env 变量,并通过Vite 的 define 传递给应用:

import dotenv from "dotenv";
dotenv.config();

export default defineConfig({
  define: {
    "process.env.VITE_NAME": `"${process.env.VITE_NAME}"`
  },
// ...

这种方式不支持 mode-specific .env 比如 .env.development,因此需要时要自行设置。

经常使用到 process.env.NODE_ENV 变量,比如用来区分 developmentproduction 的代码编译,这里还有一种访问变量的方式:

export default ({ mode }) => {
  return defineConfig({
    define: {
      "process.env.NODE_ENV": `"${mode}"`,
    },
  });
};

7. 参考链接

darekkay.com - Migrating a Create React App project to Vite (推荐)

福禄网络研发团队 - antd+react项目迁移vite的解决方案

掘金 - Vite2 实战: React + TS + Mobx 旧项目迁移

darraghoriordan.com - Migrating a Create React App (CRA) application to Vite

medium.com - Migrate from create-react-app (CRA) to Vite (ViteJS) with TypeScript

segmentfault - 从create-react-app迁移到vite

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容