vue-cli 的 Router 插件(@vue/cli-plugin-router)完全指南:安装、生成器原理与 Vue 2/Vue 3 差异
vue-cli 的 Router 插件(@vue/cli-plugin-router)完全指南:安装、生成器原理与 Vue 2/Vue 3 差异
导读
@vue/cli-plugin-router 是 vue-cli 官方提供的 Vue Router 集成插件:它既能在创建项目时按需接入路由,也支持在已创建的项目中通过一条命令快速补齐路由能力。本文将以该插件的源码与测试为证据,完整讲解 vue add router 的用法、交互式 prompt、Vue 2(vue-router 3)与 Vue 3(vue-router 4)两套模板的生成逻辑、history 模式与代码分割的实现方式,以及插件在 TypeScript 项目中的行为差异,帮助你彻底理解 vue-cli 路由集成的每一步。
一、插件定位与安装方式
@vue/cli-plugin-router 是 vue-cli 的路由插件,其 package.json(见 package.json)声明了自身的元信息:主入口为 index.js,核心依赖为 @vue/cli-shared-utils,并以 @vue/cli-service(^3.0.0 || ^4.0.0 || ^5.0.0-0)作为 peerDependency,即它必须运行在 vue-cli-service 提供的生成器环境中。
与 README 中描述一致,在已经创建好的项目中安装该插件只需一条命令:
vue add router
该命令会做两件事:安装插件包并执行其 generator,同时以交互式方式向你提问(见下文 prompt 小节)。值得注意的是,在 vue create 创建新项目的过程中,勾选 "Router" 特性时会直接内置该插件并调用同一套 generator,因此两套流程产出的代码结构完全一致。
二、交互式提问:historyMode
当插件被后期安装(late-install,即通过 vue add router 或 vue invoke 触发)时,prompts.js 会向用户提出一个问题:
const { chalk } = require('@vue/cli-shared-utils')
module.exports = [
{
name: 'historyMode',
type: 'confirm',
message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`
}
]
关键参数说明:
| 参数 | 类型 | 含义 | 注意事项 |
|---|---|---|---|
historyMode |
boolean(confirm) | 是否使用 HTML5 History 模式 | 使用后 URL 不再包含 #,但生产环境需要服务器配置 index fallback(如 nginx 的 try_files),否则刷新非根路径会 404 |
这个 historyMode 选项最终作为 options.historyMode 传入 generator,直接决定生成的路由配置代码(详见第三节)。测试 routerGenerator.spec.js 中的 history mode 用例验证了:当 historyMode: true 时,生成的 src/router/index.js 中会出现 history 相关代码。
三、生成器原理:一次生成,两套模板
插件的核心逻辑位于 generator/index.js,它根据 rootOptions.vueVersion 判断项目是 Vue 2 还是 Vue 3:
module.exports = (api, options = {}, rootOptions = {}) => {
const isVue3 = (rootOptions.vueVersion === '3')
api.injectImports(api.entryFile, `import router from './router'`)
if (isVue3) {
api.transformScript(api.entryFile, require('./injectUseRouter'))
api.extendPackage({
dependencies: {
'vue-router': '^4.0.3'
}
})
} else {
api.injectRootOptions(api.entryFile, `router`)
api.extendPackage({
dependencies: {
'vue-router': '^3.5.1'
}
})
}
api.render('./template', {
historyMode: options.historyMode,
doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
hasTypeScript: api.hasPlugin('typescript')
})
if (isVue3) {
api.render('./template-vue3', {
historyMode: options.historyMode,
doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
hasTypeScript: api.hasPlugin('typescript')
})
}
if (api.invoking) {
if (api.hasPlugin('typescript')) {
/* eslint-disable-next-line node/no-extraneous-require */
const convertFiles = require('@vue/cli-plugin-typescript/generator/convert')
convertFiles(api)
}
}
}
其执行流程可拆解为四步:
- 注入入口 import:向
api.entryFile(通常是src/main.js)注入import router from './router'; - 按版本分支处理:
- Vue 3:使用
api.transformScript配合 injectUseRouter.js 做 AST 转换,把createApp(...)改写为createApp(...).use(router),并安装vue-router@^4.0.3; - Vue 2:向根组件选项注入
router(api.injectRootOptions),并安装vue-router@^3.5.1;
- Vue 3:使用
- 渲染模板:Vue 2 渲染
./template,Vue 3 额外渲染./template-vue3,两者共享historyMode、doesCompile、hasTypeScript三个渲染变量; - TypeScript 转换:若是在已有项目上调用(
api.invoking为真)且项目使用了 TypeScript,则复用@vue/cli-plugin-typescript/generator/convert将生成的.js文件转换为.ts。
3.1 AST 转换注入 use(router)(Vue 3 专属)
在 Vue 3 中,路由不能再通过 Vue.use() 全局安装,而必须显式调用 app.use(router)。injectUseRouter.js 使用 jscodeshift 遍历入口文件的调用表达式:
const appRoots = root.find(j.CallExpression, (node) => {
if (j.Identifier.check(node.callee) && node.callee.name === 'createApp') return true
if (
j.MemberExpression.check(node.callee) &&
j.Identifier.check(node.callee.object) && node.callee.object.name === 'Vue' &&
j.Identifier.check(node.callee.property) && node.callee.property.name === 'createApp'
) return true
})
appRoots.replaceWith(({ node: createAppCall }) => {
return j.callExpression(
j.memberExpression(createAppCall, j.identifier('use')),
[j.identifier('router')]
)
})
它同时兼容 createApp(App) 与 Vue.createApp(App) 两种写法,将其统一替换为 createApp(App).use(router)。测试 use with Vue 3 用例断言生成的 src/main.js 包含 .use(router)(见 routerGenerator.spec.js),正是对这一步骤的验证。
3.2 版本与依赖的对应关系
从源码可以确认插件的版本策略,这是 Vue 2 / Vue 3 项目的重要差异:
| 项目 Vue 版本 | 安装的 vue-router | 路由实例化方式 | 入口注入方式 |
|---|---|---|---|
| Vue 2 | ^3.5.1 |
new VueRouter({ mode, routes }) + Vue.use(VueRouter) |
根组件选项注入 router |
| Vue 3 | ^4.0.3 |
createRouter({ history, routes }) |
createApp(App).use(router)(AST 改写) |
四、生成的路由代码模板详解
4.1 Vue 2 模板(vue-router 3)
template/src/router/index.js 是 Vue 2 项目的路由入口模板,它使用 EJS 语法按渲染变量动态生成代码:
import Vue from 'vue'
<%_ if (hasTypeScript) { _%>
import VueRouter, { RouteConfig } from 'vue-router'
<%_ } else { _%>
import VueRouter from 'vue-router'
<%_ } _%>
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
<%_ if (hasTypeScript) { _%>
const routes: Array<RouteConfig> = [
<%_ } else { _%>
const routes = [
<%_ } _%>
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
<%_ if (doesCompile) { _%>
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
<%_ } else { _%>
component: function () {
return import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
<%_ } _%>
}
]
const router = new VueRouter({
<%_ if (historyMode) { _%>
mode: 'history',
base: process.env.BASE_URL,
<%_ } _%>
routes
})
export default router
几个值得注意的实现细节:
- 路由级代码分割:
/about路由使用动态import()并带webpackChunkName: "about"注释,webpack 会为它单独生成about.[hash].js分块,访问该路由时才懒加载。注释明确说明了这一行为; - doesCompile 分支:当项目启用了 Babel 或 TypeScript(
api.hasPlugin('babel') || api.hasPlugin('typescript'))时,直接使用箭头函数() => import(...);否则退回普通函数function () { return import(...) },避免在无编译能力的项目中因箭头函数转换问题出错。测试use with Babel用例验证了启用 Babel 后生成component: () => import(见 routerGenerator.spec.js); - historyMode 分支:开启后为
new VueRouter添加mode: 'history'和base: process.env.BASE_URL,其中base取自 vue-cli 的publicPath配置(即process.env.BASE_URL),保证在子路径部署时路由基准正确。
4.2 Vue 3 模板(vue-router 4)
template-vue3/src/router/index.js 面向 Vue 3 项目,API 完全不同:
import { createRouter<%
if (historyMode) {
%>, createWebHistory<%
} else {
%>, createWebHashHistory<%
}
if (hasTypeScript) {
%>, RouteRecordRaw<%
}
%> } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes<% if (hasTypeScript) { %>: Array<RouteRecordRaw><% } %> = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
<%_ if (doesCompile) { _%>
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
<%_ } else { _%>
component: function () {
return import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
<%_ } _%>
}
]
const router = createRouter({
<%_ if (historyMode) { _%>
history: createWebHistory(process.env.BASE_URL),
<%_ } else { _%>
history: createWebHashHistory(),
<%_ } _%>
routes
})
export default router
与 Vue 2 模板的关键差异:
- 实例化方式:
new VueRouter({...})变为createRouter({...}); - history 对象化:
mode: 'history'字符串配置改为传入 history 实例——history 模式用createWebHistory(process.env.BASE_URL)(同样透传BASE_URL),hash 模式用createWebHashHistory(); - TypeScript 类型:启用 TypeScript 时,从
vue-router额外导入RouteRecordRaw类型,并将routes声明为Array<RouteRecordRaw>。测试Vue 3 + TypeScript用例对此有明确断言(见 routerGenerator.spec.js)。
4.3 默认路由与视图组件
模板同时生成两个默认视图:src/views/HomeView.vue 和 src/views/AboutView.vue,分别对应 /(home)与 /about(about)两个路由。/ 使用静态导入,/about 使用懒加载——这一设计示范了 vue-router 推荐的按路由拆分代码的最佳实践。
4.4 App.vue 的模板替换
插件会基于 @vue/cli-service/generator/template/src/App.vue 进行扩展替换(见 template/src/App.vue 与 template-vue3/src/App.vue 的 front matter):
---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace:
- !!js/regexp /<template>[^]*?<\/template>/
- !!js/regexp /\n<script>[^]*?<\/script>\n/
- !!js/regexp / margin-top[^]*?<\/style>/
---
替换后的模板内容包含:
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
其中 <router-link> 用于声明式导航,<router-view/> 是路由组件的渲染出口。样式部分针对 nav 和 nav a.router-link-exact-active 做了高亮处理(激活路由链接为 #42b983),并且根据 rootOptions.cssPreprocessor 自动适配三种 CSS 写法:无预处理器时输出原生 CSS,使用 stylus 时输出缩进语法,其他预处理器(scss/less 等)输出嵌套语法。
需要注意 Vue 2 与 Vue 3 模板的一个细节差异:Vue 2 的 App.vue 保留 <div id="app"> 包裹,而 Vue 3 版本没有——测试 use with Vue 3 中断言 App.vue 不包含 <div id="app">(见 routerGenerator.spec.js)。
五、TypeScript 项目中的额外处理
在已有的 TypeScript 项目中通过 vue invoke 安装本插件时(api.invoking 为真),generator 会额外调用 @vue/cli-plugin-typescript/generator/convert 把新生成的 .js 文件转换成 .ts(见 generator/index.js),因此最终生成的路由文件是 src/router/index.ts。该文件在类型层面会有以下特征:
- Vue 2 + TS:
import VueRouter, { RouteConfig } from 'vue-router',const routes: Array<RouteConfig> = [...]; - Vue 3 + TS:额外导入
RouteRecordRaw,const routes: Array<RouteRecordRaw> = [...]。
测试 Vue 3 + TypeScript 用例分别断言了这两点(见 routerGenerator.spec.js)。
六、测试覆盖:插件行为的验证依据
插件的完整行为都由 routerGenerator.spec.js 通过 @vue/cli-test-utils/generateWithPlugin 驱动真实 generator 验证,六个用例分别覆盖:
| 测试用例 | 验证点 |
|---|---|
| base | 生成 src/router/index.js(不含 history)、两个视图、App.vue 含 router-link、依赖含 vue-router |
| history mode | historyMode: true 时路由文件包含 history 配置 |
| use with Babel | 有 Babel 时使用 component: () => import 箭头函数写法 |
| use with Vue 3 | 使用 createRouter、createWebHashHistory()、入口含 .use(router)、App.vue 无 #app 包裹、vue-router 版本为 ^4 |
| Vue 3 + History Mode | 使用 createWebHistory(process.env.BASE_URL) |
| Vue 3 + TypeScript | 生成 src/router/index.ts,包含 RouteRecordRaw 类型与 Array<RouteRecordRaw> 声明 |
这些用例与本文前述源码分析一一对应,可作为插件行为最直接的验证入口。
七、实战建议与常见问题
- history 模式的部署前提:选择 history 模式后,URL 不再带
#,但生产环境必须为所有非静态路径配置回退到index.html(例如 nginx 的try_files $uri $uri/ /index.html),否则直接访问/about这类深层链接会 404。这是 prompt 中黄色警告的由来; vue add router与vue invoke router的区别:vue add会先安装插件包再触发 generator;而vue invoke只对已安装的插件重新执行 generator,通常用于在已有项目中补装新特性,或修改选项后重新生成;- 代码分割是默认行为:模板中
/about路由的懒加载写法是刻意设计的,新增路由时建议保持"首屏静态导入 + 次级路由动态导入"的节奏,以控制首屏包体积; - TypeScript 项目的版本差异:若在 Vue 3 + TS 项目中,路由文件为
.ts且带有RouteRecordRaw类型约束,新增路由时会获得编译期类型检查。
结语
@vue/cli-plugin-router 虽然 README 仅有一行安装命令,但其背后是一套完整的、版本感知的代码生成体系:通过 generator 判断 Vue 大版本、通过 AST 改写入口文件、通过 EJS 模板按需渲染 history 模式与 TypeScript 类型。理解 generator/index.js、template 与 template-vue3 三者的协作关系,你就能在自定义脚手架或二次开发时轻松复刻这套路由集成能力。