遷移 Vue v2.x 版本到 Vite

jsliang發表於2023-02-02

人生無常,大腸包小腸~

在接近年底的時候,有一個 Vue 專案,需要從中抽取 2 個模組出來。

然後想著新建專案,Vue CLI 也是學,Vite 也是學,於是哼次哼次用上了 Vite,結果開始了一路的 bug 捱打之旅……

01.png

警告:本文有 1.9w+ 字,35 張圖片,10 個以上報錯及其解決方式,1 個 Demo 和 1 個專案例項

一 前言

Hello 小夥伴們早上、中午、下午、晚上、深夜好,我是 jsliang

本次「遷移 Vue v2.x 專案到 Vite」將分為 2 個部分:

  1. 以一個簡單 Demo,進行 Vite 快速入手(同時也是補充完善例項專案未解決的問題)
  2. 對例項專案遷移,對比 Vue CLI 和 Vite,以及 Vite 構建中碰到的問題。

本文更傾向於隨手可查工具文,就好比之前寫過的 Webpack 4 文章一樣,它很快會石沉大海,但是我們將碰到的問題都反饋出來後,會讓後面的人少跑彎路,這是很棒的事情。

小夥伴們如果碰到文章同樣問題,想諮詢當時細節,可 WX: Liang123Gogo。

感謝你的點贊和關注支援~

二 簡單 Demo:透過 Vite 打包 lib 倉庫

下面開始保姆級教學,從 0 到 1 構建 Vite 專案,並處理打包問題。

前面及步驟略簡單,小夥伴可選擇跳讀。

本小節的程式碼倉庫:all-for-one/039-遷移 Vue v2.x 專案到 Vite/

2.1 步驟一:建立專案

  • 安裝 PNPM:npm i pnpm -g
  • 透過 PNPM 建立 Vite + Vue 專案:pnpm create vite jsliang-plugin --template vue

    • 建立 Vite 專案:pnpm create vite
    • 建立 Vite + Vue TypeScript 專案:pnpm create vite jsliang-vue-plugin --template vue-ts

2.2 步驟二:初始化並執行

  • 安裝 node_modules:pnpm i
  • 執行專案:pnpm run dev

02.png

如上圖所示,開啟 http://127.0.0.1:5173/ 即可。

效果如下圖:

03.png

如果需要外部訪問的話,需要在指令上加上 --host,即:

package.json
"scripts": {
-  "dev": "vite",
+  "dev": "vite --host",
  "build": "vite build",
  "preview": "vite preview"
},

2.3 步驟三:修改埠

一般 Vite + Vue 提供的埠是,像我這麼靚的靚仔,肯定要 8888

那就直接修改 vite.config.js 吧:

vite.config.js

為避免程式碼臃腫,第一次提的時候會寫全程式碼,後面會寫改動位置

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
+  server: {
+    port: 8888,
+  },
})

這時候再啟動 pnpm run dev,就能看到相應的埠有變更了。

2.4 步驟四:清場搞事

該做的事我們都做了,下面我們把 src 目錄下所有程式碼刪除,留下一個乾淨的 Vue 倉庫。

並依據下圖建立資料夾及其檔案:

04.png

我們執行 pnpm run dev 的時候,程式碼的呼叫思路如上圖所示。

  1. 先呼叫 index.html
  2. 再走 a/entry.js 或者 b/entry.js
  3. 接著走 a/a.vue 或者 b/b.vue
  4. 最後走 utils/c.js 這個公共模組

2.5 步驟五:補充程式碼

下面我們補充程式碼,使其最終展示如下:

05.png

首先,我們修改 index.html,使其提供了一個類 jsliang,其中有一個方法 addPlugin 提供注入 HTML 的能力。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
    <script>
      class jsliang {
        addPlugin({ pluginName, pluginObj }) {
          const div = document.createElement('div');
          div.classList.add('container');
          div.innerHTML = `
            <div>外掛 ${pluginName} 載入成功:</div>
          `;
          document.body.appendChild(div);
          document.body.appendChild(pluginObj());
        }
      }
      window.jsliang = new jsliang();
    </script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/components/a/entry.js"></script>
    <script type="module" src="/src/components/b/entry.js"></script>
  </body>
</html>

