基于Vue2.x的前端架構(gòu)驼唱,我們是這么做的

通過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)如下:

image-20220126101820738.png

接下來就在此基礎(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

image-20220126140342614.png

路由配置我們選擇基于文件進(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>

效果如下:

image-20220126145352732.png

當(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路由:

2022-02-10-14-01-59.gif

面包屑

和菜單類似捶箱,面包屑也是大部分頁面都需要的智什,面包屑的組成分為兩部分,一部分是在當(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>
image-20220210152155551.png

當(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-i18n9.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)如下:

image-20220211103104133.png

比如中文的hello.json文件內(nèi)容如下(忽略筆者的低水平翻譯~):

image-20220211103928440.png

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)是這樣的:

image-20220211170320562.png

我們是把index.js的整個導(dǎo)出對象作為vue-i18n的多語言數(shù)據(jù)的,而ElementUI的多語言文件是這樣的:

image-20220211165917570.png

所以我們需要把這個對象的屬性和hello屬性合并到一個對象上屋摇。

接下來我們需要把它導(dǎo)出的數(shù)據(jù)到寫到一個json文件里并輸出到public目錄下揩魂,這可以直接寫個js腳本文件來做這個事情,但是為了和項(xiàng)目的源碼分開我們寫成一個npm包炮温。

創(chuàng)建一個npm工具包

我們在項(xiàng)目的平級下創(chuàng)建一個包目錄火脉,并使用npm init初始化:

image.png

命名為-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試試:

image.png

編譯多語言文件

接下來完善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)行命令:

image.png

報錯了判呕,提示不能在模塊外使用import,其實(shí)新版本的nodejs已經(jīng)支持ES6的模塊語法了送滞,可以把文件后綴換成.mjs,或者在package.json文件里增加type=module字段辱挥,但是都要做很多修改犁嗅,這咋辦呢,有沒有更簡單的方法呢晤碘?把多語言文件換成commonjs模塊語法褂微?也可以,但是不太優(yōu)雅园爷,不過好在babel提供了一個@babel/register包宠蚂,可以把babel綁定到noderequire模塊上,然后可以在運(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)行命令:

image.png
image.png

可以看到編譯完成了扰楼,文件也輸出到了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// ++
        }
    })
    // ...
}

效果如下:

image.png

使用多語言文件

首先修改一下用戶接口的返回數(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)的修改列另,效果如下:

2022-02-12-11-01-36.gif

可以發(fā)現(xiàn)ElementUI組件的語言并沒有變化芽腾,這是當(dāng)然的,因?yàn)槲覀冞€沒有處理它访递,修改很簡單晦嵌,ElementUI支持自定義i18n的處理方法:

// /src/main.js
// ...
Vue.use(ElementUI, {
  i18n: (key, value) => i18n.t(key, value)
})
// ...
image-20220212111252574.png

通過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ī)范):

image.png

根據(jù)插件開發(fā)規(guī)范,index.jsService插件的入口文件伏穆,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 addvue 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

效果如下:

image.png
image.png

可以看到項(xiàng)目的package.json文件里面已經(jīng)注入了編譯命令,并且命令也自動執(zhí)行生成了多語言文件脆诉。

Mock數(shù)據(jù)

Mock數(shù)據(jù)推薦使用Mock甚亭,使用很簡單贷币,新建一個mock數(shù)據(jù)文件:

image.png

然后在/api/index.js里引入:

image.png

就這么簡單,該請求即可被攔截:

image-20220212150450209.png

規(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插件

新建插件目錄如下:

image-20220212162638048.png

可以看到這次我們創(chuàng)建了一個generator目錄玷过,因?yàn)槲覀冃枰秩灸0澹0逦募蜁旁谶@個目錄下卤橄,新建一個template目錄绿满,然后把我們前文配置的項(xiàng)目結(jié)構(gòu)完整的復(fù)制進(jìn)去(不包括package.json):

image.png

現(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)建一個空目錄:

image.png

然后進(jìn)入test-preset目錄運(yùn)行vue create命令時指定我們的preset路徑即可:

vue create --preset ../preset.json my-project

效果如下:

image.png
image.png
image.png

遠(yuǎn)程使用preset

preset本地測試沒問題了就可以上傳到倉庫里外潜,之后就可以給別人使用了原环,比如筆者上傳到了這個倉庫:https://github.com/wanglin2/Vue_project_design,那么你可以這么使用:

vue create --preset wanglin2/Vue_project_design project-name

總結(jié)

如果有哪里不對的或是更好的橡卤,評論區(qū)見~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扮念,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子碧库,更是在濱河造成了極大的恐慌柜与,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嵌灰,死亡現(xiàn)場離奇詭異弄匕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沽瞭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門迁匠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驹溃,你說我怎么就攤上這事城丧。” “怎么了豌鹤?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵亡哄,是天一觀的道長。 經(jīng)常有香客問我布疙,道長蚊惯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任灵临,我火速辦了婚禮截型,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘儒溉。我一直安慰自己宦焦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赶诊,像睡著了一般笼平。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舔痪,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天寓调,我揣著相機(jī)與錄音,去河邊找鬼锄码。 笑死夺英,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的滋捶。 我是一名探鬼主播痛悯,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼重窟!你這毒婦竟也來了载萌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巡扇,失蹤者是張志新(化名)和其女友劉穎扭仁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厅翔,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乖坠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了刀闷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片熊泵。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖甸昏,靈堂內(nèi)的尸體忽然破棺而出顽分,到底是詐尸還是另有隱情,我是刑警寧澤施蜜,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布怯邪,位于F島的核電站,受9級特大地震影響花墩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澄步,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一冰蘑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧村缸,春花似錦祠肥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽县恕。三九已至,卻和暖如春剂桥,著一層夾襖步出監(jiān)牢的瞬間忠烛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工权逗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留美尸,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓斟薇,卻偏偏與公主長得像师坎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子堪滨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容