It is easy to create a Vue project through Vue CLI , but it is not enough for actual projects, so generally we will add some common capabilities on the basis of the business situation, reduce some repetitive operations when creating new projects, in the spirit of learning For the purpose of sharing and sharing, this article will introduce the front-end architecture design of our Vue project. Of course, some places may not be the best way. After all, everyone's business is different, and what suits you is the best.

In addition to introducing the basic architectural design, this article will also describe how to develop a Vue CLI plugin and preset preset.

ps.本文基于Vue2.x版本,node版本16.5.0

Create a basic project

First create a basic project using Vue CLI :

vue create hello-world

Then select Vue2 option to create, the initial project structure is as follows:

image-20220126101820738.png

Next, we will build on this foundation.

routing

Routing is essential, install vue-router :

npm install vue-router

Modify App.vue file:

<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>

Add routing export, simply set the page style.

Next, add pages directory for placing pages, and move the original content of App.vue to Hello.vue :

image-20220126140342614.png

For routing configuration, we choose to configure based on files, and create a new /src/router.config.js in the src directory:

export default [
  {
    path: '/',
    redirect: '/hello',
  },
  {
    name: 'hello',
    path: '/hello/',
    component: 'Hello',
  }
]

The attribute supports all attributes of the vue-router build option routes , and the component attribute transmits the component path 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

Using factory functions and import methods to define dynamic components requires recursive processing of child routes. Finally, import routes in main.js :

// main.js
// ...
import router from './router'// ++
// ...
new Vue({
  router,// ++
  render: h => h(App),
}).$mount('#app')

menu

Our business basically needs a menu, which is displayed on the left side of the page by default. We have an internal component library, but it is not open source, so this article uses Element instead. The menu is also configured through files. Create a new /src/nav.config.js file:

export default [{
    title: 'hello',
    router: '/hello',
    icon: 'el-icon-menu'
}]

Then modify App.vue file:

<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>

The effect is as follows:

image-20220126145352732.png

Of course, the above is just for the purpose, the actual situation is more complicated, after all, the situation of nested menus is not even considered here.

permission

The granularity of our permissions is relatively large, and we only control the routing level. The specific implementation is to add a code field to each item in the menu configuration and routing configuration, and then obtain the code that the current user has permission, and the menu without permission. It is not displayed by default, and access to routes without permission will be redirected to the 403 page.

Get permission data

The permission data is returned with the user information interface, and then stored in vuex , so first configure vuex and install:

npm install vuex --save

Added /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
        }
    },
})

First obtain user information in main.js , and then initialize Vue :

// ...
import store from './store'
// ...
const initApp = async () => {
  await store.dispatch('getUserInfo')
  new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount('#app')
}
initApp()

menu

Modify nav.config.js add code field:

// nav.config.js
export default [{
    title: 'hello',
    router: '/hello',
    icon: 'el-icon-menu'
    code: '001',
}]

Then filter out menus without permission in 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)
      })
    }
  }
}

This way the menu without permission will not be displayed.

routing

Modify router.config.js and add code field:

export default [{
        path: '/',
        redirect: '/hello',
    },
    {
        name: 'hello',
        path: '/hello/',
        component: 'Hello',
        code: '001',
    }
]

code is a custom field and needs to be saved in the meta field of the routing record, otherwise it will be lost in the end. Modify createRoute method:

// 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
            }
        }
    })
}
// ...

Then you need to intercept the route jump to determine whether you have permission. If you don't have permission, go to the 403 page:

// 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 component is not available yet, add it:

// 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>

Next, modify router.config.js , add a route for the error page, and add a route for testing without permission:

// router.config.js

export default [
    // ...
    {
        name: 'Error',
        path: '/error/:type',
        component: 'Error',
    },
    {
        name: 'hi',
        path: '/hi/',
        code: '无权限测试,请输入hi',
        component: 'Hello',
    }
]

Because this code user does not exist, so now we open the /hi route and directly jump to the 403 route:

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

Bread crumbs

Similar to the menu, breadcrumbs are also required for most pages. The breadcrumbs are divided into two parts, one part is the position in the current menu, and the other part is the path generated in the page operation. Because the path of the first part may change dynamically, it is generally obtained with the user information through the interface, and then stored in vuex , and modified store.js :

// ...
async getUserInfo(ctx) {
    let userInfo = {
        code: ['001'],
        breadcrumb: {// 增加面包屑数据
            '001': ['你好'],
        },
    }
    ctx.commit('setUserInfo', userInfo)
}
// ...

The second part is configured in router.config.js :