然後,我們這裡引用了 2 個入口,即 a/entry.jsb/entry.js(因為是測試的,所以直接在 pnpm run dev 模式上測試)

src/components/a/entry.js
// 這裡引用了 a.vue 的程式碼
import A from './a.vue';

(function() {
  console.log('jsliang 外掛載入成功');

  window.jsliang && window.jsliang.addPlugin && window.jsliang.addPlugin({
    pluginName: 'jsliang',
    pluginObj: A.methods.renderDOM,
  });
})();
src/components/a/a.vue
<template>
  <div id="container">
    Hello jsliang
  </div>
</template>

<script>
// 引用公共模組 C
import { c } from '../../utils/c';

export default {
  name: 'jsliang',
  mounted() {
    c();
  },
  methods: {
    renderDOM: () => {
      const div = document.createElement('div');
      div.innerHTML = 'Hello jsliang';
      return div;
    }
  }
}
</script>
src/utils/c.js
export const c = () => {
  console.log('c 模組載入');
};

最後,我們在 src/components/b/entry.jssrc/components/b/b.vue 補上同 a 部分的程式碼即可。

這裡就省略了,可以複製過去簡單改改

2.6 步驟六:庫模式和單入口單出口打包

當前,我們執行 pnpm run build,產生的打包檔案為:

06.png

而實際上,我們需要的打包結構(打包成 JS 庫):

- dist
  - A
    - A.entry.xxx.js
    - c.xxx.js
  - B
    - B.entry.xxx.js
    - c.xxx.js

所以,我們需要逐步靠攏這個目標,這裡我們要先行第一步。

先修改 vite.config.js

vite.config.js
export default defineConfig({
  // ... 程式碼省略

  // 打包模式
  build: {
    // 庫模式
    lib: {
      // 設定入口檔案
      entry: 'src/components/a/entry.js',
      // 打包後的包名稱
      name: 'A',
      // 打包後的檔名
      fileName: (format) => `A.entry.${format}.js`,
    }
  }
});

