~/static/img/default-avatar-0.jpeg

yulu

前端开发工程师

博客

初探vite 插件

yulu 2022-06-15

什么是vite插件

Vite 是由原生 ES Module驱动的新型 Web 前端构建工具;

Vite插件 其实就是在vite的基础上扩展vite的能力,进而能实现我们在构建阶段需要处理的一些定制化的需求,比如解析用户自定义的文件输入,文件图片的压缩,在打包之前转译代码等等。

比如Galio项目中用到的vite插件: image.png

命名约定

由于vite插件扩展自Rollup接口,只是额外多了一些 vite特有选项 ,为了区分两者,所以制定了以下命名约定。

如果插件不使用 Vite 特有的钩子,可以实现为兼容的 Rollup 插件,则插件命名带有rollup-plugin-` 前缀

对于 Vite 专属的插件:

  • Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 vite-plugin 关键字。
  • 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。

如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:

  • vite-plugin-vue- 前缀作为 Vue 插件
  • vite-plugin-react- 前缀作为 React 插件
  • vite-plugin-svelte- 前缀作为 Svelte 插件

vite插件的配置

用户会将插件添加到项目的 devDependencies 中并使用数组形式的 plugins 选项配置它们。

import vitePlugin from 'vite-plugin-test'
import rollupPlugin from 'rollup-plugin-test'
export default {
  plugins: [vitePlugin(), rollupPlugin()]
}

plugins 也可以接受将多个插件作为单个元素的预设。这对于使用多个插件实现的复杂特性(如框架集成)很有用。该数组将在内部被扁平化(flatten)。

例如 vite-legacy插件中返回的就是一个插件数组: image.png

vite 的插件钩子

vite 独有的钩子

  1. enforce :值可以是prepostpre 会较于 post 先执行;
  2. apply :值可以是 buildserve 亦可以是一个函数,指明它们仅在 buildserve 模式时调用;
  3. config(config, env) :可以在 vite 被解析之前修改 vite 的相关配置。钩子接收原始用户配置 config 和一个描述配置环境的变量env(包含正在使用的 modecommand)。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置。
  4. configResolved(resolvedConfig) :在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它很有用。
  5. configureServer(server) :主要用来配置开发服务器,为 dev-server (connect 应用程序) 添加自定义的中间件;
  6. transformIndexHtml(html) :转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。 这个钩子可以是异步的,并且可以返回以下其中之一:
    • 经过转换的 HTML 字符串
    • 注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在 <head> 之前)
    • 一个包含 { html, tags } 的对象
  7. handleHotUpdate(ctx):执行自定义HMR更新,可以通过ws往客户端发送自定义的事件;

vite 与 rollup 的通用钩子(构建阶段)

  1. options(options) :在服务器启动时被调用:获取、操纵Rollup选项,严格意义上来讲,它执行于属于构建阶段之前;
  2. buildStart(options):在每次开始构建时调用;
  3. resolveId(source, importer, options):在每个传入模块请求时被调用,创建自定义确认函数,可以用来定位第三方依赖;
  4. load(id):在每个传入模块请求时被调用,可以自定义加载器,可用来返回自定义的内容;
  5. transform(code, id):在每个传入模块请求时被调用,主要是用来转换单个模块;
  6. buildEnd():在构建阶段结束后被调用,此处构建结束只是代表所有模块转义完成;

vite 与 rollup 的通用钩子(输出阶段)

  1. outputOptions(options):接受输出参数;
  2. renderStart(outputOptions, inputOptions):每次 bundle.generate 和 bundle.write 调用时都会被触发;
  3. augmentChunkHash(chunkInfo):用来给 chunk 增加 hash;
  4. renderChunk(code, chunk, options):转译单个的chunk时触发。rollup 输出每一个chunk文件的时候都会调用;
  5. generateBundle(options, bundle, isWrite):在调用 bundle.write 之前立即触发这个 hook;
  6. writeBundle(options, bundle):在调用 bundle.write后,所有的chunk都写入文件后,最后会调用一次 writeBundle;
  7. closeBundle():在服务器关闭时被调用

插件钩子的执行顺序

开发环境: image.png

构建阶段 image.png

image.png

PS: 在学习、调试或创作插件时,可以在项目中引入 vite-plugin-inspect。 它可以帮助你检查 Vite 插件的中间状态。安装后,你可以访问 localhost:3000/__inspect/ 来检查你项目的模块和栈信息。 image.png

插件的顺序

一个 Vite 插件可以额外指定一个 enforce 属性来调整它的应用顺序。enforce 的值可以是prepost。解析后的插件将按照以下顺序排列:

  • 别名处理Alias
  • 带有 enforce: 'pre' 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件
  • Vite 后置构建插件(最小化,manifest,报告)

image.png

编写属于自己的插件

推荐使用 @rollup/pluginutils 工具包,可以很方便的处理一些常规事情,比如添加文件扩展,创建过滤器等等。

统计组件使用情况的部分代码(待进一步完善) `

