你還不會寫 Vite 外掛程式嗎

現在 vite 工具越來越流行,我們能不能做一些有意義的事情,比如寫一個 vite 外掛程式,你覺得怎麼樣?
正好趁著 vite 外掛程式生態還不成熟,做一個讓自己滿意、讓領導開心、讓社區受益的外掛程式,一起攜手共進。
如果你對 vite 感興趣,可以查看專欄:"Vite 從入門到精通"
通過本文你可以學到:

如何建立 vite 外掛程式模板
vite 外掛程式各鉤子的作用
vite 外掛程式的鉤子執行順序
如何編寫自己的外掛程式

瞭解 vite 外掛程式
1. 什麼是 vite 外掛程式
vite 實際上是一個基於原生 ES Module 的新型前端開發工具。
vite 外掛程式可以很好地擴充 vite 本身無法實現的功能,例如檔案圖片壓縮、CommonJS 支援、打包進度條等。
2. 為什麼要寫 vite 外掛程式
相信在座各位對 webpack 的相關配置和常用外掛程式都已經很熟悉了;
而 vite 作為新興的前端構建工具,還很年輕,有很大的擴充空間,何不趁現在一起攜手,做一些對你、對我、對大家都更有意義的事呢?
快速體驗
要寫外掛程式,首先要建立專案。以下 vite 外掛程式通用模板可以直接 clone 使用,以後寫外掛程式時直接複用;
外掛程式通用模板 GitHub:體驗入口
外掛程式 GitHub:體驗入口

推薦包管理器優先順序:pnpm > yarn > npm > cnpm

話不多說,直接開幹~
建立 vite 外掛程式通用模板
1. 初始化
1.1 建立檔案夾並初始化:按提示操作即可
mkdir vite-plugin-progress && cd vite-plugin-progress && pnpm init

1.2 安裝 TypeScript
pnpm i typescript @types/node -D

1.3 配置 tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "dist",
"lib": ["ESNext"],
"sourceMap": false,
"noEmitOnError": true,
"noImplicitAny": false
},
"include": [
"src/*",
"*.d.ts"
],
"exclude": [
"node_modules",
"examples",
"dist"
]
}

1.4 安裝 vite
// enter package.json
{
...
"devDependencies": {
"vite": "*"
}
...
}

2. 配置 eslint 和 prettier(可選)


安裝 eslint
pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev



配置 .eslintrc:配置連結


安裝 prettier(可選)
pnpm i prettier eslint-config-prettier eslint-plugin-prettier --save-dev



配置 .prettierrc:配置連結


3. 添加 src/index.ts 入口
import type { PluginOption } from 'vite';

export default function vitePluginTemplate(): PluginOption {
return {
// plugin name
name: 'vite-plugin-template',

// pre will be executed before post
enforce: 'pre', // post

// Indicate that they are only called in 'build' or 'serve' mode
apply: 'build', // apply can also be a function

config(config, { command }) {
console.log('here is the config hook');
},

configResolved(resolvedConfig) {
console.log('here is the configResolved hook');
},

configureServer(server) {
console.log('here is the configureServer hook');
},

transformIndexHtml(html) {
console.log('here is the transformIndexHtml hook');
},
}
}

下面會詳細講解 vite 外掛程式的各鉤子函數~
到這裡,基本模板已經搭建好了,但接下來要想一想,這個外掛程式該怎麼運行呢?
那我們就需要建立一些樣本來運行這段代碼;
4. 建立 examples 目錄
這裡建立了三組專案樣本,可以直接複製使用,不再逐一介紹。

vite-react
vite-vue2
vite-vue3

如果外掛程式需要在更多樣本中運行,自行建立專案即可;
然後需要配置 examples 下的專案,與根目錄的外掛程式做聯合調試(以 examples/vite-vue3 為例)。
5. 配置 examples/vite-vue3 專案


修改 examples/vite-vue3/package.json
{
...
"devDependencies": {
...
"vite": "link:../../node_modules/vite",
"vite-plugin-template": "link:../../"
}
}



以上配置的含義:


使 examples/vite-vue3 專案中的 vite 版本與根目錄 vite-plugin-template 中的版本保持一致;


同時將 examples/vite-vue3 專案中的 vite-plugin-template 指向當前根目錄開發的外掛程式;




引入外掛程式:examples/vite-vue3/vite.config.ts
import template from 'vite-plugin-template';

export default defineConfig({
...
plugins: [vue(), template()],
...
});



安裝依賴:cd examples/vite-vue3 && pnpm install
cd examples/vite-vue3 && pnpm install




注意:examples/vite-vue2 和 examples/vite-react 的配置與此一致

思考:
到這裡再想一想,examples/vite-vue3 的專案已經配置好了,但該怎麼運行呢?
直接進入 examples/vite-vue3 目錄運行 pnpm run build 或 pnpm run dev?
這樣顯然無法成功運行,因為根目錄的 src/index.ts 不能直接運行,需要先將 .ts 檔案轉譯為 .js 檔案;
那該怎麼處理呢?
這就需要藉助一個輕量且無需配置的構建工具 tsup。
6. 安裝 tsup 並配置運行命令
tsup 是一個基於 esbuild 的輕量級零配置構建工具;
同時它可以直接將 .ts、.tsx 轉譯為 esm、cjs、iife 等不同格式;