export default [
    //...
    {
        name: 'hello',
        path: '/hello/',
        component: 'Hello',
        code: '001',
        breadcrumb: ['世界'],// ++
    }
]

breadcrumb field is the same as the code field. It belongs to a custom field, but the data of this field is used by the component. The component needs to obtain the data of this field and then render the breadcrumb menu on the page, so it is possible to save it to the meta field, but It is more troublesome to obtain in the component, so we can set it to the props field of the routing record and directly inject it as props of the component, so that it is much more convenient to use, modify 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
                } 
            }
        }
    })
}
// ...

In this way, by declaring a breadcrumbObj attribute in the component, the breadcrumb data can be obtained. You can see that code is also passed along. This is because the route code code Corresponding breadcrumb data, and then merge the two parts. In order to avoid doing this work for each component, we can write it in a global mixin and modify 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()

Finally, we render the breadcrumbs in the Hello.vue component:

<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

Of course, our breadcrumbs do not need to support clicks. If necessary, you can modify the data structure of the breadcrumbs.

interface request

The interface request uses axios , but will do some basic configuration, interception requests and responses, because there are still some scenarios that need to use the unconfigured axios directly, so we create a new instance by default and install it first:

npm install axios

Then create a new /src/api/ directory, and add a httpInstance.js file in it:

import axios from 'axios'

// 创建一个新实例
const http = axios.create({
    timeout: 10000,// 超时时间设为10秒
    withCredentials: true,// 跨域请求时是否需要使用凭证,设置为需要
    headers: {
        'X-Requested-With': 'XMLHttpRequest'// 表明是ajax请求
    },
})

export default http

Then add a request interceptor:

// ...
// 请求拦截器
http.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
// ...

In fact, I didn't do anything, just write it out first, and keep different projects to modify as needed.

Finally add a response interceptor:

// ...
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)
    },
)
// ...

We agree that a successful response (status code 200) has the following structure:

{
    code: '0',
    msg: 'xxx',
    data: xxx
}

code is not 0 even if the status code is 200 , it means that the request is wrong, then an error message prompt box will pop up. If you don’t want the prompt box to pop up automatically for a request, you can also disable it. Just add the configuration parameter noMsg: true to the request, for example:

axios.get('/xxx', {
    noMsg: true
})

If the request is successful, no prompt will be displayed by default. If necessary, you can set the configuration parameter successNotify: true .

There are only two types of errors with a status code other than [200,300) , login expiration and request timeout, and other situations can be modified according to the project.

multi-language

Multi-language is implemented using vue-i18n , first install:

npm install vue-i18n@8

The vue-i18n version of 9.x supports Vue3 , so we use the 8.x version.

Then create a directory /src/i18n/ , and create a new index.js file in the directory to create an instance of i18n :

import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)
const i18n = new VueI18n()

export default i18n

Don't do anything except create an instance, don't worry, let's go step by step.

Our general idea is that the multi-language source data is under /src/i18n/ , and then compiled into a json file and placed in the /public/i18n/ directory of the project. The initial default language of the page is also returned together with the user information interface, and the page uses the ajax request according to the default language type. The corresponding json file in the public directory is set dynamically by calling the method of VueI18n .

The purpose of this is first to facilitate the modification of the default language of the page, and secondly, the multi-language files are not packaged with the project code, which reduces the packaging time, requests on demand, and reduces unnecessary resource requests.

Next, we create the Chinese and English data of the new page, and the directory structure is as follows:

image-20220211103104133.png

