Morty's Blog | Web developer

Vue CLI 無痛轉換 Vite 紀錄

Vue CLI 無痛轉換 Vite 紀錄

由於前陣子我們團隊遇到一個 @vue/cli-plugin-babel 的 cache 命中問題,也因此檢視了一下我們目前專案使用 vue cli + Webpack 4 似乎已經有點老舊了,因此協助團隊測試及評估升級 Vite。

由於我們團隊有上千個專案,因此希望盡量維持與原本的架構一致,一方面是萬一我們想要抽換回 Webpack 5 不必更動過多,另一方面是我們在升級舊專案時可以不更改專案程式碼直接移植,減少不必要的問題。

經過整理思考後有以下主要問題需要解決:

  1. 環境變數
  2. val-loader
  3. Index.html 變數注入

環境變數

注入環境變數

我們團隊在 Webpack 時期已有自行封裝環境變數的 function ,且封裝的功能性非常多,但 Vite 使用 import.meta.env 來取得環境變數,因此首要解決問題是維持 process.env.XXX 在專案中的寫法。

Vite 有提供 define 讓我們自行注入環境變數,因此可以借助 define 來實現這個功能。

// public env
import envObject from "XXXX";
export default defineConfig(() => {
const define = {
"process.env": { ...envObject },
};
return {
define,
};
});

撰寫 rollup plugin

雖然此舉已經可以在專案中取得 process.env ,但卻沒有 TypeScript 的自動提示,因此我們透過撰寫 rollup plugin 來自動產生型別定義檔案。

// public env
import envObject from "XXXX";
import path from "path";
import { rm, existsSync } from "fs";
import { readFile, writeFile } from "fs/promises";
function autoGenerateProcessEnv() {
const filepath = path.resolve("./src/process.env.d.ts");
const getDeclaration = () => {
const typeCodeLists = extendEnv.env.map((envKey) => {
return `${envKey}: ${typeof process.env[envKey]};`;
});
if (typeCodeLists.length === 0) return "";
const code = `declare namespace NodeJS {
export interface ProcessEnv {
${typeCodeLists.join("\n ")}
}
}
`;
return code;
};
return {
name: "auto-generate-process-env",
configResolved: async () => {
const originalContent = existsSync(filepath)
? await readFile(filepath, "utf-8")
: "";
const code = getDeclaration();
if (!code) return;
if (code === originalContent) return;
await writeFile(filepath, code, "utf-8");
},
};
}
export default defineConfig(() => {
return {
plugins: [autoGenerateProcessEnv()],
};
});

如此一來在開發時也能獲得自動提示,增加開發體驗及品質。

val-loader

許多開發者常常使用 Webpack Modules + val-loader 來打包一些資源,在 Vite 與 Rollup 中可以使用 Virtual Modules 來達成。

以下 plugin 範例有簡化過,可參考概念,值得注意的是路徑方面 Virtual Modules 是以 vite.config.ts 的所在目錄(project root)為主,而不是你的 plugin 檔案所在目錄。

const fetchData = () => {
// fetch data and return
return data;
};
export default function myPlugin() {
const virtualModuleId = "virtual:fetch-data";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
return {
name: "fetch-data", // required, will show up in warnings and errors
resolveId(id: string) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
async load(id: string) {
if (id !== resolvedVirtualModuleId) return null;
// development return local data
if (process.env.NODE_ENV !== "production") {
return `
import localData from './src/...';
export const data = localData;
`;
}
// production
const data = await fetchData();
return `
export const data = ${JSON.stringify(data)};
`;
},
};
}

接著將 plugin 檔案引入 vite.config.ts 並於 plugins 呼叫即可

import fetchPlugin from './src/plugin/fetch';
export default defineConfig(() => {
return {
plugins: [
fetchPlugin()
],
}
}

並於 src/vite-env.d.ts 進行型別的宣告

src/vite-env.d.ts
declare module "virtual:fetch-data" {
const data: {
// some type
};
export const messages;
}

最後就可以在需要的地方引入了

import { data } from "virtual:fetch-data";

Index.html 變數注入

為了避免更改 index.html,我們將注入的規則寫成跟原先 webpack 一樣

/**
* Replace env variables in index.html
* @see https://github.com/vitejs/vite/issues/3105#issuecomment-939703781
* @see https://vitejs.dev/guide/api-plugin.html#transformindexhtml
*/
function htmlPlugin() {
return {
name: "html-transform",
transformIndexHtml: {
enforce: "pre" as const,
transform: (html: string): string => {
return html.replace(/<%= (.*?) %>/g, (match, envKey) => {
const htmlEnv = {
// some env
};
return htmlEnv[envKey] ?? "";
});
},
},
};
}
export default defineConfig(() => {
return {
plugins: [htmlPlugin()],
};
});

結論

經過以上更動,我們幾乎可以在不更動專案程式碼時完成舊專案移植,尤其我們團隊因為 Webpack Modules 轉換上的一些困難所以一直沒有升級,也希望此文章可以幫助一些團隊更容易進行遷移升級 Vite 的測試及評估。

參考文件