您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么手寫vite插件”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么手寫vite插件”吧!
vite 其實就是一個由原生 ES Module 驅動的新型 Web 開發前端構建工具。
vite 插件 就可以很好的擴展 vite 自身不能做到的事情,比如 文件圖片的壓縮、 對 commonjs 的支持、 打包進度條 等等。
相信在座的每位同學,到現在對 webpack 的相關配置以及常用插件都了如指掌了吧;
vite 作為一個新型的前端構建工具,它還很年輕,也有很多擴展性,那么為什么我們不趁現在與它一起攜手前進呢?做一些于你于我于大家更有意義的事呢?
要想寫一個插件,那必須從創建一個項目開始,下面的 vite 插件通用模板 大家以后寫插件可以直接clone使用;
插件通用模板 github:體驗入口
插件 github:體驗入口
建議包管理器使用優先級:pnpm > yarn > npm > cnpm
長話短說,直接開干 ~
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
// 進入 package.json { ... "devDependencies": { "vite": "*" } ... }
安裝 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 :配置連接
import type { PluginOption } from 'vite'; export default function vitePluginTemplate(): PluginOption { return { // 插件名稱 name: 'vite-plugin-template', // pre 會較于 post 先執行 enforce: 'pre', // post // 指明它們僅在 'build' 或 'serve' 模式時調用 apply: 'build', // apply 亦可以是一個函數 config(config, { command }) { console.log('這里是config鉤子'); }, configResolved(resolvedConfig) { console.log('這里是configResolved鉤子'); }, configureServer(server) { console.log('這里是configureServer鉤子'); }, transformIndexHtml(html) { console.log('這里是transformIndexHtml鉤子'); }, } }
其中的 vite 插件函數鉤子會在下面詳細詳解 ~
到這里,那么我們的基本模版就建好了,但是我們現在思考一下,我們應該怎么去運行這個插件呢?
那么我們就需要創建一些 examples 例子來運行這個代碼了;
我這里創建了三套項目 demo,大家直接 copy 就行了,這里就不詳細介紹了
vite-react
vite-vue2
vite-vue3
如果你的插件需要多跑一些 demo,自行創建項目即可;
那么下面我們就需要配置 examples 下的項目與當前根目錄的插件做一個聯調了(下面以 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 了。
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" }, ... }
開發環境運行:實時監聽文件修改后重新打包(熱更新)
pnpm run dev
運行 examples 中的任意一個項目(以 vite-vue3 為例)
pnpm run example:vue3
注意:
如果你的插件只會在 build 時運行,那就設置
"example:vue3": "cd examples/vite-vue3 && pnpm run build" ;
反之就運行
pnpm run dev
輸出:
到這里你就可以 邊開發邊運行 了,尤雨溪看了都說爽歪歪 ~
安裝 bumpp 添加版本控制與 tag
pnpm i bumpp -D
配置 package.json
{ ... "scripts": { ... "prepublishOnly": "pnpm run build", "release": "npx bumpp --push --tag --commit && pnpm publish", }, ... }
開發完插件后運行發布
# 第一步 pnpm run prepublishOnly # 第二步 pnpm run release
那么到這里,我們的 vite 插件模板 就已經寫好了,大家可以直接克隆 vite-plugin-template 模板 使用;
如果你對 vite 的插件鉤子 和 實現一個真正的 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往客戶端發送自定義的事件;
options(options) :在服務器啟動時被調用:獲取、操縱Rollup選項,嚴格意義上來講,它執行于屬于構建階段之前;
buildStart(options):在每次開始構建時調用;
resolveId(source, importer, options):在每個傳入模塊請求時被調用,創建自定義確認函數,可以用來定位第三方依賴;
load(id):在每個傳入模塊請求時被調用,可以自定義加載器,可用來返回自定義的內容;
transform(code, id):在每個傳入模塊請求時被調用,主要是用來轉換單個模塊;
buildEnd():在構建階段結束后被調用,此處構建結束只是代表所有模塊轉義完成;
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 之前立即觸發這個 hook;
writeBundle(options, bundle):在調用 bundle.write后,所有的chunk都寫入文件后,最后會調用一次 writeBundle;
closeBundle():在服務器關閉時被調用
別名處理Alias
用戶插件設置enforce: 'pre'
vite 核心插件
用戶插件未設置enforce
vite 構建插件
用戶插件設置enforce: 'post'
vite 構建后置插件(minify, manifest, reporting)
下面以 vite 打包進度條 插件為例
inde.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<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & 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, complete: '\u2588', incomplete: '\u2591', ...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'); const reg = /\.(vue|ts|js|jsx|tsx|css|scss||sass|styl|less)$/gi; 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('\n'); stream.write('\n'); } }; /** * run cache data of progress */ function runCachedData() { if (transformCount === 1) { stream.write('\n'); 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插件”的內容了,經過本文的學習后,相信大家對怎么手寫vite插件這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。