当前位置:网站首页>怎么手写vite插件

怎么手写vite插件

2022-06-23 12:53:00 亿速云

怎么手写vite插件

这篇文章主要讲解了“怎么手写vite插件”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么手写vite插件”吧!

    1. 什么是 vite 插件

    vite 其实就是一个由原生 ES Module 驱动的新型 Web 开发前端构建工具。

    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

    // 进入 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 {    // 插件名称    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 例子来运行这个代码了;

    4. 创建 examples 目录

    我这里创建了三套项目 demo,大家直接 copy 就行了,这里就不详细介绍了

    vite-react

    vite-vue2

    vite-vue3

    如果你的插件需要多跑一些 demo,自行创建项目即可;

    那么下面我们就需要配置 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

    注意:

    如果你的插件只会在 build 时运行,那就设置

    "example:vue3": "cd examples/vite-vue3 && pnpm run build" ;

    反之就运行

    pnpm run dev

    输出:

    怎么手写vite插件

    到这里你就可以 边开发边运行 了,尤雨溪看了都说爽歪歪 ~

    8. 发布

    安装 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 插件 感兴趣可以继续往下面看;

    vite 的插件钩子 hooks 们

    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 之前立即触发这个 hook;

    • writeBundle(options, bundle):在调用 bundle.write后,所有的chunk都写入文件后,最后会调用一次 writeBundle;

    • closeBundle():在服务器关闭时被调用

    4. 插件钩子函数 hooks 的执行顺序(如下图)

    怎么手写vite插件

    5. 插件的执行顺序

    • 别名处理Alias

    • 用户插件设置enforce: 'pre'

    • vite 核心插件

    • 用户插件未设置enforce

    • vite 构建插件

    • 用户插件设置enforce: 'post'

    • vite 构建后置插件(minify, manifest, reporting)

    手撸一个 vite 插件

    下面以 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 =&gt; {    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) =&gt; {    !isExists &amp;&amp; fs.mkdirSync(dirPath);    fs.writeFileSync(filePath, JSON.stringify(data));};

    感谢各位的阅读,以上就是“怎么手写vite插件”的内容了,经过本文的学习后,相信大家对怎么手写vite插件这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

    原网站

    版权声明
    本文为[亿速云]所创,转载请带上原文链接,感谢
    https://www.yisu.com/zixun/721437.html

    随机推荐