For example, the content of the Chinese hello.json file is as follows (ignoring the author's low-level translation~):

image-20220211103928440.png

Import the index.js file and the ElementUI language file into the hello.json file, and merge and export:

import hello from './hello.json'
import elementLocale from 'element-ui/lib/locale/lang/zh-CN'

export default {
    hello,
    ...elementLocale
}

Why is it ...elementLocale , because the multilingual data structure passed to Vue-i18n is like this:

image-20220211170320562.png

We take the entire export object of index.js as the multilingual data of vue-i18n , and the multilingual file of ElementUI is like this:

image-20220211165917570.png

So we need to combine the properties of this object and the properties of hello into one object.

Next, we need to write the exported data to a json file and output it to the public directory. This can be done directly by writing a js script file, but in order to separate it from the source code of the project, we write a npm package.

Create an npm toolkit

We create a package directory under the project's level and initialize it with npm init :

image.png

The reason for naming it -tool is that there may be similar requirements for compiling multiple languages in the future, so a common name is adopted to facilitate adding other functions later.

The command line interactive tool uses Commander.js , install:

npm install commander

Then create a new entry file index.js :

#!/usr/bin/env node

const {
    program
} = require('commander');

// 编译多语言文件
const buildI18n = () => {
    console.log('编译多语言文件');
}

program
    .command('i18n') // 添加i18n命令
    .action(buildI18n)

program.parse(process.argv);

Because our package is to be used as a command-line tool, the first line of the file needs to specify that the script's interpreter is node , and then use commander configure a i18n command to compile multilingual files. If you want to add other new functions later Just add the command, and the executable file is there. We also need to add a bin field to the package.json file of the package to indicate that there is an executable file in our package, and let npm create a symbolic link for us by the way when installing the package. , which maps commands to files.

// hello-tool/package.json
{
    "bin": {
        "hello": "./index.js"
    }
}

Because our package has not been released to npm , it is directly linked to the project for use, first execute it in the hello-tool directory:

npm link

Then go to our hello world directory and execute:

npm link hello-tool

Now try typing hello i18n on the command line:

image.png

Compile multilingual files

Next, improve the logic of the buildI18n function, which is mainly divided into three steps:

1. Empty the target directory, which is the /public/i18n directory

2. Obtain data exported from various multilingual files under /src/i18n

3. Write to the json file and output to the /public/i18n directory

code show as below:

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('多语言编译完成');
}

The code is very simple, next we run the command:

image.png

An error is reported, indicating that import cannot be used outside the module. In fact, the new version of nodejs already supports the module syntax of ES6 . You can change the file suffix to .mjs , or add the type=module field to the package.json file. Is there an easier way to do it? Change multilingual files to commonjs module syntax? It is also possible, but it is not very elegant, but fortunately, babel provides a @babel/register package, which can bind node to the babel require module, which can then be compiled at runtime, that is, when require('/src/i18n/xxx/index.js') will be 0622f4. Compile by babel first. Of course, there is no import statement after compilation. Install it first:

npm install @babel/core @babel/register @babel/preset-env

Then create a new babel configuration file:

// hello-tool/babel.config.js
module.exports = {
  'presets': ['@babel/preset-env']
}

Finally used in the hello-tool/index.js file:

const path = require('path')
const {
    program
} = require('commander');
const fs = require('fs')
require("@babel/register")({
    configFile: path.resolve(__dirname, './babel.config.js'),
})
// ...

Next run the command again:

image.png

image.png

It can be seen that the compilation is completed, and the file is also output to the public directory, but there is a default attribute in the json file. Obviously we do not need this layer, so when require('i18n/xxx/index.js') , we store the exported default object and modify 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// ++
        }
    })
    // ...
}

The effect is as follows:

image.png

Use multilingual files

First, modify the return data of the user interface and add the default language field:

// /src/store.js
// ...
async getUserInfo(ctx) {
    let userInfo = {
        // ...
        language: 'zh_CN'// 默认语言
    }
    ctx.commit('setUserInfo', userInfo)
}
// ...

Then request and set multi-language immediately after obtaining user information in 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 method requests multilingual files and switches:

// /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
}

Then replace the information displayed in each component with the form of $t('xxx') . Of course, the menu and routing need to be modified accordingly. The effect is as follows:

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

It can be found that the language of the ElementUI component has not changed, of course, because we have not dealt with it yet, the modification is very simple, ElementUI supports the processing method of custom i18n :

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

image-20220212111252574.png

Generate initial polyglot files via CLI plugin

Finally, there is another question, what should I do if there are no multi-language files when the project is initialized? Is it necessary to manually run the command to compile the multi-language after the project is created? There are several workarounds:

1. In the end, a project scaffolding will generally be provided, so we can directly add the initial multilingual file to the default template;

2. Compile the multilingual file first when starting the service and packaging, like this:

"scripts": {
    "serve": "hello i18n && vue-cli-service serve",
    "build": "hello i18n && vue-cli-service build"
  }

3. Develop a Vue CLI plugin to help us automatically run a multi-language compilation command when the project is created;

Next, simply implement the third method, also create a plugin directory at the same level of the project, and create the corresponding file (note the naming convention of plugins):

image.png

According to the plug-in development specification, index.js is the entry file of the Service plug-in. The Service plug-in can modify the webpack configuration, create a new vue-cli service command or modify an existing command. We don’t need it. Our logic is in generator.js . The scene is called:

1. During project creation, when the CLI plugin was installed as part of the project creation preset

2. Called when the plugin is installed separately through vue add or vue invoke when the project is created

What we need is to automatically run the multi-language compilation command for us when the project is created or when the plugin is installed. generator.js needs to export a function, the content is as follows:

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);
        });
    })
}

