如何编写一个 Vite 插件

9,874 阅读7分钟

相信对 Vue3 有所了解的同学都知道,尤大放弃了 webpack 采用了 Vite 来构建项目。Vite 也支持自定义插件,但是关于这部分的官方文档还没出,我在网上搜索了一圈,找到了一篇关于如何编写插件的文章。翻译了下来,希望能对大家有所帮助。好了,下面一起来看看吧! ps:我也尝试写了一个 vite 插件:把 md 转化为 string,代码地址

原文地址:Writing a vite plugin

尤大是这么描述 Vite 的: 「一个基于浏览器原生 ES imports 的开发服务器。 利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念, 服务器随起随用。 同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。 针对生产环境则可以把同一份代码用 rollup 打包。」

今天我们来看一下如何编写一个简单的 Vite 插件,代码地址

插件的功能

我们的插件将基于 Vue 组件目录自动生成 vue-router 路由, 这是我从 Nuxt 的路由功能中获得的灵感。

我们的目录结构:

src/
|-- pages/
    |-- about.vue
    |-- contact.vue

自动生成的路由:

[
  {
    name: 'about',
    path: '/about',
    component: '/src/pages/about.vue'
  },
  {
    name: 'contact',
    path: '/contact',
    component: '/src/pages/contact.vue'
  },
]

关于 Vite

vue3 为什么选择 vite?

不了解的 Vite 同学可以看看Vite 官方的解释,或者 听尤雨溪大佬在 Full Stack Radio 上的讨论。 从而了解什么是 Vite,以及为什么会出现 Vite。

和 Webpack 的相比,Vite 的构建时间大大缩短。 尝试一下 vite 吧,我相信你会理解为什么尤大选择了它!

Vite 插件的概念

在我写这篇文章的时候,还没有关于如何编写 Vite 插件的官方文档。

所以,目前只能通过阅读 Vite 源代码,来深入了解如何编写插件。

我们需要先来明确一些相关概念,方便后续的工作:

  • 开发服务器(dev server): 浏览器可以处理 js 文件的 ES 模块导入。

但不能直接处理其他文件类型的 import。 每次浏览器在代码中遇到 import 的时候, 会先通过 Vite 的 dev 服务器编译,然后直接提供给浏览器。

比如:*.vue 文件会先在开发服务器编译,再发送给浏览器。

这些 import 可以是 javascript,vue,css 文件。 也可以是其他类型的文件,不过需要在 vite 中指定变异工具。

这就是为什么 Vite 开发服务器如此有用 —— 它解决了浏览器的限制。

  • rollup 生产包(rollup production bundle): 对于静态内容,在生产版本中没有 vite 开发服务器可用。 所以,vite 使用rollup 打包生产环境的代码。

  • 自定义块(custom block): 有时候,你使用的其他库可以向 vue 文件中添加自定义块,例如<docs><story>。 可以使用 Vite 来制定如何处理自定义块。

编写我们的插件

我们将自动生成一个 vue-auto-routes.js 文件,从这个文件中导出路由数组。

这个文件是一个虚拟文件, 它是开发时(用于开发服务器)和构建时(用于 rollup)动态生成的。

最后,我们将路由数组导入代码中,就可以使用了。

自动生成 vue-router 路由

要生成 vue-auto-routes.js 文件,我们需要分析 src/pages 目录, 并把它转换为一些 import 语句和路由数组。

使用 node 内置的 fs 模块:

function parsePagesDirectory() {
  const files = fs
    .readdirSync('./src/pages')
    .map((f) => ({ name: f.split('.')[0], importPath: `/src/pages/${f}` }))

  const imports = files.map((f) => `import ${f.name} from '${f.importPath}'`)

  const routes = files.map(
    (f) => `{
        name: '${f.name}',
        path: '/${f.name}',
        component: ${f.name},
      }
      `,
  )

  return { imports, routes }
}

这个函数返回了两个数组:

  • imports:import 声明数组,eg. import about from 'src/pages/about.vue'
  • routes: 路由数组,eg. "{ name: 'about', path: '/about', component: about }"

请记住,现在这些是字符串,

创建一个空插件

Vite 插件只是一个对象,我们可以先返回一个空对象。

创建一个用作为插件的 js 文件 plugin.js

module.exports = function() {
 return {}
}

然后,在 vite.config.js 这样使用:

const viteAutoRoute = require('./plugin.js')

export default {
  plugins: [viteAutoRoute()],
}

细心的小伙伴可能会发现,我们导出了一个函数而不是对象。 这是为了方便以后在插件中添加自定义选项。

比如:想实现指定一个自定义路由功能,可以这样 viteAutoRoute({ pagesDir: './src/docs' })

开发环境

现在,来使用我们之前创建的那些路由。

使用 vite 插件的选项configureServer

module.exports = function () {
  const { imports, routes } = parsePagesDirectory()

  const moduleContent = `
    ${imports.join('\n')}
    export const routes = [${routes.join(', \n')}]
  `

  const configureServer = [
    async ({ app }) => { // koa 的代码
      app.use(async (ctx, next) => {
        if (ctx.path.startsWith('/@modules/vue-auto-routes')) {
          ctx.type = 'js'
          ctx.body = moduleContent
        } else {
          await next()
        }
      })
    },
  ]
  
  return { configureServer }
}

