通过Vue CLI 可以方便的创建一个Vue 项目,但是对于实际项目来说还是不够的,所以一般都会根据业务的情况来在其基础上添加一些共性能力,减少创建新项目时的一些重复操作,本着学习和分享的目的,本文会介绍一下我们Vue 项目的前端架构设计,当然,有些地方可能不是最好的方式,毕竟大家的业务不尽相同,适合你的就是最好的。
除了介绍基本的架构设计,本文还会介绍如何开发一个Vue CLI 插件和preset 预设。
ps.本文基于Vue2.x版本,node版本16.5.0
创建一个基本项目
先使用Vue CLI 创建一个基本的项目:
vue create hello-world
然后选择Vue2 选项创建,初始项目结构如下:
接下来就在此基础上添砖加瓦。
路由
路由是必不可少的,安装vue-router :
npm install vue-router
修改App.vue 文件:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
* {
padding: 0;
margin: 0;
border: 0;
outline: none;
}
html,
body {
width: 100%;
height: 100%;
}
</style>
<style scoped>
#app {
width: 100%;
height: 100%;
display: flex;
}
</style>
增加路由出口,简单设置了一下页面样式。
接下来新增pages 目录用于放置页面, 把原本App.vue 的内容移到了Hello.vue :
路由配置我们选择基于文件进行配置,在src 目录下新建一个/src/router.config.js :
export default [
{
path: '/',
redirect: '/hello',
},
{
name: 'hello',
path: '/hello/',
component: 'Hello',
}
]
属性支持vue-router 构建选项routes的所有属性,component 属性传的是pages 目录下的组件路径,规定路由组件只能放到pages 目录下,然后新建一个/src/router.js 文件:
import Vue from 'vue'
import Router from 'vue-router'
import routes from './router.config.js'
Vue.use(Router)
const createRoute = (routes) => {
if (!routes) {
return []
}
return routes.map((item) => {
return {
...item,
component: () => {
return import('./pages/' + item.component)
},
children: createRoute(item.children)
}
})
}
const router = new Router({
mode: 'history',
routes: createRoute(routes),
})
export default router
使用工厂函数和import 方法来定义动态组件,需要递归对子路由进行处理。最后,在main.js 里面引入路由:
// main.js
// ...
import router from './router'// ++
// ...
new Vue({
router,// ++
render: h => h(App),
}).$mount('#app')
菜单
我们的业务基本上都需要一个菜单,默认显示在页面左侧,我们有内部的组件库,但没有对外开源,所以本文就使用Element 替代,菜单也通过文件来配置,新建/src/nav.config.js 文件:
export default [{
title: 'hello',
router: '/hello',
icon: 'el-icon-menu'
}]
然后修改App.vue 文件:
<template>
<div id="app">
<el-menu
style="width: 250px; height: 100%"
:router="true"
:default-active="defaultActive"
>
<el-menu-item
v-for="(item, index) in navList"
:key="index"
:index="item.router"
>
<i :class="item.icon"></i>
<span slot="title">{{ item.title }}</span>
</el-menu-item>
</el-menu>
<router-view />
</div>
</template>
<script>
import navList from './nav.config.js'
export default {
name: 'App',
data() {
return {
navList,
}
},
computed: {
defaultActive() {
let path = this.$route.path
// 检查是否有完全匹配的
let fullMatch = navList.find((item) => {
return item.router === path
})
// 没有则检查是否有部分匹配
if (!fullMatch) {
fullMatch = navList.find((item) => {
return new RegExp('^' + item.router + '/').test(path)
})
}
return fullMatch ? fullMatch.router : ''
},
},
}
</script>
效果如下:
当然,上述只是意思一下,实际的要复杂一些,毕竟这里连嵌套菜单的情况都没考虑。
权限
我们的权限颗粒度比较大,只控制到路由层面,具体实现就是在菜单配置和路由配置里的每一项都新增一个code 字段,然后通过请求获取当前用户有权限的code ,没有权限的菜单默认不显示,访问没有权限的路由会重定向到403 页面。
获取权限数据
权限数据随用户信息接口一起返回,然后存储到vuex 里,所以先配置一下vuex ,安装:
npm install vuex --save
新增/src/store.js :
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
userInfo: null,
},
actions: {
// 请求用户信息
async getUserInfo(ctx) {
let userInfo = {
// ...
code: ['001'] // 用户拥有的权限
}
ctx.commit('setUserInfo', userInfo)
}
},
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
}
},
})
在main.js 里面先获取用户信息,然后再初始化Vue :
// ...
import store from './store'
// ...
const initApp = async () => {
await store.dispatch('getUserInfo')
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
}
initApp()
菜单
修改nav.config.js 新增code 字段:
// nav.config.js
export default [{
title: 'hello',
router: '/hello',
icon: 'el-icon-menu'
code: '001',
}]
然后在App.vue 里过滤掉没有权限的菜单:
export default {
name: 'App',
data() {
return {
navList,// --
}
},
computed: {
navList() {// ++
const { userInfo } = this.$store.state
if (!userInfo || !userInfo.code || userInfo.code.length <= 0) return []
return navList.filter((item) => {
return userInfo.code.includes(item.code)
})
}
}
}
这样没有权限的菜单就不会显示出来。
路由
修改router.config.js ,增加code 字段:
export default [{
path: '/',
redirect: '/hello',
},
{
name: 'hello',
path: '/hello/',
component: 'Hello',
code: '001',
}
]
code 是自定义字段,需要保存到路由记录的meta 字段里,否则最后会丢失,修改createRoute 方法:
// router.js
// ...
const createRoute = (routes) => {
// ...
return routes.map((item) => {
return {
...item,
component: () => {
return import('./pages/' + item.component)
},
children: createRoute(item.children),
meta: {// ++
code: item.code
}
}
})
}
// ...
然后需要拦截路由跳转,判断是否有权限,没有权限就转到403 页面:
// router.js
// ...
import store from './store'
// ...
router.beforeEach((to, from, next) => {
const userInfo = store.state.userInfo
const code = userInfo && userInfo.code && userInfo.code.length > 0 ? userInfo.code : []
// 去错误页面直接跳转即可,否则会引起死循环
if (/^\/error\//.test(to.path)) {
return next()
}
// 有权限直接跳转
if (code.includes(to.meta.code)) {
next()
} else if (to.meta.code) { // 路由存在,没有权限,跳转到403页面
next({
path: '/error/403'
})
} else { // 没有code则代表是非法路径,跳转到404页面
next({
path: '/error/404'
})
}
})
error 组件还没有,新增一下:
// pages/Error.vue
<template>
<div class="container">{{ errorText }}</div>
</template>
<script>
const map = {
403: '无权限',
404: '页面不存在',
}
export default {
name: 'Error',
computed: {
errorText() {
return map[this.$route.params.type] || '未知错误'
},
},
}
</script>
<style scoped>
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
}
</style>
接下来修改一下router.config.js ,增加错误页面的路由,及增加一个测试无权限的路由:
// router.config.js
export default [
// ...
{
name: 'Error',
path: '/error/:type',
component: 'Error',
},
{
name: 'hi',
path: '/hi/',
code: '无权限测试,请输入hi',
component: 'Hello',
}
]
因为这个code 用户并没有,所以现在我们打开/hi 路由会直接跳转到403 路由:
面包屑
和菜单类似,面包屑也是大部分页面都需要的,面包屑的组成分为两部分,一部分是在当前菜单中的位置,另一部分是在页面操作中产生的路径。第一部分的路径因为可能会动态的变化,所以一般是通过接口随用户信息一起获取,然后存到vuex 里,修改store.js :
// ...
async getUserInfo(ctx) {
let userInfo = {
code: ['001'],
breadcrumb: {// 增加面包屑数据
'001': ['你好'],
},
}
ctx.commit('setUserInfo', userInfo)
}
// ...
第二部分的在router.config.js 里面配置:
export default [
//...
{
name: 'hello',
path: '/hello/',
component: 'Hello',
code: '001',
breadcrumb: ['世界'],// ++
}
]
breadcrumb 字段和code 字段一样,属于自定义字段,但是这个字段的数据是给组件使用的,组件需要获取这个字段的数据然后在页面上渲染出面包屑菜单,所以保存到meta 字段上虽然可以,但是在组件里面获取比较麻烦,所以我们可以设置到路由记录的props 字段上,直接注入为组件的props ,这样使用就方便多了,修改router.js :
// router.js
// ...
const createRoute = (routes) => {
// ...
return routes.map((item) => {
return {
...item,
component: () => {
return import('./pages/' + item.component)
},
children: createRoute(item.children),
meta: {
code: item.code
},
props: {// ++
breadcrumbObj: {
breadcrumb: item.breadcrumb,
code: item.code
}
}
}
})
}
// ...
这样在组件里声明一个breadcrumbObj 属性即可获取到面包屑数据,可以看到把code 也一同传过去了,这是因为还要根据当前路由的code 从用户接口获取的面包屑数据中取出该路由code 对应的面包屑数据,然后把两部分的进行合并,这个工作为了避免让每个组件都要做一遍,我们可以写在一个全局的mixin 里,修改main.js :
// ...
Vue.mixin({
props: {
breadcrumbObj: {
type: Object,
default: () => null
}
},
computed: {
breadcrumb() {
if (!this.breadcrumbObj) {
return []
}
let {
code,
breadcrumb
} = this.breadcrumbObj
// 用户接口获取的面包屑数据
let breadcrumbData = this.$store.state.userInfo.breadcrumb
// 当前路由是否存在面包屑数据
let firstBreadcrumb = breadcrumbData && Array.isArray(breadcrumbData[code]) ? breadcrumbData[code] : []
// 合并两部分的面包屑数据
return firstBreadcrumb.concat(breadcrumb || [])
}
}
})
// ...
initApp()
最后我们在Hello.vue 组件里面渲染一下面包屑:
<template>
<div class="container">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(item, index) in breadcrumb" :key="index">{{item}}</el-breadcrumb-item>
</el-breadcrumb>
// ...
</div>
</template>
当然,我们的面包屑是不需要支持点击的,如果需要的话可以修改一下面包屑的数据结构。
接口请求
接口请求使用的是axios ,但是会做一些基础配置、拦截请求和响应,因为还是有一些场景需要直接使用未配置的axios ,所以我们默认创建一个新实例,先安装:
npm install axios
然后新建一个/src/api/ 目录,在里面新增一个httpInstance.js 文件:
import axios from 'axios'
// 创建一个新实例
const http = axios.create({
timeout: 10000,// 超时时间设为10秒
withCredentials: true,// 跨域请求时是否需要使用凭证,设置为需要
headers: {
'X-Requested-With': 'XMLHttpRequest'// 表明是ajax请求
},
})
export default http
然后增加一个请求拦截器:
// ...
// 请求拦截器
http.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// ...
其实啥也没做,先写出来,留着不同的项目按需修改。
最后增加一个响应拦截器:
// ...
import { Message } from 'element-ui'
// ...
// 响应拦截器
http.interceptors.response.use(
function (response) {
// 对错误进行统一处理
if (response.data.code !== '0') {
// 弹出错误提示
if (!response.config.noMsg && response.data.msg) {
Message.error(response.data.msg)
}
return Promise.reject(response)
} else if (response.data.code === '0' && response.config.successNotify && response.data.msg) {
// 弹出成功提示
Message.success(response.data.msg)
}
return Promise.resolve({
code: response.data.code,
msg: response.data.msg,
data: response.data.data,
})
},
function (error) {
// 登录过期
if (error.status === 403) {
location.reload()
return
}
// 超时提示
if (error.message.indexOf('timeout') > -1) {
Message.error('请求超时,请重试!')
}
return Promise.reject(error)
},
)
// ...
我们约定一个成功的响应(状态码为200)结构如下:
{
code: '0',
msg: 'xxx',
data: xxx
}
code 不为0 即使状态码为200 也代表请求出错,那么弹出错误信息提示框,如果某次请求不希望自动弹出提示框的话也可以禁止,只要在请求时加上配置参数noMsg: true 即可,比如:
axios.get('/xxx', {
noMsg: true
})
请求成功默认不弹提示,需要的话可以设置配置参数successNotify: true 。
状态码在非[200,300) 之间的错误只处理两种,登录过期和请求超时,其他情况可根据项目自行修改。
多语言
多语言使用vue-i18n实现,先安装:
npm install vue-i18n@8
vue-i18n 的9.x 版本支持的是Vue3 ,所以我们使用8.x 版本。
然后创建一个目录/src/i18n/ ,在目录下新建index.js 文件用来创建i18n 实例:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n()
export default i18n
除了创建实例其他啥也没做,别急,接下来我们一步步来。
我们的总体思路是,多语言的源数据在/src/i18n/ 下,然后编译成json 文件放到项目的/public/i18n/ 目录下,页面的初始默认语言也是和用户信息接口一起返回,页面根据默认的语言类型使用ajax 请求public 目录下的对应json 文件,调用VueI18n 的方法动态进行设置。
这么做的目的首先是方便修改页面默认语言,其次是多语言文件不和项目代码打包到一起,减少打包时间,按需请求,减少不必要的资源请求。
接下来我们新建页面的中英文数据,目录结构如下:
比如中文的hello.json 文件内容如下(忽略笔者的低水平翻译~):
在index.js 文件里导入hello.json 文件及ElementUI 的语言文件,并合并导出:
import hello from './hello.json'
import elementLocale from 'element-ui/lib/locale/lang/zh-CN'
export default {
hello,
...elementLocale
}
为什么是...elementLocale 呢,因为传给Vue-i18n 的多语言数据结构是这样的:
我们是把index.js 的整个导出对象作为vue-i18n 的多语言数据的,而ElementUI 的多语言文件是这样的:
所以我们需要把这个对象的属性和hello 属性合并到一个对象上。
接下来我们需要把它导出的数据到写到一个json 文件里并输出到public 目录下,这可以直接写个js 脚本文件来做这个事情,但是为了和项目的源码分开我们写成一个npm 包。
创建一个npm工具包
我们在项目的平级下创建一个包目录,并使用npm init 初始化:
命名为-tool 的原因是后续可能还会有类似编译多语言这种需求,所以取一个通用名字,方便后面增加其他功能。
命令行交互工具使用Commander.js,安装:
npm install commander
然后新建入口文件index.js :
#!/usr/bin/env node
const {
program
} = require('commander');
// 编译多语言文件
const buildI18n = () => {
console.log('编译多语言文件');
}
program
.command('i18n') // 添加i18n命令
.action(buildI18n)
program.parse(process.argv);
因为我们的包是要作为命令行工具使用的,所以文件第一行需要指定脚本的解释程序为node ,然后使用commander 配置了一个i18n 命令,用来编译多语言文件,后续如果要添加其他功能新增命令即可,执行文件有了,我们还要在包的package.json 文件里添加一个bin 字段,用来指示我们的包里有可执行文件,让npm 在安装包的时候顺便给我们创建一个符号链接,把命令映射到文件。
// hello-tool/package.json
{
"bin": {
"hello": "./index.js"
}
}
因为我们的包还没有发布到npm ,所以直接链接到项目上使用,先在hello-tool 目录下执行:
npm link
然后到我们的hello world 目录下执行:
npm link hello-tool
现在在命令行输入hello i18n 试试:
编译多语言文件
接下来完善buildI18n 函数的逻辑,主要分三步:
1.清空目标目录,也就是/public/i18n 目录
2.获取/src/i18n 下的各种多语言文件导出的数据
3.写入到json 文件并输出到/public/i18n 目录下
代码如下:
const path = require('path')
const fs = require('fs')
// 编译多语言文件
const buildI18n = () => {
// 多语言源目录
let srcDir = path.join(process.cwd(), 'src/i18n')
// 目标目录
let destDir = path.join(process.cwd(), 'public/i18n')
// 1.清空目标目录,clearDir是一个自定义方法,递归遍历目录进行删除
clearDir(destDir)
// 2.获取源多语言导出数据
let data = {}
let langDirs = fs.readdirSync(srcDir)
langDirs.forEach((dir) => {
let dirPath = path.join(srcDir, dir)
// 读取/src/i18n/xxx/index.js文件,获取导出的多语言对象,存储到data对象上
let indexPath = path.join(dirPath, 'index.js')
if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) {
// 使用require加载该文件模块,获取导出的数据
data[dir] = require(indexPath)
}
})
// 3.写入到目标目录
Object.keys(data).forEach((lang) => {
// 创建public/i18n目录
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir)
}
let dirPath = path.join(destDir, lang)
let filePath = path.join(dirPath, 'index.json')
// 创建多语言目录
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath)
}
// 创建json文件
fs.writeFileSync(filePath, JSON.stringify(data[lang], null, 4))
})
console.log('多语言编译完成');
}
代码很简单,接下来我们运行命令:
报错了,提示不能在模块外使用import ,其实新版本的nodejs 已经支持ES6 的模块语法了,可以把文件后缀换成.mjs ,或者在package.json 文件里增加type=module 字段,但是都要做很多修改,这咋办呢,有没有更简单的方法呢?把多语言文件换成commonjs 模块语法?也可以,但是不太优雅,不过好在babel 提供了一个@babel/register包,可以把babel 绑定到node 的require 模块上,然后可以在运行时进行即时编译,也就是当require('/src/i18n/xxx/index.js') 时会先由babel 进行编译,编译完当然就不存在import 语句了,先安装:
npm install @babel/core @babel/register @babel/preset-env
然后新建一个babel 配置文件:
// hello-tool/babel.config.js
module.exports = {
'presets': ['@babel/preset-env']
}
最后在hello-tool/index.js 文件里使用:
const path = require('path')
const {
program
} = require('commander');
const fs = require('fs')
require("@babel/register")({
configFile: path.resolve(__dirname, './babel.config.js'),
})
// ...
接下来再次运行命令:
可以看到编译完成了,文件也输出到了public 目录下,但是json 文件里存在一个default 属性,这一层显然我们是不需要的,所以require('i18n/xxx/index.js') 时我们存储导出的default 对象即可,修改hello-tool/index.js :
const buildI18n = () => {
// ...
langDirs.forEach((dir) => {
let dirPath = path.join(srcDir, dir)
let indexPath = path.join(dirPath, 'index.js')
if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) {
data[dir] = require(indexPath).default// ++
}
})
// ...
}
效果如下:
使用多语言文件
首先修改一下用户接口的返回数据,增加默认语言字段:
// /src/store.js
// ...
async getUserInfo(ctx) {
let userInfo = {
// ...
language: 'zh_CN'// 默认语言
}
ctx.commit('setUserInfo', userInfo)
}
// ...
然后在main.js 里面获取完用户信息后立刻请求并设置多语言:
// /src/main.js
import { setLanguage } from './utils'// ++
import i18n from './i18n'// ++
const initApp = async () => {
await store.dispatch('getUserInfo')
await setLanguage(store.state.userInfo.language)// ++
new Vue({
i18n,// ++
router,
store,
render: h => h(App),
}).$mount('#app')
}
setLanguage 方法会请求多语言文件并切换:
// /src/utils/index.js
import axios from 'axios'
import i18n from '../i18n'
// 请求并设置多语言数据
const languageCache = {}
export const setLanguage = async (language = 'zh_CN') => {
let languageData = null
// 有缓存,使用缓存数据
if (languageCache[language]) {
languageData = languageCache[language]
} else {
// 没有缓存,发起请求
const {
data
} = await axios.get(`/i18n/${language}/index.json`)
languageCache[language] = languageData = data
}
// 设置语言环境的 locale 信息
i18n.setLocaleMessage(language, languageData)
// 修改语言环境
i18n.locale = language
}
然后把各个组件里显示的信息都换成$t('xxx') 形式,当然,菜单和路由都需要做相应的修改,效果如下:
可以发现ElementUI 组件的语言并没有变化,这是当然的,因为我们还没有处理它,修改很简单,ElementUI 支持自定义i18n 的处理方法:
// /src/main.js
// ...
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
// ...
通过CLI插件生成初始多语言文件
最后还有一个问题,就是项目初始化时还没有多语言文件怎么办,难道项目创建完还要先手动运行命令编译一下多语言?有几种解决方法:
1.最终一般会提供一个项目脚手架,所以默认的模板里我们就可以直接加上初始的多语言文件;
2.启动服务和打包时先编译一下多语言文件,像这样:
"scripts": {
"serve": "hello i18n && vue-cli-service serve",
"build": "hello i18n && vue-cli-service build"
}
3.开发一个Vue CLI 插件来帮我们在项目创建完时自动运行一次多语言编译命令;
接下来简单实现一下第三种方式,同样在项目同级新建一个插件目录,并创建相应的文件(注意插件的命名规范):
根据插件开发规范,index.js 为Service 插件的入口文件,Service 插件可以修改webpack 配置,创建新的 vue-cli service 命令或者修改已经存在的命令,我们用不上,我们的逻辑在generator.js 里,这个文件会在两个场景被调用:
1.项目创建期间,CLI 插件被作为项目创建preset 的一部分被安装时
2.项目创建完成时通过vue add 或vue invoke 单独安装插件时调用
我们需要的刚好是在项目创建时或安装该插件时自动帮我们运行多语言编译命令,generator.js 需要导出一个函数,内容如下:
const {
exec
} = require('child_process');
module.exports = (api) => {
// 为了方便在项目里看到编译多语言的命令,我们把hello i18n添加到项目的package.json文件里,修改package.json文件可以使用提供的api.extendPackage方法
api.extendPackage({
scripts: {
buildI18n: 'hello i18n'
}
})
// 该钩子会在文件写入硬盘后调用
api.afterInvoke(() => {
// 获取项目的完整路径
let targetDir = api.generator.context
// 进入项目文件夹,然后运行命令
exec(`cd ${targetDir} && npm run buildI18n`, (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log(stdout);
console.error(stderr);
});
})
}
我们在afterInvoke 钩子里运行编译命令,因为太早运行可能依赖都还没有安装完成,另外我们还获取了项目的完整路径,这是因为通过preset 配置插件时,插件被调用时可能不在实际的项目文件夹,比如我们在a 文件夹下通过该命令创建b 项目:
vue create b
插件被调用时是在a 目录,显然hello-i18n 包是被安装在b 目录,所以我们要先进入项目实际目录然后运行编译命令。
接下来测试一下,先在项目下安装该插件:
npm install --save-dev file:完整路径\vue-cli-plugin-i18n
然后通过如下命令来调用插件的生成器:
vue invoke vue-cli-plugin-i18n
效果如下:
可以看到项目的package.json 文件里面已经注入了编译命令,并且命令也自动执行生成了多语言文件。
Mock数据
Mock 数据推荐使用Mock,使用很简单,新建一个mock 数据文件:
然后在/api/index.js 里引入:
就这么简单,该请求即可被拦截:
规范化
有关规范化的配置,比如代码风格检查、git 提交规范等,笔者之前写过一篇组件库搭建的文章,其中一个小节详细的介绍了配置过程,可移步:【万字长文】从零配置一个vue组件库-规范化配置小节。
其他
请求代理
本地开发测试接口请求时难免会遇到跨域问题,可以配置一下webpack-dev-server 的代理选项,新建vue.config.js 文件:
module.exports = {
devServer: {
proxy: {
'^/api/': {
target: 'http://xxx:xxx',
changeOrigin: true
}
}
}
}
编译node_modules内的依赖
默认情况下babel-loader 会忽略所有node_modules 中的文件,但是有些依赖可能是没有经过编译的,比如我们自己编写的一些包为了省事就不编译了,那么如果用了最新的语法,在低版本浏览器上可能就无法运行了,所以打包的时候也需要对它们进行编译,要通过Babel 显式转译一个依赖,可以在这个transpileDependencies 选项配置,修改vue.config.js :
module.exports = {
// ...
transpileDependencies: ['your-package-name']
}
环境变量
需要环境变量可以在项目根目录下新建.env 文件,需要注意的是如果要通过插件渲染. 开头的模板文件,要用_ 来替代点,也就是_env ,最终会渲染为. 开头的文件。
脚手架
当我们设计好了一套项目结构后,肯定是作为模板来快速创建项目的,一般会创建一个脚手架工具来生成,但是Vue CLI 提供了preset (预设)的能力,所谓preset 指的是一个包含创建新项目所需预定义选项和插件的 JSON 对象,所以我们可以创建一个CLI 插件来创建模板,然后创建一个preset ,再把这个插件配置到preset 里,这样使用vue create 命令创建项目时使用我们的自定义preset 即可。
创建一个生成模板的CLI插件
新建插件目录如下:
可以看到这次我们创建了一个generator 目录,因为我们需要渲染模板,而模板文件就会放在这个目录下,新建一个template 目录,然后把我们前文配置的项目结构完整的复制进去(不包括package.json):
现在我们来完成/generator/index.js 文件的内容:
1.因为不包括package.json ,所以我们要修改vue 项目默认的package.json ,添加我们需要的东西,使用的就是前面提到的api.extendPackage 方法:
// generator/index.js
module.exports = (api) => {
// 扩展package.json
api.extendPackage({
"dependencies": {
"axios": "^0.25.0",
"element-ui": "^2.15.6",
"vue-i18n": "^8.27.0",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"mockjs": "^1.1.0",
"sass": "^1.49.7",
"sass-loader": "^8.0.2",
"hello-tool": "^1.0.0"// 注意这里,不要忘记把我们的工具包加上
}
})
}
添加了一些额外的依赖,包括我们前面开发的hello-tool 。
2.渲染模板
module.exports = (api) => {
// ...
api.render('./template')
}
render 方法会渲染template 目录下的所有文件。
创建一个自定义preset
插件都有了,最后让我们来创建一下自定义preset ,新建一个preset.json 文件,把我们前面写的template 插件和i18n 插件一起配置进去:
{
"plugins": {
"vue-cli-plugin-template": {
"version": "^1.0.0"
},
"vue-cli-plugin-i18n": {
"version": "^1.0.0"
}
}
}
同时为了测试这个preset ,我们再创建一个空目录:
然后进入test-preset 目录运行vue create 命令时指定我们的preset 路径即可:
vue create --preset ../preset.json my-project
效果如下:
远程使用preset
preset 本地测试没问题了就可以上传到仓库里,之后就可以给别人使用了,比如笔者上传到了这个仓库:https://github.com/wanglin2/Vue_project_design,那么你可以这么使用:
vue create --preset wanglin2/Vue_project_design project-name
总结
如果有哪里不对的或是更好的,评论区见~
|