likes
comments
collection
share

[陈同学i前端] 一起学Vite|实现第一个插件

作者站长头像
站长
· 阅读数 19

前言

大家好,我是陈同学,一枚野生前端开发者,感谢各位的点赞、收藏、评论

近年来,前端领域技术更新迭代节奏较快,前端工程师们为了更好的进行项目开发、测试、构建、部署,开发出了各种各样的构建工具

像常见的Webpack、Rollup、Esbuild、Vite,每一类工具都有它的特点,均致力于提高前端领域的工程化水平

而工具出现的目标是解决前端工程当中的一些影响通性问题

常见的痛点(需求点)有:模块化需求(ESM)、兼容高级语法、代码质量测试、静态资源处理、代码压缩、开发效率等

本节我们继续进行Vite知识的学习,具体安排如下:

  • 一起学Vite|初识下一代的前端工具链
  • 一起学Vite|原来这玩意叫依赖预构建
  • 一起学Vite|实现第一个Vite插件(本节)
  • 一起学Vite|插件流水线
  • 一起学Vite|HMR,你好👋
  • 一起学Vite|模块联邦——代码共享的终极解决方案
  • 一起学Vite|简单手写开发服务器
  • 一起学Vite|简单手写打包器

本文阅读成本与收益如下:

阅读耗时:5mins

全文字数:6k+

预期效益

  • Vite插件开发环境准备
  • Vite插件提供形式
  • 实现Vite插件并发布到NPM仓库

插件开发环境准备

  • 本地开发机nodenpm环境

  • 一个Vite驱动的项目

若没有Vite驱动的项目,则使用命令npm create vite@latest初始化一个项目

Vite插件提供形式

首先要说明的是我们开发的插件,最终是以一个对象的方式传入到Vite配置的plugin数组当中

[陈同学i前端] 一起学Vite|实现第一个插件

故无论我们是通过暴露一个函数(函数内部执行后返回一个对象)还是说直接提供一个对象字面量到Vite配置中都是可行的

简单的插件对象

如现在我们需要在构建应用后的HTML中的body标签里增加一个script标签,标签内部有一条console.log语句打印HelloWorld字符串到控制台当中,我们便可简单定义一个对象用于实现该功能

type IndexHtmlTransformResult =
  | string
  | HtmlTagDescriptor[] // 注入到现有 HTML 中的标签描述符对象数组
  | {
      html: string
      tags: HtmlTagDescriptor[]
    }
const printPlugin = {
    name: 'vite-plugin-simple-print',
    transformIndexHtml(html): IndexHtmlTransformResult {
        const htmlStr = `console.log('HelloWorld')`
        return [
            {
                tag: 'script',
                children: htmlStr,
                injectTo: 'body'
            },
        ]
    }
}
// vite.config.ts
export default defineConfig({
  plugins: [vue(), printPlugin],
})

