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