人生無常,大腸包小腸~
在接近年底的時候,有一個 Vue 專案,需要從中抽取 2 個模組出來。
然後想著新建專案,Vue CLI 也是學,Vite 也是學,於是哼次哼次用上了 Vite,結果開始了一路的 bug 捱打之旅……
警告:本文有 1.9w+ 字,35 張圖片,10 個以上報錯及其解決方式,1 個 Demo 和 1 個專案例項
一 前言
Hello 小夥伴們早上、中午、下午、晚上、深夜好,我是 jsliang。
本次「遷移 Vue v2.x 專案到 Vite」將分為 2 個部分:
- 以一個簡單 Demo,進行 Vite 快速入手(同時也是補充完善例項專案未解決的問題)
- 對例項專案遷移,對比 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
- 建立 Vite 專案:
2.2 步驟二:初始化並執行
- 安裝 node_modules:
pnpm i
- 執行專案:
pnpm run dev
如上圖所示,開啟 http://127.0.0.1:5173/
即可。
效果如下圖:
如果需要外部訪問的話,需要在指令上加上 --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 倉庫。
並依據下圖建立資料夾及其檔案:
我們執行 pnpm run dev
的時候,程式碼的呼叫思路如上圖所示。
- 先呼叫
index.html
- 再走
a/entry.js
或者b/entry.js
- 接著走
a/a.vue
或者b/b.vue
- 最後走
utils/c.js
這個公共模組
2.5 步驟五:補充程式碼
下面我們補充程式碼,使其最終展示如下:
首先,我們修改 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.js
和 b/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.js
和 src/components/b/b.vue
補上同 a
部分的程式碼即可。
這裡就省略了,可以複製過去簡單改改
2.6 步驟六:庫模式和單入口單出口打包
當前,我們執行 pnpm run build
,產生的打包檔案為:
而實際上,我們需要的打包結構(打包成 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
,打包內容如下:
從而實現了單入口單出口打包。
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`,
},
}
});
此時打包內容如下:
無疑,這一個打包結果,距離我們差的有點多。
主要問題,出在打包後,公共程式碼並沒有分 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
指令,並且新增了 A
和 B
指令。
我們應當可以理解,此時我們只需要在 pnpm run build
指令操作時,在 build.js
上,執行 A
和 B
指令即可。
步驟二:改造 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');
這樣,我們就搞定了多倉庫分別單獨打包:
三 例項專案:遷移 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
,打包的內容如下:
這種打包方式不太符合我們的需求,因為這個外掛,應該是單入口的,最終目錄是這樣的:
- 資料夾 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
,這樣就可以打包出來:
3.2 遷移 - Vite 方案
這種打包出來的結構,已經很像了,仍需要調整檔名包含 entry
和新增 mainfest.json
檔案。
這種情況下,對於新手來說,感覺探索 Vue CLI + Webpack 方式,跟用 Vite 差不多了。
而獵奇的我,肯定要去耍耍 Vite。
3.3 報錯 - import Vue from 'vue'
遷移首個困難:
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 個多小時,一點頭緒都沒有。
搞得我下班的時候還是檬茶茶的,帶著困惑下班了。
3.4 思考 - 究竟哪裡犯錯了
回來後我就在糾結了,前面看到了很多冗餘訊息,例如:
- Vite 上 yyx 說
import Comp from 'comp.vue'
才是真實寫法,之前的import Comp from 'comp'
其實並不支援……(Issue 上看到的,別糾結,糾結就是你贏) - Vue 並沒有預設匯出,我看了下
vue.d.ts
確實如此
- Vite 上用
import a from 'a.js'
這種形式需要裝一個commonjs
的包,並配置vite.config.js
(其實搜到這裡我已經迷糊了,連自己要什麼都不清楚了)
- ……
回來後鎮定思痛,感覺還是要從 “版本” 2 字找起,因為我想起之前玩 Webpack v4.x 的時候,也是這麼被愚弄的。
所以我搜了下 Vite + Vue 2.x 的說法,還真找到了:
好傢伙,原來盡在文件……(其實這裡很困惑,上面報錯的時候,是不是可以指引下降低版本的資訊,而不是直接來個 "default" is not exported by
?)
3.5 釋疑 - 總有一個版本適合你
操作方法很簡單:
- 首先,刪包,把你的
node_modules
刪掉。 - 然後,按照下面版本,修改
package.json
:
"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
:
我們需要用 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
:
似乎可行!新增一個 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
,控制檯報錯:
OK,報錯 process is not defined
,並且導向 process.env
。
這個問題,在 Vite 導向 的時候,#8090 有所表示:
檢視對應的 ISSUE:
所以,還需要再次修改 vite.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`,
}
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
})
搞定收工,又解決了一個難題~
3.7 對比 - Vite 對比 Vue CLI
我們拿前面的打包資料檢視:
發現經過 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 或其相應的型別宣告
。
這種情況之前構建 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
- 報錯截圖:
- 修復方式:
直接修改 vue.config.js
,新增 lintOnSave: false
4.4 WARN:Issue with peer dependencies founc
簡單來說,你可以暫時忽略這玩意,對當前專案不會有影響。
這個提示是想告訴你:
- NPM、PNPM 本身沒有很好的辦法去解決庫當中版本依賴不一致的問題,所以會有警告提示你,讓你去升級包或者外掛
當然,如果你不瞭解這些包的引用背景,那就不要管它,畢竟有時候不同版本解決不同問題,尤其是 Windows 使用者會深有體會。
4.5 TypeScript 型別報錯:型別 Window 不存在屬性
可以在全域性上,新增 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 建構函式
設定方式,在 jsconfig.json
中新增 lib
宣告:
- ?當 target 為 ES5 的時候,TS 會認為你的 TS 原始碼只使用 ES5 的 API,否則會如上報錯。
這裡需要新增 es2015.promise
配置,同時需要新增其他配置,因為單單配置一個是不夠的,例如 ES5 預設帶 dom
、scripthost
和 es5
三個 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 註解宣告
看到個註解宣告有點意思,宣告一個函式廢棄:
4.8 報錯:The following dependencies are imported but could not be resolved
步驟一:修改 vite.config.ts
:
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');
這種情況很坑,解決方案:
- 要麼你換包
- 要麼你提醒包作者修改匯出方式
- 要麼你直接修改包,並作為第三方包在專案中執行
五 參考文獻
- Vite 官方中文文件 - Vue
- GitHub - vitejs - vite-plugin-vue2
- 掘金 - 孤獨的根號3 - vite打包lib庫
- Vite 導向
- Vite Issue - Multiple entry points/output in library mode? #1736
- Vite Discussions - vite lib multiple outputs
- 掘金 - 程式鋪子 - npm中,你不瞭解的.npmrc檔案
- SegmentFault - JS_Even_JS - tsconfig常用配置解析
- Vue CLI - 指南 - 構建目標
- Vite 官方中文文件 - 開始
- vite中解構匯出或有bug?
- bilibili - 飛葉_程式設計師 - vite專案不支援require語法怎麼辦?| 前端技術探索
- bilibili - 飛葉_程式設計師 - 你不需要三方plugin來支援require語法, vite對commonjs模組的處理
- GitHub - vitejs/awesome-vite
- 掘金 - 熬夜的浪子 - PNPM 安裝包時為什麼會出現 missing peer xxx
- CSDN - 紫軒閣 - typeScript 上寫window報any 型別“Window & typeof globalThis”上不存在屬性“App”
- 掘金 - 為振挽伊琳 - ts中使用Promise中不識別問題
- JSDoc
- bilibili - 【D1n910】專案遷移 Vue3.0 + Vite + typescript 踩31坑記錄
- 掘金 - anduinnwrynn - vite 嚐鮮
不折騰的前端,和鹹魚有什麼區別!
覺得文章不錯的小夥伴歡迎點贊/點 Star。
如果小夥伴需要聯絡 jsliang:
個人聯絡方式存放在 Github 首頁,歡迎一起折騰~
爭取打造自己成為一個充滿探索欲,喜歡折騰,樂於擴充套件自己知識面的終身學習斜槓程式設計師。
jsliang 的文件庫由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議 進行許可。<br/>基於 https://github.com/LiangJunrong/document-library 上的作品創作。<br/>本許可協議授權之外的使用許可權可以從 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 處獲得。