通過Vue CLI
可以方便的創(chuàng)建一個Vue
項(xiàng)目,但是對于實(shí)際項(xiàng)目來說還是不夠的凸郑,所以一般都會根據(jù)業(yè)務(wù)的情況來在其基礎(chǔ)上添加一些共性能力裳食,減少創(chuàng)建新項(xiàng)目時的一些重復(fù)操作,本著學(xué)習(xí)和分享的目的芙沥,本文會介紹一下我們Vue
項(xiàng)目的前端架構(gòu)設(shè)計(jì)诲祸,當(dāng)然,有些地方可能不是最好的方式而昨,畢竟大家的業(yè)務(wù)不盡相同救氯,適合你的就是最好的。
除了介紹基本的架構(gòu)設(shè)計(jì)歌憨,本文還會介紹如何開發(fā)一個Vue CLI
插件和preset
預(yù)設(shè)着憨。
ps.本文基于Vue2.x版本,node版本16.5.0
創(chuàng)建一個基本項(xiàng)目
先使用Vue CLI
創(chuàng)建一個基本的項(xiàng)目:
vue create hello-world
然后選擇Vue2
選項(xiàng)創(chuàng)建务嫡,初始項(xiàng)目結(jié)構(gòu)如下:
接下來就在此基礎(chǔ)上添磚加瓦甲抖。
路由
路由是必不可少的,安裝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>
增加路由出口心铃,簡單設(shè)置了一下頁面樣式准谚。
接下來新增pages
目錄用于放置頁面, 把原本App.vue
的內(nèi)容移到了Hello.vue
:
路由配置我們選擇基于文件進(jìn)行配置去扣,在src
目錄下新建一個/src/router.config.js
:
export default [
{
path: '/',
redirect: '/hello',
},
{
name: 'hello',
path: '/hello/',
component: 'Hello',
}
]
屬性支持vue-router
構(gòu)建選項(xiàng)routes的所有屬性柱衔,component
屬性傳的是pages
目錄下的組件路徑,規(guī)定路由組件只能放到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
使用工廠函數(shù)和import
方法來定義動態(tài)組件秀存,需要遞歸對子路由進(jìn)行處理捶码。最后羽氮,在main.js
里面引入路由:
// main.js
// ...
import router from './router'// ++
// ...
new Vue({
router,// ++
render: h => h(App),
}).$mount('#app')
菜單
我們的業(yè)務(wù)基本上都需要一個菜單或链,默認(rèn)顯示在頁面左側(cè),我們有內(nèi)部的組件庫档押,但沒有對外開源澳盐,所以本文就使用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>
效果如下:
當(dāng)然叼耙,上述只是意思一下,實(shí)際的要復(fù)雜一些粒没,畢竟這里連嵌套菜單的情況都沒考慮筛婉。
權(quán)限
我們的權(quán)限顆粒度比較大,只控制到路由層面癞松,具體實(shí)現(xiàn)就是在菜單配置和路由配置里的每一項(xiàng)都新增一個code
字段爽撒,然后通過請求獲取當(dāng)前用戶有權(quán)限的code
,沒有權(quán)限的菜單默認(rèn)不顯示响蓉,訪問沒有權(quán)限的路由會重定向到403
頁面硕勿。
獲取權(quán)限數(shù)據(jù)
權(quán)限數(shù)據(jù)隨用戶信息接口一起返回,然后存儲到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'] // 用戶擁有的權(quán)限
}
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
里過濾掉沒有權(quán)限的菜單:
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)
})
}
}
}
這樣沒有權(quán)限的菜單就不會顯示出來想幻。
路由
修改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
}
}
})
}
// ...
然后需要攔截路由跳轉(zhuǎn),判斷是否有權(quán)限抄沮,沒有權(quán)限就轉(zhuǎn)到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 : []
// 去錯誤頁面直接跳轉(zhuǎn)即可跋核,否則會引起死循環(huán)
if (/^\/error\//.test(to.path)) {
return next()
}
// 有權(quán)限直接跳轉(zhuǎn)
if (code.includes(to.meta.code)) {
next()
} else if (to.meta.code) { // 路由存在,沒有權(quán)限叛买,跳轉(zhuǎn)到403頁面
next({
path: '/error/403'
})
} else { // 沒有code則代表是非法路徑砂代,跳轉(zhuǎn)到404頁面
next({
path: '/error/404'
})
}
})
error
組件還沒有,新增一下:
// pages/Error.vue
<template>
<div class="container">{{ errorText }}</div>
</template>
<script>
const map = {
403: '無權(quán)限',
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
率挣,增加錯誤頁面的路由刻伊,及增加一個測試無權(quán)限的路由:
// router.config.js
export default [
// ...
{
name: 'Error',
path: '/error/:type',
component: 'Error',
},
{
name: 'hi',
path: '/hi/',
code: '無權(quán)限測試,請輸入hi',
component: 'Hello',
}
]
因?yàn)檫@個code
用戶并沒有,所以現(xiàn)在我們打開/hi
路由會直接跳轉(zhuǎn)到403
路由:
面包屑
和菜單類似捶箱,面包屑也是大部分頁面都需要的智什,面包屑的組成分為兩部分,一部分是在當(dāng)前菜單中的位置丁屎,另一部分是在頁面操作中產(chǎn)生的路徑荠锭。第一部分的路徑因?yàn)榭赡軙討B(tài)的變化,所以一般是通過接口隨用戶信息一起獲取晨川,然后存到vuex
里证九,修改store.js
:
// ...
async getUserInfo(ctx) {
let userInfo = {
code: ['001'],
breadcrumb: {// 增加面包屑數(shù)據(jù)
'001': ['你好'],
},
}
ctx.commit('setUserInfo', userInfo)
}
// ...
第二部分的在router.config.js
里面配置:
export default [
//...
{
name: 'hello',
path: '/hello/',
component: 'Hello',
code: '001',
breadcrumb: ['世界'],// ++
}
]
breadcrumb
字段和code
字段一樣,屬于自定義字段共虑,但是這個字段的數(shù)據(jù)是給組件使用的愧怜,組件需要獲取這個字段的數(shù)據(jù)然后在頁面上渲染出面包屑菜單,所以保存到meta
字段上雖然可以妈拌,但是在組件里面獲取比較麻煩拥坛,所以我們可以設(shè)置到路由記錄的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
屬性即可獲取到面包屑數(shù)據(jù),可以看到把code
也一同傳過去了音诫,這是因?yàn)檫€要根據(jù)當(dāng)前路由的code
從用戶接口獲取的面包屑數(shù)據(jù)中取出該路由code
對應(yīng)的面包屑數(shù)據(jù)惨奕,然后把兩部分的進(jìn)行合并,這個工作為了避免讓每個組件都要做一遍竭钝,我們可以寫在一個全局的mixin
里梨撞,修改main.js
:
// ...
Vue.mixin({
props: {
breadcrumbObj: {
type: Object,
default: () => null
}
},
computed: {
breadcrumb() {
if (!this.breadcrumbObj) {
return []
}
let {
code,
breadcrumb
} = this.breadcrumbObj
// 用戶接口獲取的面包屑數(shù)據(jù)
let breadcrumbData = this.$store.state.userInfo.breadcrumb
// 當(dāng)前路由是否存在面包屑數(shù)據(jù)
let firstBreadcrumb = breadcrumbData && Array.isArray(breadcrumbData[code]) ? breadcrumbData[code] : []
// 合并兩部分的面包屑數(shù)據(jù)
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>
當(dāng)然,我們的面包屑是不需要支持點(diǎn)擊的香罐,如果需要的話可以修改一下面包屑的數(shù)據(jù)結(jié)構(gòu)卧波。
接口請求
接口請求使用的是axios
,但是會做一些基礎(chǔ)配置庇茫、攔截請求和響應(yīng)港粱,因?yàn)檫€是有一些場景需要直接使用未配置的axios
,所以我們默認(rèn)創(chuàng)建一個新實(shí)例旦签,先安裝:
npm install axios
然后新建一個/src/api/
目錄查坪,在里面新增一個httpInstance.js
文件:
import axios from 'axios'
// 創(chuàng)建一個新實(shí)例
const http = axios.create({
timeout: 10000,// 超時時間設(shè)為10秒
withCredentials: true,// 跨域請求時是否需要使用憑證,設(shè)置為需要
headers: {
'X-Requested-With': 'XMLHttpRequest'// 表明是ajax請求
},
})
export default http
然后增加一個請求攔截器:
// ...
// 請求攔截器
http.interceptors.request.use(function (config) {
// 在發(fā)送請求之前做些什么
return config;
}, function (error) {
// 對請求錯誤做些什么
return Promise.reject(error);
});
// ...
其實(shí)啥也沒做宁炫,先寫出來偿曙,留著不同的項(xiàng)目按需修改。
最后增加一個響應(yīng)攔截器:
// ...
import { Message } from 'element-ui'
// ...
// 響應(yīng)攔截器
http.interceptors.response.use(
function (response) {
// 對錯誤進(jìn)行統(tǒng)一處理
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)
},
)
// ...
我們約定一個成功的響應(yīng)(狀態(tài)碼為200)結(jié)構(gòu)如下:
{
code: '0',
msg: 'xxx',
data: xxx
}
code
不為0
即使?fàn)顟B(tài)碼為200
也代表請求出錯罩阵,那么彈出錯誤信息提示框,如果某次請求不希望自動彈出提示框的話也可以禁止启摄,只要在請求時加上配置參數(shù)noMsg: true
即可稿壁,比如:
axios.get('/xxx', {
noMsg: true
})
請求成功默認(rèn)不彈提示,需要的話可以設(shè)置配置參數(shù)successNotify: true
歉备。
狀態(tài)碼在非[200,300)
之間的錯誤只處理兩種傅是,登錄過期和請求超時,其他情況可根據(jù)項(xiàng)目自行修改威创。
多語言
多語言使用vue-i18n實(shí)現(xiàn)落午,先安裝:
npm install vue-i18n@8
vue-i18n
的9.x
版本支持的是Vue3
,所以我們使用8.x
版本肚豺。
然后創(chuàng)建一個目錄/src/i18n/
,在目錄下新建index.js
文件用來創(chuàng)建i18n
實(shí)例:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n()
export default i18n
除了創(chuàng)建實(shí)例其他啥也沒做界拦,別急吸申,接下來我們一步步來。
我們的總體思路是享甸,多語言的源數(shù)據(jù)在/src/i18n/
下截碴,然后編譯成json
文件放到項(xiàng)目的/public/i18n/
目錄下,頁面的初始默認(rèn)語言也是和用戶信息接口一起返回蛉威,頁面根據(jù)默認(rèn)的語言類型使用ajax
請求public
目錄下的對應(yīng)json
文件日丹,調(diào)用VueI18n
的方法動態(tài)進(jìn)行設(shè)置。
這么做的目的首先是方便修改頁面默認(rèn)語言蚯嫌,其次是多語言文件不和項(xiàng)目代碼打包到一起哲虾,減少打包時間栅盲,按需請求该编,減少不必要的資源請求稠氮。
接下來我們新建頁面的中英文數(shù)據(jù)抓韩,目錄結(jié)構(gòu)如下:
比如中文的hello.json
文件內(nèi)容如下(忽略筆者的低水平翻譯~):
在index.js
文件里導(dǎo)入hello.json
文件及ElementUI
的語言文件苍日,并合并導(dǎo)出:
import hello from './hello.json'
import elementLocale from 'element-ui/lib/locale/lang/zh-CN'
export default {
hello,
...elementLocale
}
為什么是...elementLocale
呢笨觅,因?yàn)閭鹘oVue-i18n
的多語言數(shù)據(jù)結(jié)構(gòu)是這樣的:
我們是把index.js
的整個導(dǎo)出對象作為vue-i18n
的多語言數(shù)據(jù)的,而ElementUI
的多語言文件是這樣的:
所以我們需要把這個對象的屬性和hello
屬性合并到一個對象上屋摇。
接下來我們需要把它導(dǎo)出的數(shù)據(jù)到寫到一個json
文件里并輸出到public
目錄下揩魂,這可以直接寫個js
腳本文件來做這個事情,但是為了和項(xiàng)目的源碼分開我們寫成一個npm
包炮温。
創(chuàng)建一個npm工具包
我們在項(xiàng)目的平級下創(chuàng)建一個包目錄火脉,并使用npm init
初始化:
命名為-tool
的原因是后續(xù)可能還會有類似編譯多語言這種需求,所以取一個通用名字柒啤,方便后面增加其他功能倦挂。
命令行交互工具使用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);
因?yàn)槲覀兊陌且鳛槊钚泄ぞ呤褂玫牡9晕募谝恍行枰付_本的解釋程序?yàn)?code>node方援,然后使用commander
配置了一個i18n
命令,用來編譯多語言文件涛癌,后續(xù)如果要添加其他功能新增命令即可犯戏,執(zhí)行文件有了送火,我們還要在包的package.json
文件里添加一個bin
字段,用來指示我們的包里有可執(zhí)行文件先匪,讓npm
在安裝包的時候順便給我們創(chuàng)建一個符號鏈接种吸,把命令映射到文件。
// hello-tool/package.json
{
"bin": {
"hello": "./index.js"
}
}
因?yàn)槲覀兊陌€沒有發(fā)布到npm
呀非,所以直接鏈接到項(xiàng)目上使用坚俗,先在hello-tool
目錄下執(zhí)行:
npm link
然后到我們的hello world
目錄下執(zhí)行:
npm link hello-tool
現(xiàn)在在命令行輸入hello i18n
試試:
編譯多語言文件
接下來完善buildI18n
函數(shù)的邏輯,主要分三步:
1.清空目標(biāo)目錄岸裙,也就是/public/i18n
目錄
2.獲取/src/i18n
下的各種多語言文件導(dǎo)出的數(shù)據(jù)
3.寫入到json
文件并輸出到/public/i18n
目錄下
代碼如下:
const path = require('path')
const fs = require('fs')
// 編譯多語言文件
const buildI18n = () => {
// 多語言源目錄
let srcDir = path.join(process.cwd(), 'src/i18n')
// 目標(biāo)目錄
let destDir = path.join(process.cwd(), 'public/i18n')
// 1.清空目標(biāo)目錄猖败,clearDir是一個自定義方法,遞歸遍歷目錄進(jìn)行刪除
clearDir(destDir)
// 2.獲取源多語言導(dǎo)出數(shù)據(jù)
let data = {}
let langDirs = fs.readdirSync(srcDir)
langDirs.forEach((dir) => {
let dirPath = path.join(srcDir, dir)
// 讀取/src/i18n/xxx/index.js文件降允,獲取導(dǎo)出的多語言對象恩闻,存儲到data對象上
let indexPath = path.join(dirPath, 'index.js')
if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) {
// 使用require加載該文件模塊,獲取導(dǎo)出的數(shù)據(jù)
data[dir] = require(indexPath)
}
})
// 3.寫入到目標(biāo)目錄
Object.keys(data).forEach((lang) => {
// 創(chuàng)建public/i18n目錄
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir)
}
let dirPath = path.join(destDir, lang)
let filePath = path.join(dirPath, 'index.json')
// 創(chuàng)建多語言目錄
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath)
}
// 創(chuàng)建json文件
fs.writeFileSync(filePath, JSON.stringify(data[lang], null, 4))
})
console.log('多語言編譯完成');
}
代碼很簡單拟糕,接下來我們運(yùn)行命令:
報錯了判呕,提示不能在模塊外使用import
,其實(shí)新版本的nodejs
已經(jīng)支持ES6
的模塊語法了送滞,可以把文件后綴換成.mjs
,或者在package.json
文件里增加type=module
字段辱挥,但是都要做很多修改犁嗅,這咋辦呢,有沒有更簡單的方法呢晤碘?把多語言文件換成commonjs
模塊語法褂微?也可以,但是不太優(yōu)雅园爷,不過好在babel
提供了一個@babel/register包宠蚂,可以把babel
綁定到node
的require
模塊上,然后可以在運(yùn)行時進(jìn)行即時編譯童社,也就是當(dāng)require('/src/i18n/xxx/index.js')
時會先由babel
進(jìn)行編譯求厕,編譯完當(dāng)然就不存在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'),
})
// ...
接下來再次運(yùn)行命令:
可以看到編譯完成了扰楼,文件也輸出到了public
目錄下呀癣,但是json
文件里存在一個default
屬性,這一層顯然我們是不需要的弦赖,所以require('i18n/xxx/index.js')
時我們存儲導(dǎo)出的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// ++
}
})
// ...
}
效果如下:
使用多語言文件
首先修改一下用戶接口的返回數(shù)據(jù),增加默認(rèn)語言字段:
// /src/store.js
// ...
async getUserInfo(ctx) {
let userInfo = {
// ...
language: 'zh_CN'// 默認(rèn)語言
}
ctx.commit('setUserInfo', userInfo)
}
// ...
然后在main.js
里面獲取完用戶信息后立刻請求并設(shè)置多語言:
// /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'
// 請求并設(shè)置多語言數(shù)據(jù)
const languageCache = {}
export const setLanguage = async (language = 'zh_CN') => {
let languageData = null
// 有緩存蹬竖,使用緩存數(shù)據(jù)
if (languageCache[language]) {
languageData = languageCache[language]
} else {
// 沒有緩存沼沈,發(fā)起請求
const {
data
} = await axios.get(`/i18n/${language}/index.json`)
languageCache[language] = languageData = data
}
// 設(shè)置語言環(huán)境的 locale 信息
i18n.setLocaleMessage(language, languageData)
// 修改語言環(huán)境
i18n.locale = language
}
然后把各個組件里顯示的信息都換成$t('xxx')
形式流酬,當(dāng)然,菜單和路由都需要做相應(yīng)的修改列另,效果如下:
可以發(fā)現(xiàn)ElementUI
組件的語言并沒有變化芽腾,這是當(dāng)然的,因?yàn)槲覀冞€沒有處理它访递,修改很簡單晦嵌,ElementUI
支持自定義i18n
的處理方法:
// /src/main.js
// ...
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
// ...
通過CLI插件生成初始多語言文件
最后還有一個問題,就是項(xiàng)目初始化時還沒有多語言文件怎么辦拷姿,難道項(xiàng)目創(chuàng)建完還要先手動運(yùn)行命令編譯一下多語言惭载?有幾種解決方法:
1.最終一般會提供一個項(xiàng)目腳手架,所以默認(rèn)的模板里我們就可以直接加上初始的多語言文件响巢;
2.啟動服務(wù)和打包時先編譯一下多語言文件描滔,像這樣:
"scripts": {
"serve": "hello i18n && vue-cli-service serve",
"build": "hello i18n && vue-cli-service build"
}
3.開發(fā)一個Vue CLI
插件來幫我們在項(xiàng)目創(chuàng)建完時自動運(yùn)行一次多語言編譯命令;
接下來簡單實(shí)現(xiàn)一下第三種方式踪古,同樣在項(xiàng)目同級新建一個插件目錄含长,并創(chuàng)建相應(yīng)的文件(注意插件的命名規(guī)范):
根據(jù)插件開發(fā)規(guī)范,index.js
為Service
插件的入口文件伏穆,Service
插件可以修改webpack
配置拘泞,創(chuàng)建新的 vue-cli service
命令或者修改已經(jīng)存在的命令,我們用不上枕扫,我們的邏輯在generator.js
里陪腌,這個文件會在兩個場景被調(diào)用:
1.項(xiàng)目創(chuàng)建期間,CLI
插件被作為項(xiàng)目創(chuàng)建preset
的一部分被安裝時
2.項(xiàng)目創(chuàng)建完成時通過vue add
或vue invoke
單獨(dú)安裝插件時調(diào)用
我們需要的剛好是在項(xiàng)目創(chuàng)建時或安裝該插件時自動幫我們運(yùn)行多語言編譯命令烟瞧,generator.js
需要導(dǎo)出一個函數(shù)诗鸭,內(nèi)容如下:
const {
exec
} = require('child_process');
module.exports = (api) => {
// 為了方便在項(xiàng)目里看到編譯多語言的命令,我們把hello i18n添加到項(xiàng)目的package.json文件里参滴,修改package.json文件可以使用提供的api.extendPackage方法
api.extendPackage({
scripts: {
buildI18n: 'hello i18n'
}
})
// 該鉤子會在文件寫入硬盤后調(diào)用
api.afterInvoke(() => {
// 獲取項(xiàng)目的完整路徑
let targetDir = api.generator.context
// 進(jìn)入項(xiàng)目文件夾强岸,然后運(yùn)行命令
exec(`cd ${targetDir} && npm run buildI18n`, (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log(stdout);
console.error(stderr);
});
})
}
我們在afterInvoke
鉤子里運(yùn)行編譯命令,因?yàn)樘邕\(yùn)行可能依賴都還沒有安裝完成砾赔,另外我們還獲取了項(xiàng)目的完整路徑蝌箍,這是因?yàn)橥ㄟ^preset
配置插件時,插件被調(diào)用時可能不在實(shí)際的項(xiàng)目文件夾过蹂,比如我們在a
文件夾下通過該命令創(chuàng)建b
項(xiàng)目:
vue create b
插件被調(diào)用時是在a
目錄十绑,顯然hello-i18n
包是被安裝在b
目錄,所以我們要先進(jìn)入項(xiàng)目實(shí)際目錄然后運(yùn)行編譯命令酷勺。
接下來測試一下本橙,先在項(xiàng)目下安裝該插件:
npm install --save-dev file:完整路徑\vue-cli-plugin-i18n
然后通過如下命令來調(diào)用插件的生成器:
vue invoke vue-cli-plugin-i18n
效果如下:
可以看到項(xiàng)目的package.json
文件里面已經(jīng)注入了編譯命令,并且命令也自動執(zhí)行生成了多語言文件脆诉。
Mock數(shù)據(jù)
Mock
數(shù)據(jù)推薦使用Mock甚亭,使用很簡單贷币,新建一個mock
數(shù)據(jù)文件:
然后在/api/index.js
里引入:
就這么簡單,該請求即可被攔截:
規(guī)范化
有關(guān)規(guī)范化的配置亏狰,比如代碼風(fēng)格檢查役纹、git
提交規(guī)范等,筆者之前寫過一篇組件庫搭建的文章暇唾,其中一個小節(jié)詳細(xì)的介紹了配置過程促脉,可移步:【萬字長文】從零配置一個vue組件庫-規(guī)范化配置小節(jié)。
其他
請求代理
本地開發(fā)測試接口請求時難免會遇到跨域問題策州,可以配置一下webpack-dev-server
的代理選項(xiàng)瘸味,新建vue.config.js
文件:
module.exports = {
devServer: {
proxy: {
'^/api/': {
target: 'http://xxx:xxx',
changeOrigin: true
}
}
}
}
編譯node_modules內(nèi)的依賴
默認(rèn)情況下babel-loader
會忽略所有node_modules
中的文件,但是有些依賴可能是沒有經(jīng)過編譯的够挂,比如我們自己編寫的一些包為了省事就不編譯了旁仿,那么如果用了最新的語法,在低版本瀏覽器上可能就無法運(yùn)行了孽糖,所以打包的時候也需要對它們進(jìn)行編譯枯冈,要通過Babel
顯式轉(zhuǎn)譯一個依賴,可以在這個transpileDependencies
選項(xiàng)配置办悟,修改vue.config.js
:
module.exports = {
// ...
transpileDependencies: ['your-package-name']
}
環(huán)境變量
需要環(huán)境變量可以在項(xiàng)目根目錄下新建.env
文件尘奏,需要注意的是如果要通過插件渲染.
開頭的模板文件,要用_
來替代點(diǎn)病蛉,也就是_env
罪既,最終會渲染為.
開頭的文件。
腳手架
當(dāng)我們設(shè)計(jì)好了一套項(xiàng)目結(jié)構(gòu)后铡恕,肯定是作為模板來快速創(chuàng)建項(xiàng)目的,一般會創(chuàng)建一個腳手架工具來生成丢间,但是Vue CLI
提供了preset
(預(yù)設(shè))的能力探熔,所謂preset
指的是一個包含創(chuàng)建新項(xiàng)目所需預(yù)定義選項(xiàng)和插件的 JSON
對象,所以我們可以創(chuàng)建一個CLI
插件來創(chuàng)建模板烘挫,然后創(chuàng)建一個preset
,再把這個插件配置到preset
里,這樣使用vue create
命令創(chuàng)建項(xiàng)目時使用我們的自定義preset
即可噪奄。
創(chuàng)建一個生成模板的CLI插件
新建插件目錄如下:
可以看到這次我們創(chuàng)建了一個generator
目錄玷过,因?yàn)槲覀冃枰秩灸0澹0逦募蜁旁谶@個目錄下卤橄,新建一個template
目錄绿满,然后把我們前文配置的項(xiàng)目結(jié)構(gòu)完整的復(fù)制進(jìn)去(不包括package.json):
現(xiàn)在我們來完成/generator/index.js
文件的內(nèi)容:
1.因?yàn)椴话?code>package.json,所以我們要修改vue
項(xiàng)目默認(rèn)的package.json
窟扑,添加我們需要的東西喇颁,使用的就是前面提到的api.extendPackage
方法:
// generator/index.js
module.exports = (api) => {
// 擴(kuò)展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"http:// 注意這里漏健,不要忘記把我們的工具包加上
}
})
}
添加了一些額外的依賴,包括我們前面開發(fā)的hello-tool
橘霎。
2.渲染模板
module.exports = (api) => {
// ...
api.render('./template')
}
render
方法會渲染template
目錄下的所有文件蔫浆。
創(chuàng)建一個自定義preset
插件都有了,最后讓我們來創(chuàng)建一下自定義preset
姐叁,新建一個preset.json
文件瓦盛,把我們前面寫的template
插件和i18n
插件一起配置進(jìn)去:
{
"plugins": {
"vue-cli-plugin-template": {
"version": "^1.0.0"
},
"vue-cli-plugin-i18n": {
"version": "^1.0.0"
}
}
}
同時為了測試這個preset
,我們再創(chuàng)建一個空目錄:
然后進(jìn)入test-preset
目錄運(yùn)行vue create
命令時指定我們的preset
路徑即可:
vue create --preset ../preset.json my-project
效果如下:
遠(yuǎn)程使用preset
preset
本地測試沒問題了就可以上傳到倉庫里外潜,之后就可以給別人使用了原环,比如筆者上傳到了這個倉庫:https://github.com/wanglin2/Vue_project_design,那么你可以這么使用:
vue create --preset wanglin2/Vue_project_design project-name
總結(jié)
如果有哪里不對的或是更好的橡卤,評論區(qū)見~