首先,创建一个字符串 moduleContent,其中包含路由 js 文件的所需内容。

然后,我们创建 configureServer 一个数组,用于 vite dev 服务器中的中间件。

每当 vite 导入 javascript 或者 vue 文件的时候, 它都会向该文件发送请求到其 dev 服务器, 在必要时进行一些转换,然后以浏览器可以处理的形式发送回去。

当它遇到类似 import { routes } from 'vue-auto-routes', 它会要求 @/modules/vue-auto-routes

因此,我们要做的是拦截该请求并返回我们生成 moduleContent 的主体,并将其 type 声明为 js。

最后,我们将此 configureServer 数组添加到返回的对象中,以供 Vite 使用。 Vite 看到这一点后,将我们的列表(共1个)中间件与自己的中间件合并。

现在,我们可以就在自己的路由器中使用这些动态生成的路由啦:

import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { routes } from 'vue-auto-routes'
import App from './App.vue'

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})
createApp(App).use(router).mount('#app')

现在,我们可以运行 yarn dev 来查看路由是怎么工作的, http://localhost:3000/#/about 🎉

请注意,我们使用的 vue-router-next 是即将推出的 Vue 3 路由器。

生产环境

不过,当我们运行 yarn build 它时,它不会使用我们刚刚完成的 configureServer, 因为在生产环境中它使用 rollup 编译而不是 Vite 的 dev 服务器。

因此,我们需要添加一些其他配置让它其在生产环境中也能正常工作:

const virtual = require('@rollup/plugin-virtual')

module.exports = function () {
  // 这块是之前定义的 configureServer 代码

  const rollupInputOptions = {
    plugins: [virtual({ 'vue-auto-routes': moduleContent })],
  }

  return { configureServer, rollupInputOptions }
}

在这里,我们使用了 vite 的插件选项 rollupInputOptions。 这使我们可以定义 rollup 的插件功能。

我们使用 @rollup/plugin-virtual 带有模块名称,并让返回需要的 js 内容。

本质上,它和开发服务器做的是相同的事情。

现在,我们的自动路由在本地开发中和生产环境中都起作用啦。

添加自定义块

各个 Vue 页面可能希望给路由提供其他选项。

比如说,我们可能想要向我们的路线添加其他选项,或者使用自定义路线名称。

为了实现这个功能,我们将重新实现 vue-cli-plugin-auto-routingroute 块, 以便在 vue 组件中执行以下操作:

<route>
{
  "meta": {
    "requiresLogin": true,
  }
}
</route>

为此,我们将使用该 vueCustomBlockTransforms 选项。 这样,你可以告诉 vite 在遇到 vue 文件的时候如何处理自定义块。

这是我们要添加的最后一个功能,所以让我们把它作为整个插件的一部分来看一下:

const fs = require('fs')
const virtual = require('@rollup/plugin-virtual')

function parsePagesDirectory() {
  const files = fs
    .readdirSync('./src/pages')
    .map((f) => ({ name: f.split('.')[0], importPath: `/src/pages/${f}` }))

  const imports = files.map((f) => `import ${f.name} from '${f.importPath}'`)

  const routes = files.map(
    (f) => `{
        name: '${f.name}',
        path: '/${f.name}',
        component: ${f.name},
        ...(${f.name}.__routeOptions || {}),
      }
      `,
  )

  return { imports, routes }
}

module.exports = function () {
  const { imports, routes } = parsePagesDirectory()

  const moduleContent = `
    ${imports.join('\n')}
    export const routes = [${routes.join(', \n')}]
  `

  const configureServer = [
    async ({ app }) => {
      app.use(async (ctx, next) => {
        if (ctx.path.startsWith('/@modules/vue-auto-routes')) {
          ctx.type = 'js'
          ctx.body = moduleContent
        } else {
          await next()
        }
      })
    },
  ]

  const rollupInputOptions = {
    plugins: [virtual({ 'vue-auto-routes': moduleContent })],
  }

  const vueCustomBlockTransforms = {
    route: ({ code }) => {
      return `
        export default function (Component) {
          Component.__routeOptions = ${code}
        }
      `
    },
  }

  return { configureServer, rollupInputOptions, vueCustomBlockTransforms }
}

我们添加了一个 vueCustomBlockTransforms 对象, 该对象将键 route(我们的块名)映射到返回虚拟 js 文件的函数中。

vueCustomBlockTransforms 中添加额外字段 __routeOptions, 该字段映射到我们在任何自定义 <route> 块中声明的代码。

然后,我们在路由生成代码(...(${f.name}.__routeOptions || {}))中使用此代码。

这就是为什么我在虚拟文件 vue-auto-routes 中导入组件, 现在可以直接访问 __routeOptions 已添加的字段啦。

现在,我们可以在 vue 页面组件中使用$route.meta.requiresLogin 啦!

尾声

希望大家有了解到 Vite 的工作原理,还有如何编写一些基本的插件。

我们下一篇文章再见 💗💗💗

水平有限,文章中如果有错误恳请各位大佬指出,感谢~!

本文使用 mdnice 排版