通過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
總結
如果有哪里不對的或是更好的,評論區見~