随着Vue 3最近进入发布候选阶段,是你尝试一下的绝佳时机。要马上开始,你需要使用vite,Vue的创建者提供的新的web开发构建工具。Vite提供了一个新的插件系统来扩展Vite的功能。今天我们来看看如何设置一个简单的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
/ 什么是vite?/
vite 是一个基于 Vue3 单文件组件的非打包开发服务器,Vite提供了比vue-cli和其他基于webpack的设置更快的构建时间。它做到了本地快速开发启动:
- 快速的冷启动,不需要等待打包操作;
- 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
- 真正的按需编译,不再等待整个应用编译完成,这是一个巨大的改变。
/ Vite插件概念 /
在撰写本文时,还没有真正的文档来创建vite插件。vite仓库自述文件中讨论了一些概念,但是你需要在vite源代码中进行一些挖掘,以确切地了解如何编写插件。让我们讨论一些关键概念。
dev server:当涉及到javascript时,现代浏览器可以处理(某种程度上)ES模块导入,但是如果遇到其他文件类型的导入则不能。每次浏览器在代码中找到导入时,它将先通过vite的开发服务器,然后直接提供给浏览器。这些导入可以是javascript,vue,css——如果你告诉vite如何处理它,则可以是任何东西。这就是为什么Vite开发服务器如此有用的原因——它为你解决了该浏览器的限制。
rollup production bundle:对于静态内容,你不需要在生产构建中使用vite dev服务器。因此,vite使用rollup将你的代码捆绑用于生产。
Vue自定义块转换:有时你使用的其他库会让你在vue文件中添加自定义块,比如 或 。Vite让你指定遇到这些块时如何处理。
编写我们的插件
我们采用的一般方法是自动生成一个 vue-auto-routes.js 文件,然后从中导出路由数组。我们将把该文件视为虚拟文件,该文件是在运行时(对于开发服务器)和构建时(对于汇总)动态生成的。然后,我们可以在我们的代码中导入这些路由,并在我们的应用程序中使用它们。
/ 自动生成vue-router路由 /
为了填充我们的 vue-auto-routes.js 文件,我们需要解析我们的 src/pages 目录,并把它变成一些导入语句和一个路由数组。我们将粗略地使用内置的 fs Node模块执行此操作:
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 about from 'src/pages/about.vue'”
- routes:我们的路由数组,例如“{ name: 'about', path: '/about', component: about }”
请记住,现在这些是字符串,因为我们正在使用它们创建一个javascript文件。请注意,对于路由数组中的组件,我们使用导入的变量,而不是字符串或动态组件——你将在后面看到原因。
/ 创建一个空插件 /
Vite插件只是一个添加了各种选项的对象。我们将很快开始添加这些选项。让我们创建一个实际上可以用作插件的javascript文件:
module.exports = function() { return {}}
然后,我们可以在 vite.config.js 中使用它,如下所示:
const viteAutoRoute = require('./plugin.js')module.exports = { plugins: [viteAutoRoute()],}
我们使用一个函数而不只是一个普通的对象,以便将来我们可以向插件添加自己的自定义选项。例如,也许我们想使用 viteAutoRoute({ pagesDir: './src/docs' }) 之类的名称指定自定义页面目录。
/ 为本地开发开辟道路 /
现在让我们实际利用我们之前创建的那些路由。为此我们将使用 configureServer vite插件选项。
module.exports = function () { const { imports, routes } = parsePagesDirectory() const moduleContent = ` ${imports.join('')} export const routes = [${routes.join(', ')}] ` 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() } }) }, ] return { configureServer }}
首先,我们创建一个字符串 moduleContent,其中包含路由javascript文件的所需内容。然后,我们创建 configureServer ——将用作vite开发服务器中的附加中间件的函数列表。每当vite在你的javascript或vue文件中遇到导入时,它就会向其开发服务器发出请求,在必要时进行一些转换,然后以浏览器可以处理的形式发送回来。
当它遇到类似 import { routes } from 'vue-auto-routes' 这样的东西时,它将会发出 @/modules/vue-auto-routes 的请求。因此,我们要做的是拦截该请求,并将生成的 moduleContent 作为正文返回,并将其声明为 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 () { // 我们之前的moduleContent和configureServer ... const rollupInputOptions = { plugins: [virtual({ 'vue-auto-routes': moduleContent })], } return { configureServer, rollupInputOptions }}
在这里,我们使用 rollupInputOptions vite插件选项,这使我们可以传递rollup插件。我们使用 @rollup/plugin-virtual 来获取模块名称,并让你返回所需的任何javascript内容。本质上与开发服务器解决方案做相同的事情。
现在,我们的路由将在本地开发和生产中都起作用。
/ 添加自定义块 /
各个Vue页面可能希望为其所对应的路由提供其他选项。例如,我们可能想要向我们的路由添加一些其他元选项,或使用自定义路由名称。为此,我们将重新实现 vue-cli-plugin-auto-routing 的路由块,以便在vue组件中执行以下操作:
{ "meta": { "requiresLogin": true, }}
为此,我们将使用 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('')} export const routes = [${routes.join(', ')}] ` 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(我们的块名)映射到一个函数,该函数本质上是返回另一个虚拟javascript文件。它有一个默认的导出函数,它为相关的 vue 文件取了一个组件,并添加了一个我们选择的额外字段 __routeOptions,它映射到我们在任何自定义 块中声明的代码。
然后,我们在路由生成代码中使用它 ( ...(${f.name}.__routeOptions || {}) ),并将其与特定路由的其他设置合并。这就是为什么我们在虚拟的 vue-auto-routes 文件中导入组件的原因——因此我们可以访问已添加的 __routeOptions 字段。
现在,我们可以在vue页面组件中使用 $route.meta.requiresLogin 之类的东西!