React 從 CRA 到 Vite 遷移筆記

如果你還沒聽說過 Vite.js 疚沐,那你應(yīng)該去試一試待锈。

Vite 提供了一個(gè)開發(fā)服務(wù)器舶沛,它基于 原生 ES 模塊 提供了 豐富的內(nèi)建功能罩抗,如速度快到驚人的 模塊熱更新(HMR)求摇。

在開始改造之前窍荧,先來看一下本項(xiàng)目用了哪些東西:

  • Create React App (CRA) 創(chuàng)建項(xiàng)目
  • 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 創(chuàng)建的項(xiàng)目并進(jìn)入命令行蕊退,執(zhí)行命令郊楣,安裝 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 標(biāo)簽內(nèi)追加代碼

   <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

如果項(xiàng)目使用了 sass输硝,則需要執(zhí)行命令進(jìn)行安裝:

# npm
$ npm install -D sass

# yarn
$ yarn add -D sass

如果 scss 文件里面引入了一些 node_modules 的 css 是使用 ~ 符號的今瀑,可以做出調(diào)整:

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

調(diào)整為

@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 項(xiàng)目使用了 mobx 的裝飾器 @,本來期望通過配置 babel 來解決但沒有生效郎逃。

關(guān)于在 CRA 中啟用裝飾器主要是用到兩個(gè)插件: @babel/plugin-proposal-decorators@babel/plugin-proposal-class-properties哥童,這里列出參考的鏈接:

經(jīng)過一系列嘗試,最終還是決定移除裝飾器褒翰,需將 Mobx 里面用到的裝飾器改成函數(shù)調(diào)用的形式贮懈,mobx用法改寫過程可以參考鏈接,最新版的 mobx(本機(jī)版本:"mobx": "^6.3.12")可以參考官方示例 - React integration · MobX优训,下面列出本人的調(diào)整方式朵你。

Store 的創(chuàng)建方式調(diào)整

項(xiàng)目的 Mobx 版本如下:

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

現(xiàn)在需要將使用到的裝飾器移除,如下:

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

關(guān)于報(bào)錯(cuò)

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

解決方法:如果一個(gè) action 方法使用到了 async 的話揣非,則需要寫一個(gè) runInAction 在內(nèi)部來修改 observable 屬性抡医,或者調(diào)用另外一個(gè) action 方法修改,或者使用其他方式來實(shí)現(xiàn) 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
        })
        // 或者使用獨(dú)立方法
        this.setUser(authUser)
    }

    // 這個(gè)方法將被自動封裝成`action`,因?yàn)槭褂昧?`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.

但由于本人項(xiàng)目從一開始的裝飾器更改過來搁嗓,繼續(xù)采取 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 時(shí)
export default withRouter(inject('userStore')(observer(RouterPage)))

Provider 使用方式

// 定義多個(gè) 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
};
// 默認(rèn)導(dǎo)出接口
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 初學(xué)者入門指南

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

5. vite.config.js 配置

在項(xiàng)目根目錄新建一個(gè) vite.config.js 文件,內(nèi)容如下:

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 更改

經(jīng)過上面的遷移改造腺逛,項(xiàng)目已經(jīng)能夠正常啟動開發(fā)荷愕,但是在 npm run build 之后執(zhí)行 npm run preview 會發(fā)現(xiàn)報(bào)錯(cuò):

Uncaught ReferenceError: React is not defined

這是因?yàn)槭褂玫搅?mobx-reactinject 導(dǎo)致,關(guān)于報(bào)錯(cuò)信息,本人在網(wǎng)上搜索找到類似問題的鏈接:

具體配置可以參考 @vitejs/plugin-react 插件的相關(guān)描述安疗,因此接下來需要繼續(xù)調(diào)整 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.PROD荐类, REACT_APP_XXX 的環(huán)境變量怖现,則切換為 VITE_XXX,假如有一個(gè) .env 文件:

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

第一步玉罐,修改 vite.config.js 里面的配置支持下面兩個(gè)變量:

-  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 傳遞給應(yīng)用:

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

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

這種方式不支持 mode-specific .env 比如 .env.development,因此需要時(shí)要自行設(shè)置吊输。

經(jīng)常使用到 process.env.NODE_ENV 變量饶号,比如用來區(qū)分 developmentproduction 的代碼編譯,這里還有一種訪問變量的方式:

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

7. 參考鏈接

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

福祿網(wǎng)絡(luò)研發(fā)團(tuán)隊(duì) - antd+react項(xiàng)目遷移vite的解決方案

掘金 - Vite2 實(shí)戰(zhàn): React + TS + Mobx 舊項(xiàng)目遷移

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末季蚂,一起剝皮案震驚了整個(gè)濱河市茫船,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扭屁,老刑警劉巖算谈,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篮迎,死亡現(xiàn)場離奇詭異乍惊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)控乾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門幔欧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丽声,你說我怎么就攤上這事礁蔗。” “怎么了雁社?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵浴井,是天一觀的道長。 經(jīng)常有香客問我霉撵,道長磺浙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任徒坡,我火速辦了婚禮撕氧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喇完。我一直安慰自己伦泥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著不脯,像睡著了一般府怯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上防楷,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天牺丙,我揣著相機(jī)與錄音,去河邊找鬼复局。 笑死冲簿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肖揣。 我是一名探鬼主播民假,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼龙优!你這毒婦竟也來了羊异?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤彤断,失蹤者是張志新(化名)和其女友劉穎野舶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宰衙,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡平道,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了供炼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片一屋。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖袋哼,靈堂內(nèi)的尸體忽然破棺而出冀墨,到底是詐尸還是另有隱情,我是刑警寧澤涛贯,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布诽嘉,位于F島的核電站,受9級特大地震影響弟翘,放射性物質(zhì)發(fā)生泄漏虫腋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一稀余、第九天 我趴在偏房一處隱蔽的房頂上張望悦冀。 院中可真熱鬧,春花似錦滚躯、人聲如沸雏门。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茁影。三九已至宙帝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間募闲,已是汗流浹背步脓。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浩螺,地道東北人靴患。 一個(gè)月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像要出,于是被迫代替她去往敵國和親鸳君。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容