除此之外,順帶刪除專案下 public 目錄(包含裡面的 vite.svg,避免打包時參和進來。

此時,我們再次執行 pnpm run build,打包內容如下:

07.png

從而實現了單入口單出口打包。

2.7 步驟六:庫模式和多倉庫打包

其實上面步驟,我們發覺應該是同時走 2 個入口,然後打包出 2 個資料夾出來。

- dist
  - A
    - A.entry.xxx.js
    - c.xxx.js
  - B
    - B.entry.xxx.js
    - c.xxx.js

經過一番折騰,我們修改 Vite 配置如下:

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

// https://vitejs.dev/config/
export default defineConfig({
  // 載入外掛
  plugins: [vue()],
  // 埠設定
  server: {
    port: 8888,
  },
  // 打包模式
  build: {
    // 庫模式
    lib: {
      // 設定入口檔案
      entry: {
        'A': 'src/components/a/entry.js',
        'B': 'src/components/b/entry.js'
      },
      formats: ['es'],
      // 打包後的檔名
      fileName: (format, entryName) => `${entryName}/${entryName}.entry.${format}.js`,
    },
  }
});

此時打包內容如下:

08.png

無疑,這一個打包結果,距離我們差的有點多。

主要問題,出在打包後,公共程式碼並沒有分 2 個檔案裝到指定資料夾。

關於這個問題,厚顏無恥在 Vite 的 Discussions 上請教大佬:

經小夥伴 sapphi-red 的提醒,我也是意識到這種還是需要依靠外掛。

在自身對 Vite 和 Rollup 不想深入瞭解的情況下,我們應該把關注點放在解決問題上

於是開始修改程式碼。

步驟一:改造 package.json

"scripts": {
  "dev": "vite --host",
- "build": "vite build",
+  "build": "node build.js",
+  "A": "vite build --mode A",
+  "B": "vite build --mode B",
  "preview": "vite preview"
},

在這裡,我們看到修改了下 build 指令,並且新增了 AB 指令。

我們應當可以理解,此時我們只需要在 pnpm run build 指令操作時,在 build.js 上,執行 AB 指令即可。

步驟二:改造 vite.config.js,讓它能根據模式單獨打包

vite.config.js
// 打包模式
build: {
 outDir: `dist/${mode}`,
 lib: {
   entry: {
     [mode]: mode === 'A'
       ? 'src/components/a/entry.js'
       : 'src/components/b/entry.js'
   },
   formats: ['es'],
   fileName: (format, entryName) => `${entryName}.entry.${format}.js`,
 },
}

這裡程式碼很容易理解,就是打包的時候,區分模組進行單入口單出口的打包。

第三步:在專案資料夾上,新增 build.js,配合 package.json 中的 pnpm run build 指令:

build.js
import shell from 'shelljs';

shell.exec('pnpm run A');
shell.exec('pnpm run B');

這樣,我們就搞定了多倉庫分別單獨打包:

09.png

三 例項專案:遷移 Vue v2.x 專案到 Vite 新專案

在公司專案中,有個 Vue v2.x 版本的專案,希望從中遷移 2 個模組出來。

然後因為舊專案的打包流程過於複雜,牽扯了一些沒必要的元素。

所以新專案希望整一個乾淨的流程,單獨打 2 個 JS 包出來,專案結構大致如下:

- 新專案
 - src
  - A 包
  - B 包

確認過眼神,是熟悉的人,即【簡單專案:透過 Vite 打包 lib 倉庫】中探索的內容。

這裡我們們將講下折騰過程中,jsliang 的探索和思考,在過程中碰到的問題和解決方案,給後續小夥伴開發提供思路。

3.1 遷移 - Vue CLI 方案

透過 Vue CLI 構建的方式,可以檢視:https://cli.vuejs.org/zh/guid...

我們新舊的構建方案對比如下:

package.json
"scripts": {
  "build-new": "vue-cli-service build --target lib --inline-vue --name FullTextCommentPc src/main.js",
  "build": "vue-cli-service build",
},

自帶的是 npm run build,打包的內容如下:

10.png

這種打包方式不太符合我們的需求,因為這個外掛,應該是單入口的,最終目錄是這樣的:

- 資料夾 A
  - A.entry.js
  - info.json
- 資料夾 B
  - B.entry.js
  - info.json

所以參考連結,將打包改為:vue-cli-service build --target lib --inline-vue --name FullTextCommentPc src/main.js,這樣就可以打包出來:

11.png

3.2 遷移 - Vite 方案

這種打包出來的結構,已經很像了,仍需要調整檔名包含 entry 和新增 mainfest.json 檔案。

這種情況下,對於新手來說,感覺探索 Vue CLI + Webpack 方式,跟用 Vite 差不多了。

而獵奇的我,肯定要去耍耍 Vite。

3.3 報錯 - import Vue from 'vue'

遷移首個困難:

12.png

1: import Vue from 'vue'
          ^
2: import main from './main.vue'
error during build:
RollupError: "default" is not exported by "node_modules/.pnpm/vue@3.2.45/node_modules/vue/dist/vue.runtime.esm-bundler.js", imported by "src/components/FullTextCommentPc.js".

震驚!什麼,import Vue from 'vue' 還能報錯,你是不是跟我開玩笑?

然後我就去傻傻地搜:RollupError: "default" is not exported by ……

您猜怎麼著,翻了 1 個多小時,一點頭緒都沒有。

13.png

搞得我下班的時候還是檬茶茶的,帶著困惑下班了。

3.4 思考 - 究竟哪裡犯錯了

回來後我就在糾結了,前面看到了很多冗餘訊息,例如:

  • Vite 上 yyx 說 import Comp from 'comp.vue' 才是真實寫法,之前的 import Comp from 'comp' 其實並不支援……(Issue 上看到的,別糾結,糾結就是你贏)
  • Vue 並沒有預設匯出,我看了下 vue.d.ts 確實如此

14.png

  • Vite 上用 import a from 'a.js' 這種形式需要裝一個 commonjs 的包,並配置 vite.config.js(其實搜到這裡我已經迷糊了,連自己要什麼都不清楚了)

15.png

  • ……

回來後鎮定思痛,感覺還是要從 “版本” 2 字找起,因為我想起之前玩 Webpack v4.x 的時候,也是這麼被愚弄的。

所以我搜了下 Vite + Vue 2.x 的說法,還真找到了:

好傢伙,原來盡在文件……(其實這裡很困惑,上面報錯的時候,是不是可以指引下降低版本的資訊,而不是直接來個 "default" is not exported by?)

3.5 釋疑 - 總有一個版本適合你

操作方法很簡單:

  • 首先,刪包,把你的 node_modules 刪掉。
  • 然後,按照下面版本,修改 package.json

16.png

"dependencies": {
  "@vitejs/plugin-vue2": "^2.2.0",
  "vue": "^2.7.0"
},
"devDependencies": {
  "vite": "^4.0.0"
}
  • 接著,重新裝包 pnpm i

這裡需要注意一點:@vitejs/plugin-vue2 的包,只支援 Vue^2.7.0 版本:

Note: this plugin only works with Vue@^2.7.0.

詳細可以看上面提到的 GitHub 倉庫,裡面有強調。

  • 最後,修改下 vite.config.js

17.png

我們需要用 plugin-vue2 替換掉 plugin-vue 外掛。

再執行 pnpm run build,打包成功,搞定收工!

3.6 最佳化 - 我不需要 index.html

OK,打包報錯的問題解決了,下面開始操作,讓它剩下單入口:

- 資料夾 A
  - A.entry.js
  - info.json
- 資料夾 B
  - B.entry.js
  - info.json我們繼續操作,權當先打通 資料夾 A

這裡我們參考:

這時候修改 vue.config.js 為:

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

// https://vitejs.dev/config/
export default defineConfig({
  // 使用的外掛
  plugins: [vue()],
  build: {
    lib: {
      // 設定入口檔案
      entry: 'src/main.js',
      // 打包後的包名稱
      name: 'A',
      // 打包後的檔名
      fileName: (format) => `A.entry.${format}.js`,
    }
  },
})

打包 pnpm run build

18.png

似乎可行!新增一個 index.html 試試:

dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>測試</title>
  <script src="./A.entry.umd.js"></script>
</head>
<body>
  Hello jsliang
</body>
</html>

執行:

  • 前往目錄:cd dist
  • 執行專案:live-server
  • 開啟 127.0.0.1:8080,控制檯報錯:

19.png

20.png

OK,報錯 process is not defined,並且導向 process.env

這個問題,在 Vite 導向 的時候,#8090 有所表示:

21.png

檢視對應的 ISSUE:

22.png

所以,還需要再次修改 vite.config.js

23.png

它的最終程式碼:

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

// https://vitejs.dev/config/
export default defineConfig({
  // 使用的外掛
  plugins: [vue()],
  build: {
    lib: {
      // 設定入口檔案
      entry: 'src/main.js',
      // 打包後的包名稱
      name: 'A',
      // 打包後的檔名
      fileName: (format) => `A.entry.${format}.js`,
    }
  },
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  },
})