安裝 tsup
pnpm i tsup -D



在根目錄的 package.json 中配置
{
...
"scripts": {
"dev": "pnpm run build -- --watch --ignore-watch examples",
"build": "tsup src/index.ts --dts --format cjs,esm",
"example:react": "cd examples/vite-react && pnpm run build",
"example:vue2": "cd examples/vite-vue2 && pnpm run build",
"example:vue3": "cd examples/vite-vue3 && pnpm run build"
},
...
}



7. 開發環境運行


開發環境運行:即時監控檔案修改並重新打包(熱更新)
pnpm run dev



運行 examples 中的任意專案(以 vite-vue3 為例)
pnpm run example:vue3



注意:

如果外掛程式僅在構建時運行,設定 "example:vue3": "cd examples/vite-vue3 && pnpm run build";
否則運行 pnpm run dev


輸出:


到這裡就可以邊開發邊運行了,看到效果是不是很酷~
8. 發布

安裝 bumpp 進行版本管理和打標籤

pnpm i bumpp -D


配置 package.json

{
...
"scripts": {
...
"prepublishOnly": "pnpm run build",
"release": "npx bumpp --push --tag --commit && pnpm publish",
},
...
}


外掛程式開發完成後執行發布

# first step
pnpm run prepublishOnly

# second step
pnpm run release

到這裡,vite 外掛程式模板就寫好了,可以直接 clone vite-plugin-template 模板使用;
如果對 vite 的外掛程式鉤子和實現一個真實的 vite 外掛程式感興趣,可以繼續往下看;
vite 的外掛程式鉤子
1. vite 專屬的鉤子

enforce:取值可以是 pre 或 post,pre 在 post 之前執行;
apply:取值可以是 build 或 serve 或一個函數,指定僅在 build 或 serve 模式下調用;
config(config, env):可以在 vite 解析配置之前修改 vite 相關配置。該鉤子接收原始使用者配置 config 和描述配置環境的變數 env;
configResolved(resolvedConfig):在 vite 配置解析完成後調用。用於讀取並儲存最終解析的配置。當外掛程式需要根據啟動並執行命令做不同處理時非常有用;
configureServer(server):主要用於配置程式開發伺服器,為 dev-server 添加自訂中介軟體(connect 應用);
transformIndexHtml(html):專門用於轉換 index.html 的鉤子。接收當前 HTML 字串和轉換上下文;
handleHotUpdate(ctx):執行自訂 HMR 更新,可通過 ws 向用戶端發送自訂事件;

2. vite 和 rollup 通用鉤子的構建階段

options(options):伺服器啟動時調用,擷取並操作 Rollup 選項,嚴格來說在構建階段之前執行;
buildStart(options):每次構建開始時調用;
resolveId(source, importer, options):對每個傳入的模組請求調用,建立自訂確認函數,可用於定位第三方依賴;
load(id):對每個傳入的模組請求調用,可自訂載入器,用於返回自訂內容;
transform(code, id):對每個傳入的模組請求調用,主要用於轉換單個模組;
buildEnd():構建階段結束時調用,此處的構建結束僅指所有模組轉譯完成;

3. vite 和 rollup 通用鉤子的輸出階段

outputOptions(options):接收輸出參數;
renderStart(outputOptions, inputOptions):每次調用 bundle.generate 和 bundle.write 時觸發;
augmentChunkHash(chunkInfo):用於為 chunk 添加 hash;
renderChunk(code, chunk, options):在單個 chunk 轉譯時觸發。rollup 在輸出每個 chunk 檔案時調用;
generateBundle(options, bundle, isWrite):在調用 bundle.write 之前立即觸發該鉤子;
writeBundle(options, bundle):調用 bundle.write 後,所有 chunk 寫入檔案後,最終調用 writeBundle;
closeBundle():伺服器關閉時調用

4. 外掛程式鉤子函數的執行順序(如下圖所示)

5. 外掛程式的執行順序

Alias 處理別名
使用者外掛程式設定 enforce: 'pre'
vite 核心外掛程式
使用者外掛程式未設定 enforce
vite 構建外掛程式
使用者外掛程式設定 enforce: 'post'
vite 構建後置外掛程式(minify、manifest、reporting)

實戰一個 vite 外掛程式
下面以 vite 打包進度條外掛程式為例;

外掛程式地址:GitHub 覺得不錯的話歡迎 star ⭐️
該外掛程式已被 vite 官方收錄到官方文檔:連結地址
由於本文重點不在該外掛程式的詳細實現過程,因此只貼出原始碼供參考。詳細介紹將在下一篇文章中講解,敬請期待!

index.ts

import type { PluginOption } from 'vite';
import colors from 'picocolors';
import progress from 'progress';
import rd from 'rd';
import { isExists, getCacheData, setCacheData } from './cache';