这样我们就开发好了一个简单的插件并将其提供给了ViteVite在构建阶段便会在特定的时机执行我们提供好的钩子方法(transformIndexHtml

说明:

  • [name]插件名称(Vite插件要以vite-plugin-开头):vite-plugin-simple-print
  • [transformIndexHtml(html)]钩子函数:转换 index.html 的专用钩子,钩子接收当前的 HTML 字符串和转换上下文

该钩子函数支持不同的返回类型:

  1. 返回 HTML字符串:即使用经过转换的 HTML 字符串作为最终产物的HTML结果
  2. 返回元素(标签元素)为HtmlTagDescriptor类型的数组:即遍历数组每一个标签元素,通过元素里的属性描述对产物HTML作修改,每个标签元素可以指定标签应该被注入到哪里
  3. 返回以上两种类型组成的对象:能够同时实现HTML自定义以及标签元素的构建时动态变更

[陈同学i前端] 一起学Vite|实现第一个插件

最终呈现给用户的HTML中便会包含一个用于打印字符串的script标签

[陈同学i前端] 一起学Vite|实现第一个插件

[陈同学i前端] 一起学Vite|实现第一个插件

实现Vite插件并发布到NPM仓库

以上我们已经实现了一个以对象字面量形式提供的Vite插件

但若我们希望所开发的插件能够支持接收插件使用者提供的配置属性参数以实现按需启用构建行为的能力

我们便不能直接提供插件对象,而是提供一个函数,由函数接收用户提供的入参经过处理并形成闭包作用域后返回一个插件对象

插件对象生成函数

const printPlugin = function (printTxt = '') {
  const txt = 'Welcome ' + printTxt
  return {
    name: 'vite-plugin-simple-print',
    transformIndexHtml(html): IndexHtmlTransformResult {
        const htmlStr = `console.log('${txt}')`
        return [
            {
                tag: 'script',
                children: htmlStr,
                injectTo: 'body'
            },
        ]
    }
  }
}

// vite.config.ts
export default defineConfig({
  plugins: [vue(), printPlugin('charlex')], // 函数执行后返回一个插件对象
})

printPlugin改造成一个函数后,它便可以接收入参,并根据入参影响插件钩子函数内的行为

[陈同学i前端] 一起学Vite|实现第一个插件

这样一来将使得我们所开发的插件变得更加的灵活、可扩展

接下来我们为插件新增一个能力,支持通过入参选项的属性来控制是否去读取package.json中的nameversionauthor信息并打印到控制台中

// change_1: 读取package.json文件并拼接成js逻辑字符串
const handlePkgInfo = function () {
    // 读取项目目录下的package.json
    const pkg: any = readFileSync(process.cwd() + '/package.json', 'utf-8')
    const { name, version, author } = JSON.parse(pkg)
    // 组装即将插入到HTML的字符串
    const htmlStr = `
        const styles = [
            'padding: 5px',
            'font-size: 15px',
            'background: #0cc160',
            'color: white',
        ].join(';');
        console.log('Hello, ${inputName}')
        console.log('%cPackageInfo:${name}-${version}-${author}', styles)
    `
    return htmlStr
}
// change_2: 新增函数入参options用于控制部分插件能力
const printPlugin = function (printTxt = '', options = {
    printPkgInfo: false,
}) {
  const txt = 'Welcome ' + printTxt
  return {
    name: 'vite-plugin-simple-print',
    transformIndexHtml(html): IndexHtmlTransformResult {
        let htmlStr = `console.log('${txt}')`
        // change_3: 条件判断-由入参控制的能力
        if (options.printPkgInfo) {
            htmlStr += handlePkgInfo()
        }
        return [
            {
                tag: 'script',
                children: htmlStr,
                injectTo: 'body'
            },
        ]
    }
  }
}

变更后的插件支持在调用函数生成插件对象时,由开发者自行决定是否开启package.json信息的读取并打印到控制台的能力(默认关闭)

[陈同学i前端] 一起学Vite|实现第一个插件

在函数入参将options.printPkgInfo设置为true后,插件便启用额外能力

[陈同学i前端] 一起学Vite|实现第一个插件

[陈同学i前端] 一起学Vite|实现第一个插件

打包插件并发布到NPM仓库

现在我们已经开发好了一个插件,下一步就需要为该插件初始化一个项目用于将其发布成一个NPM的第三方依赖包

当其它项目需要进行使用时,就可以直接通过依赖安装后将插件对象生成函数导入到Vite配置文件中使用

[陈同学i前端] 一起学Vite|实现第一个插件

  • package.json内容:
{
  "name": "vite-plugin-simple-print",
  "version": "1.0.0",
  "description": "简单版控制台打印Vite插件",
  "main": "dist/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "tsc -w -p .",
    "build": "rimraf dist && tsc -p ."
  },
  "keywords": [
    "print"
  ],
  "author": "charles",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^18.11.14",
    "rimraf": "^3.0.2",
    "typescript": "^4.9.4",
    "vite": "^4.0.1"
  }
}
  • src/index.ts内容:
import { readFileSync } from "fs";
import type { Plugin, IndexHtmlTransformResult } from 'vite';

const handlePkgInfo = function () {
  // 读取项目目录下的package.json
  const pkg: string = readFileSync(process.cwd() + '/package.json', 'utf-8');
  const { name, version, author } = JSON.parse(pkg);
  // 组装即将插入到HTML的字符串
  const htmlStr = `
    const styles = [
        'padding: 5px',
        'font-size: 15px',
        'background: #0cc160',
        'color: white',
    ].join(';');
    console.log('%cPackageInfo:${name}-${version}-${author}', styles)
    `;
  return htmlStr;
};
export const printPlugin = function (
  printTxt = '',
  options = {
    printPkgInfo: false,
  }
): Plugin {
  const txt = 'Welcome ' + printTxt;
  return {
    name: 'vite-plugin-simple-print',
    transformIndexHtml(html: any): IndexHtmlTransformResult {
      let htmlStr = `console.log('${txt}')\n`;
      if (options.printPkgInfo) {
        htmlStr += handlePkgInfo();
      }
      return [
        {
          tag: 'script',
          children: htmlStr,
          injectTo: 'body',
        },
      ];
    },
  };
};
  • tsconfig.json可以通过npx tsc --init自动生成,并将compilerOptions.outDit设置为dist

完成项目搭建后便可以通过NPM发布命令进行NPM包的发布


npm login # 登陆

npm publish # 发布NPM包

其他钩子函数

由于Vite插件机制是基于Rollup来构建的,故我们可以参考Rollup插件官方文档,根据实际需求选用对应的插件钩子进行插件开发

之前写过一篇关于Rollup插件机制的文章,有兴趣的读者可以看看~

[一起学Rollup|构建工作流与插件机制]juejin.cn/post/715570…

讲到最后

本节文章我们学习了如何实现一个简单的Vite插件

一开始我们通过简单提供对象字面量的方式将插件提供给Vite,之后我们发现这种形式无法将部分插件能力的开关控制权交给业务开发者使用

故我们将原来以对象字面量形式提供Vite插件,改成了以插件对象生成函数的形式提供插件对象,后者便可以通过函数入参形成的闭包作用域控制插件内钩子的行为

最后,我们将开发好的Vite插件发布到NPM仓库,之后其他项目需要使用到插件能力时便可以直接安装依赖并导入到Vite配置文件中使用

参考补充

Vite官方文档

Rollup官方文档

Esbuild官方文档

Vue3文档