搞定收工,又解決了一個難題~

3.7 對比 - Vite 對比 Vue CLI

我們拿前面的打包資料檢視:

24.png

25.png

發現經過 Vite 打包,確實減輕了構建大小?

這裡無法給出明確判斷,本來想找個 Vue v2.x 的 GitHub 專案試試水,來驗證下的。

但是怕小夥伴們沒興趣,所以就淺嘗而止吧,在我單專案上是 OK 的。

如果小夥伴們在打包構建時碰到問題,可以私聊 jsliang 大家一起折騰下。

四 問題及其處理方式

4.1 PNPM 安裝報錯

在執行 pnpm i 的時候,報錯:@xxx/xx is not in the npm registry, or you have no permission to fetch it

這種情況下可能是因為你遷移的專案,有使用到私服 NPM,可以修改倉庫源達成目的。

.npmrc
sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"
phantomjs_cdnurl="http://cnpmjs.org/downloads"
electron_mirror="https://npm.taobao.org/mirrors/electron/"
profiler_binary_host_mirror="https://npm.taobao.org/mirrors/node-inspector/"
chromedriver_cdnurl="https://cdn.npm.taobao.org/dist/chromedriver"

4.2 TypeScript 引用報錯:找不到模組

報錯提示:找不到模組 xx 或其相應的型別宣告

26.png

這種情況之前構建 Node.js + TypeScript 專案中也解釋過,要補充 jsconfig.json