type Omit = Pick>;
type Merge = Omit> & N;

type PluginOptions = Merge<
ProgressBar.ProgressBarOptions,
{
/**
* total number of ticks to complete
* @default 100
*/
total?: number;

/**
* The format of the progress bar
*/
format?: string;
}
>;

export default function viteProgressBar(options?: PluginOptions): PluginOption {

const { cacheTransformCount, cacheChunkCount } = getCacheData()

let bar: progress;
const stream = options?.stream || process.stderr;
let outDir: string;
let transformCount = 0
let chunkCount = 0
let transformed = 0
let fileCount = 0
let lastPercent = 0
let percent = 0

return {
name: 'vite-plugin-progress',

enforce: 'pre',

apply: 'build',

config(config, { command }) {
if (command === 'build') {
config.logLevel = 'silent';
outDir = config.build?.outDir || 'dist';

options = {
width: 40,

...options
};
options.total = options?.total || 100;

const transforming = isExists ? `${colors.magenta('Transforms:')} :transformCur/:transformTotal | ` : ''
const chunks = isExists ? `${colors.magenta('Chunks:')} :chunkCur/:chunkTotal | ` : ''
const barText = `${colors.cyan(`[:bar]`)}`

const barFormat =
options.format ||
`${colors.green('Bouilding')} ${barText} :percent | ${transforming}${chunks}Time: :elapseds`

delete options.format;
bar = new progress(barFormat, options as ProgressBar.ProgressBarOptions);



// not cache: Loop files in src directory
if (!isExists) {
const readDir = rd.readSync('src');
readDir.forEach((item) => reg.test(item) && fileCount++);
}
}
},

transform(code, id) {
transformCount++

// not cache
if(!isExists) {
const reg = /node_modules/gi;

if (!reg.test(id) && percent < 0.25) {
transformed++
percent = +(transformed / (fileCount * 2)).toFixed(2)
percent < 0.8 && (lastPercent = percent)
}

if (percent >= 0.25 && lastPercent <= 0.65) {
lastPercent = +(lastPercent + 0.001).toFixed(4)
}
}

// go cache
if (isExists) runCachedData()

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})

return {
code,
map: null
};
},

renderChunk() {
chunkCount++

if (lastPercent <= 0.95)
isExists ? runCachedData() : (lastPercent = +(lastPercent + 0.005).toFixed(4))

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: chunkCount,
})

return null
},

closeBundle() {
// close progress
bar.update(1)
bar.terminate()

// set cache data
setCacheData({
cacheTransformCount: transformCount,
cacheChunkCount: chunkCount,
})

// out successful message
stream.write(
`${colors.cyan(colors.bold(`Build successful. Please see ${outDir} directory`))}`
);
stream.write(' ');
stream.write(' ');
}
};

/**
* run cache data of progress
*/
function runCachedData() {

if (transformCount === 1) {
stream.write(' ');

bar.tick({
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})
}

transformed++
percent = lastPercent = +(transformed / (cacheTransformCount + cacheChunkCount)).toFixed(2)
}
}



cache.ts

import fs from 'fs';
import path from 'path';

const dirPath = path.join(process.cwd(), 'node_modules', '.progress');
const filePath = path.join(dirPath, 'index.json');

export interface ICacheData {
/**
* Transform all count
*/
cacheTransformCount: number;

/**
* chunk all count
*/
cacheChunkCount: number
}

/**
*It has been cached
* @return boolean
*/
export const isExists = fs.existsSync(filePath) || false;

/**
* Get cached data
* @returns ICacheData
*/
export const getCacheData = (): ICacheData => {
if (!isExists) return {
cacheTransformCount: 0,
cacheChunkCount: 0
};

return JSON.parse(fs.readFileSync(filePath, 'utf8'));
};

/**
* Set the data to be cached
* @returns
*/
export const setCacheData = (data: ICacheData) => {
!isExists && fs.mkdirSync(dirPath);
fs.writeFileSync(filePath, JSON.stringify(data));
};


最後
本系列將持續更新。關於整個"Vite 從入門到精通"專欄,主要從以下幾個方面進行講解,敬請期待!

各位看到這裡了,還不點個贊👍.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs- quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number ,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector -id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font -weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet, .hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background: #fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 分類:前端 標籤:前端 Vite 文章收錄於專欄:
Vite 從入門到精通

Vite(發音類似 [weɪt],意為輕、輕快)是一個基於原生 ES Module 的 Web 開發前端構建工具。
具有以下特性:
💡 極速的服務啟動;
⚡️ 輕量快速的熱更新;
🛠️ 豐富的功能;
📦 最佳化的構建;
🔩 通用的外掛程式;
🔑 完全類型化的 API。

相關文章

探索更多特惠

  1. 簡訊服務(SMS)和郵件服務

    50,000封郵件包低至1.99美元,120條簡訊僅需1.00美元起

phone 聯絡我們
Hi, I'm Alibaba Cloud AI Assistant!
I can help with questions and solutions.