We run the compile command in the afterInvoke hook, because if it runs too early, the dependencies may not be installed. In addition, we also get the full path of the project. This is because when the plugin is configured through preset , the plugin may not be called in the actual project file. folder, for example, we create the b project under the a folder with this command:

vue create b

When the plugin is called, it is in the a directory. Obviously, the hello-i18n package is installed in the b directory, so we must first enter the actual directory of the project and then run the compile command.

Next test, first install the plugin under the project:

npm install --save-dev file:完整路径\vue-cli-plugin-i18n

Then invoke the plugin's generator with the following command:

vue invoke vue-cli-plugin-i18n

The effect is as follows:

image.png

image.png

It can be seen that the compilation command has been injected into the package.json file of the project, and the command is also automatically executed to generate a multilingual file.

Mock data

It is recommended to use Mock for Mock data. It is very simple to use. Create a new mock data file:

image.png

Then import it in /api/index.js :

image.png

As simple as that, the request can be intercepted:

image-20220212150450209.png

normalize

Regarding standardized configuration, such as code style check, git submission specification, etc., the author has written an article on component library construction before, one of the subsections describes the configuration process in detail, you can move: [10,000-character long article] Configure from zero A vue component library - canonical configuration subsection .

other

request proxy

It is inevitable to encounter cross-domain problems when requesting the local development and testing interface. You can configure the proxy options of webpack-dev-server and create a new vue.config.js file:

module.exports = {
    devServer: {
        proxy: {
            '^/api/': {
                target: 'http://xxx:xxx',
                changeOrigin: true
            }
        }
    }
}

Compile dependencies in node_modules

By default, babel-loader will ignore all files in node_modules , but some dependencies may not be compiled. For example, some packages we wrote ourselves are not compiled in order to save trouble, then if the latest syntax is used, on low-version browsers It may not work, so you also need to compile them when packaging. To explicitly translate a dependency through Babel , you can configure it in this transpileDependencies option and modify vue.config.js :

module.exports = {
    // ...
    transpileDependencies: ['your-package-name']
}

environment variable

If you need environment variables, you can create a new .env file in the project root directory. It should be noted that if you want to render a template file starting with . through a plug-in, use _ to replace the dot, that is, _env , which will eventually be rendered as a file starting with . .

scaffold

When we have designed a project structure, it must be used as a template to quickly create a project. Generally, a scaffolding tool will be created to generate it, but Vue CLI provides the ability of preset (preset). The so-called preset refers to a project that contains The JSON object of the pre-defined options and plugins required for the new project, so we can create a CLI plugin to create a template, then create a preset , and then configure this plugin into preset , so that we use our custom when creating a project using the vue create command preset .

Create a CLI plugin that generates templates

The new plugin directory is as follows:

image-20220212162638048.png

It can be seen that this time we created a generator directory, because we need to render the template, and the template file will be placed in this directory, create a new template directory, and then copy the project structure we configured earlier into it (excluding package .json):

image.png

Now let's complete the contents of the /generator/index.js file:

1. Because package.json is not included, we need to modify the default package.json of the vue project and add what we need, using the api.extendPackage method mentioned earlier:

// 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"// 注意这里,不要忘记把我们的工具包加上
        }
    })
}

Added some additional dependencies, including hello-tool that we developed earlier.

2. Render the template

module.exports = (api) => {
    // ...
    api.render('./template')
}

render method will render all files in the template directory.

Create a custom preset

The plugins are all there. Finally, let's create a custom preset , create a new preset.json file, and configure the template plugin and i18n plugin we wrote earlier:

{
    "plugins": {
        "vue-cli-plugin-template": {
            "version": "^1.0.0"
        },
        "vue-cli-plugin-i18n": {
            "version": "^1.0.0"
        }
    }
}

At the same time, in order to test this preset , we create an empty directory:

image.png

Then enter the test-preset directory and specify our preset path when running the vue create command:

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

The effect is as follows:

image.png

image.png

image.png

Use preset remotely

preset there is no problem with the local test, you can upload it to the warehouse, and then you can use it for others. For example, the author uploaded it to this warehouse: https://github.com/wanglin2/Vue_project_design , then you can use it like this:

vue create --preset wanglin2/Vue_project_design project-name

Summarize

If there is something wrong or better, see you in the comment section~

My blog will be synced to Tencent Cloud + Community, and I invite everyone to join: https://cloud.tencent.com/developer/support-plan?invite_code=cdvx38zb3864


街角小林
886 声望773 粉丝