jsconfig.json
{  
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@utils/*": ["utils/*"],
    }
  },
  "exclude": ["node_modules", "dist"],
  "include": ["src"]
}

4.3 報錯:Component name "main" should always be multi-word

  • 報錯截圖:

27.png

  • 修復方式:

28.png

直接修改 vue.config.js,新增 lintOnSave: false

4.4 WARN:Issue with peer dependencies founc

29.png

簡單來說,你可以暫時忽略這玩意,對當前專案不會有影響。

這個提示是想告訴你:

  • NPM、PNPM 本身沒有很好的辦法去解決庫當中版本依賴不一致的問題,所以會有警告提示你,讓你去升級包或者外掛

當然,如果你不瞭解這些包的引用背景,那就不要管它,畢竟有時候不同版本解決不同問題,尤其是 Windows 使用者會深有體會。

4.5 TypeScript 型別報錯:型別 Window 不存在屬性

30.png

可以在全域性上,新增 vite-env.d.ts

// declare 的意思是告訴編輯器我知道 Windows 是啥型別
declare interface Window {
  APP: any,
}

當然,也可以一開始建立專案的時候,直接生成 TypeScript 的:

  • 建立 Vite + Vue TypeScript 專案:pnpm create vite airpage-vue-plugin --template vue-ts

4.6 TypeScript 型別宣告報錯:需要 Promise 建構函式

31.png

設定方式,在 jsconfig.json 中新增 lib 宣告:

  • ?當 target 為 ES5 的時候,TS 會認為你的 TS 原始碼只使用 ES5 的 API,否則會如上報錯。

這裡需要新增 es2015.promise 配置,同時需要新增其他配置,因為單單配置一個是不夠的,例如 ES5 預設帶 domscripthostes5 三個 lib,如果改得剩下一個,TS 編譯器還是會報錯,不認識 ES5 和 DOM 的 API

  • ? “--lib” 選項的引數必須為 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'webworker.iterable', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.sharedmemory', 'es2020.string', 'es2020.symbol.wellknown', 'es2020.intl', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint', 'esnext.string', 'esnext.promise', 'esnext.weakref'
jsconfig.json
{  
  "compilerOptions": {
    "lib": [
      "dom",
      "dom.iterable",
      "scripthost",
      "es5",
      "es2015.promise",
      "es2015.core",
      "es2016",
      "es2017",
    ]
  }
}

4.7 JSDoc 註解宣告

看到個註解宣告有點意思,宣告一個函式廢棄:

32.png

33.png

4.8 報錯:The following dependencies are imported but could not be resolved

34.png

步驟一:修改 vite.config.ts

35.png

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
import { resolve } from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  // 使用的外掛
  plugins: [vue()],
  // 別名
  resolve: {
    alias: [{ find: '@', replacement: resolve(__dirname, 'src') }]
  },
})

步驟二:修改 jsconfig.json

{  
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@dialog/*": ["src/dialog/*"],
    }
  }
}

4.9 報錯:Preprocessor dependency "less" not found. Did you install it?

安裝 less(切記,因為 less 沒有在程式碼中 import,所以將其放在 devDependencies 中)

  • npm install less -D

4.10 報錯:"sanitize" is not exported by ... , imported by ...

匯出:

function createDOMPurify() {
  DOMPurify.sanitize = function (dirty) {
  }
  return DOMPurify;
}

var purify = createDOMPurify();

export { purify as default };

引用:

import { sanitize } from 'dompurify';
sanitize('jsliang');

這種情況很坑,解決方案:

  1. 要麼你換包
  2. 要麼你提醒包作者修改匯出方式
  3. 要麼你直接修改包,並作為第三方包在專案中執行

五 參考文獻


不折騰的前端,和鹹魚有什麼區別!

覺得文章不錯的小夥伴歡迎點贊/點 Star。

如果小夥伴需要聯絡 jsliang

個人聯絡方式存放在 Github 首頁,歡迎一起折騰~

爭取打造自己成為一個充滿探索欲,喜歡折騰,樂於擴充套件自己知識面的終身學習斜槓程式設計師。

jsliang 的文件庫由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議 進行許可。<br/>基於 https://github.com/LiangJunrong/document-library 上的作品創作。<br/>本許可協議授權之外的使用許可權可以從 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 處獲得。

相關文章