import { createFilter } from '@rollup/pluginutils'
import { walk } from 'estree-walker'

const fs = require('fs')
/**
 * 几种情况
 * <OkkiButton>按钮</OkkiButton>
 * import { Button } from '@okki-design/ui'
 * import { Button as AButton, Input as OkkiInput } from '@okki-design/ui'
 */
export default function myPlugin(options = {}) {
  const source_map = {}
  const source_map2 = {}
  const components = new Set()
  const cwd = process.cwd()

  var filter = createFilter(options.include, options.exclude)
  return {
    name: 'my-example',
    apply: 'build',
    transform(code, id) {
      if (!filter(id)) return
      const file_path = id.replace(cwd, '')

      const ast = this.parse(code)

      const data = {}
      let locals = []
      walk(ast, {
        enter(node, parent) {
          if (node.type === 'ImportDeclaration' && node.source.value === '@okki-design/ui') {
            for (const item of node.specifiers) {
              data[item.local.name] = {
                num: 0,
                refer: item.imported.name,
              }
              if (!components[item.local.name]) {
                components.add(item.local.name)
              }
            }
            locals = Object.keys(data)
            return
          }
          if (node.type === 'Identifier' && locals.includes(node.name) && parent.type !== 'ImportSpecifier') {
            data[node.name].num++
          }
          if (node.type === 'Literal' && typeof node.value === 'string' && node.value.startsWith('Okki')) {
            if (data[node.value] === undefined) {
              data[node.value] = {
                num: 0,
                refer: node.value.replace('Okki', ''),
              }
            }
            data[node.value].num++
          }
        },
      })

      try {
        Object.keys(data).forEach((key) => {
          if (source_map[file_path] === undefined) {
            source_map[file_path] = {}
          }
          source_map[file_path][data[key].refer] = data[key].num
        })
      } catch {}

      try {
        Object.keys(data).forEach((key) => {
          if (source_map2[data[key].refer] === undefined) {
            source_map2[data[key].refer] = {}
          }
          source_map2[data[key].refer][file_path] = data[key].num
        })
      } catch {}
    },
    buildEnd() {
      fs.writeFile('data.json', `${JSON.stringify(source_map)}\n`, {}, (error) => {
        console.log(error)
      })
      fs.writeFile('data2.json', `${JSON.stringify(source_map2)}\n`, {}, (error) => {
        console.log(error)
      })
    },
  }
}
作者相关知识精选
  • Promise.allSettled 的参数接受一个 Promise 的数组,返回一个新的 Promise。执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。

    image.png

    可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。

    适用场景:

    Promise.allSettled: 适用于需要执行多个不依赖于彼此成功完成的异步任务,或者当你总是想知道每个承诺的结果时;

    Promise.all: 适用于任务相互依赖/想在其中任何一个拒绝时立即拒绝。

    查看更多
  • git revert是用一次新的commit来回滚之前的commit;

    git reset是直接删除指定的commit。

    在回滚这一操作上看,效果差不多。但是在以后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此之后合并老的branch时,导致这部分改变不会再次出现,但是git reset是直接把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。

    查看更多
  • 浏览器提供了原生的 API,可以轻松实现浏览器滚动后不记住滚动位置每次刷新都回到顶部的能力!使用很简单,在页面的任意位置执行下面几行 JS 代码就可以了

    if (history.scrollRestoration) {
      history.scrollRestoration = 'manual';
    }
    
    查看更多