通过Vue CLI可以方便的创建一个Vue项目,但是对于实际项目来说还是不够的,所以一般都会根据业务的情况来在其基础上添加一些共性能力,减少创建新项目时的一些重复操作,本着学习和分享的目的,本文会介绍一下我们Vue项目的前端架构设计,当然,有些地方可能不是最好的方式,毕竟大家的业务不尽相同,适合你的就是最好的。

除了介绍基本的架构设计,本文还会介绍如何开发一个Vue CLI插件和preset预设。

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

创建一个基本项目

先使用Vue CLI创建一个基本的项目:

  1. vue create hello-world

然后选择Vue2选项创建,初始项目结构如下:

接下来就在此基础上添砖加瓦。

路由

路由是必不可少的,安装vue-router

  1. npm install vue-router

修改App.vue文件:

  1. <template>
  2. <div id="app">
  3. <router-view />
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: 'App',
  9. }
  10. </script>
  11. <style>
  12. * {
  13. padding: 0;
  14. margin: 0;
  15. border: 0;
  16. outline: none;
  17. }
  18. html,
  19. body {
  20. width: 100%;
  21. height: 100%;
  22. }
  23. </style>
  24. <style scoped>
  25. #app {
  26. width: 100%;
  27. height: 100%;
  28. display: flex;
  29. }
  30. </style>

增加路由出口,简单设置了一下页面样式。

接下来新增pages目录用于放置页面, 把原本App.vue的内容移到了Hello.vue

路由配置我们选择基于文件进行配置,在src目录下新建一个/src/router.config.js

  1. export default [
  2. {
  3. path: '/',
  4. redirect: '/hello',
  5. },
  6. {
  7. name: 'hello',
  8. path: '/hello/',
  9. component: 'Hello',
  10. }
  11. ]

属性支持vue-router构建选项routes的所有属性,component属性传的是pages目录下的组件路径,规定路由组件只能放到pages目录下,然后新建一个/src/router.js文件:

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3. import routes from './router.config.js'
  4. Vue.use(Router)
  5. const createRoute = (routes) => {
  6. if (!routes) {
  7. return []
  8. }
  9. return routes.map((item) => {
  10. return {
  11. ...item,
  12. component: () => {
  13. return import('./pages/' + item.component)
  14. },
  15. children: createRoute(item.children)
  16. }
  17. })
  18. }
  19. const router = new Router({
  20. mode: 'history',
  21. routes: createRoute(routes),
  22. })
  23. export default router

使用工厂函数和import方法来定义动态组件,需要递归对子路由进行处理。最后,在main.js里面引入路由:

  1. // main.js
  2. // ...
  3. import router from './router'// ++
  4. // ...
  5. new Vue({
  6. router,// ++
  7. render: h => h(App),
  8. }).$mount('#app')

菜单

我们的业务基本上都需要一个菜单,默认显示在页面左侧,我们有内部的组件库,但没有对外开源,所以本文就使用Element替代,菜单也通过文件来配置,新建/src/nav.config.js文件:

  1. export default [{
  2. title: 'hello',
  3. router: '/hello',
  4. icon: 'el-icon-menu'
  5. }]

然后修改App.vue文件:

  1. <template>
  2. <div id="app">
  3. <el-menu
  4. style="width: 250px; height: 100%"
  5. :router="true"
  6. :default-active="defaultActive"
  7. >
  8. <el-menu-item
  9. v-for="(item, index) in navList"
  10. :key="index"
  11. :index="item.router"
  12. >
  13. <i :class="item.icon"></i>
  14. <span slot="title">{{ item.title }}</span>
  15. </el-menu-item>
  16. </el-menu>
  17. <router-view />
  18. </div>
  19. </template>
  20. <script>
  21. import navList from './nav.config.js'
  22. export default {
  23. name: 'App',
  24. data() {
  25. return {
  26. navList,
  27. }
  28. },
  29. computed: {
  30. defaultActive() {
  31. let path = this.$route.path
  32. // 检查是否有完全匹配的
  33. let fullMatch = navList.find((item) => {
  34. return item.router === path
  35. })
  36. // 没有则检查是否有部分匹配
  37. if (!fullMatch) {
  38. fullMatch = navList.find((item) => {
  39. return new RegExp('^' + item.router + '/').test(path)
  40. })
  41. }
  42. return fullMatch ? fullMatch.router : ''
  43. },
  44. },
  45. }
  46. </script>

效果如下:

当然,上述只是意思一下,实际的要复杂一些,毕竟这里连嵌套菜单的情况都没考虑。

权限

我们的权限颗粒度比较大,只控制到路由层面,具体实现就是在菜单配置和路由配置里的每一项都新增一个code字段,然后通过请求获取当前用户有权限的code,没有权限的菜单默认不显示,访问没有权限的路由会重定向到403页面。

获取权限数据

权限数据随用户信息接口一起返回,然后存储到vuex里,所以先配置一下vuex,安装:

  1. npm install vuex --save

新增/src/store.js

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. Vue.use(Vuex)
  4. export default new Vuex.Store({
  5. state: {
  6. userInfo: null,
  7. },
  8. actions: {
  9. // 请求用户信息
  10. async getUserInfo(ctx) {
  11. let userInfo = {
  12. // ...
  13. code: ['001'] // 用户拥有的权限
  14. }
  15. ctx.commit('setUserInfo', userInfo)
  16. }
  17. },
  18. mutations: {
  19. setUserInfo(state, userInfo) {
  20. state.userInfo = userInfo
  21. }
  22. },
  23. })

main.js里面先获取用户信息,然后再初始化Vue

  1. // ...
  2. import store from './store'
  3. // ...
  4. const initApp = async () => {
  5. await store.dispatch('getUserInfo')
  6. new Vue({
  7. router,
  8. store,
  9. render: h => h(App),
  10. }).$mount('#app')
  11. }
  12. initApp()

菜单

修改nav.config.js新增code字段:

  1. // nav.config.js
  2. export default [{
  3. title: 'hello',
  4. router: '/hello',
  5. icon: 'el-icon-menu'
  6. code: '001',
  7. }]

然后在App.vue里过滤掉没有权限的菜单:

  1. export default {
  2. name: 'App',
  3. data() {
  4. return {
  5. navList,// --
  6. }
  7. },
  8. computed: {
  9. navList() {// ++
  10. const { userInfo } = this.$store.state
  11. if (!userInfo || !userInfo.code || userInfo.code.length <= 0) return []
  12. return navList.filter((item) => {
  13. return userInfo.code.includes(item.code)
  14. })
  15. }
  16. }
  17. }

这样没有权限的菜单就不会显示出来。

路由

修改router.config.js,增加code字段:

  1. export default [{
  2. path: '/',
  3. redirect: '/hello',
  4. },
  5. {
  6. name: 'hello',
  7. path: '/hello/',
  8. component: 'Hello',
  9. code: '001',
  10. }
  11. ]

code是自定义字段,需要保存到路由记录的meta字段里,否则最后会丢失,修改createRoute方法:

  1. // router.js
  2. // ...
  3. const createRoute = (routes) => {
  4. // ...
  5. return routes.map((item) => {
  6. return {
  7. ...item,
  8. component: () => {
  9. return import('./pages/' + item.component)
  10. },
  11. children: createRoute(item.children),
  12. meta: {// ++
  13. code: item.code
  14. }
  15. }
  16. })
  17. }
  18. // ...

然后需要拦截路由跳转,判断是否有权限,没有权限就转到403页面:

  1. // router.js
  2. // ...
  3. import store from './store'
  4. // ...
  5. router.beforeEach((to, from, next) => {
  6. const userInfo = store.state.userInfo
  7. const code = userInfo && userInfo.code && userInfo.code.length > 0 ? userInfo.code : []
  8. // 去错误页面直接跳转即可,否则会引起死循环
  9. if (/^\/error\//.test(to.path)) {
  10. return next()
  11. }
  12. // 有权限直接跳转
  13. if (code.includes(to.meta.code)) {
  14. next()
  15. } else if (to.meta.code) { // 路由存在,没有权限,跳转到403页面
  16. next({
  17. path: '/error/403'
  18. })
  19. } else { // 没有code则代表是非法路径,跳转到404页面
  20. next({
  21. path: '/error/404'
  22. })
  23. }
  24. })

error组件还没有,新增一下:

  1. // pages/Error.vue
  2. <template>
  3. <div class="container">{{ errorText }}</div>
  4. </template>
  5. <script>
  6. const map = {
  7. 403: '无权限',
  8. 404: '页面不存在',
  9. }
  10. export default {
  11. name: 'Error',
  12. computed: {
  13. errorText() {
  14. return map[this.$route.params.type] || '未知错误'
  15. },
  16. },
  17. }
  18. </script>
  19. <style scoped>
  20. .container {
  21. width: 100%;
  22. height: 100%;
  23. display: flex;
  24. justify-content: center;
  25. align-items: center;
  26. font-size: 50px;
  27. }
  28. </style>

接下来修改一下router.config.js,增加错误页面的路由,及增加一个测试无权限的路由:

  1. // router.config.js
  2. export default [
  3. // ...
  4. {
  5. name: 'Error',
  6. path: '/error/:type',
  7. component: 'Error',
  8. },
  9. {
  10. name: 'hi',
  11. path: '/hi/',
  12. code: '无权限测试,请输入hi',
  13. component: 'Hello',
  14. }
  15. ]

因为这个code用户并没有,所以现在我们打开/hi路由会直接跳转到403路由:

面包屑

和菜单类似,面包屑也是大部分页面都需要的,面包屑的组成分为两部分,一部分是在当前菜单中的位置,另一部分是在页面操作中产生的路径。第一部分的路径因为可能会动态的变化,所以一般是通过接口随用户信息一起获取,然后存到vuex里,修改store.js

  1. // ...
  2. async getUserInfo(ctx) {
  3. let userInfo = {
  4. code: ['001'],
  5. breadcrumb: {// 增加面包屑数据
  6. '001': ['你好'],
  7. },
  8. }
  9. ctx.commit('setUserInfo', userInfo)
  10. }
  11. // ...

第二部分的在router.config.js里面配置:

  1. export default [
  2. //...
  3. {
  4. name: 'hello',
  5. path: '/hello/',
  6. component: 'Hello',
  7. code: '001',
  8. breadcrumb: ['世界'],// ++
  9. }
  10. ]

breadcrumb字段和code字段一样,属于自定义字段,但是这个字段的数据是给组件使用的,组件需要获取这个字段的数据然后在页面上渲染出面包屑菜单,所以保存到meta字段上虽然可以,但是在组件里面获取比较麻烦,所以我们可以设置到路由记录的props字段上,直接注入为组件的props,这样使用就方便多了,修改router.js

  1. // router.js
  2. // ...
  3. const createRoute = (routes) => {
  4. // ...
  5. return routes.map((item) => {
  6. return {
  7. ...item,
  8. component: () => {
  9. return import('./pages/' + item.component)
  10. },
  11. children: createRoute(item.children),
  12. meta: {
  13. code: item.code
  14. },
  15. props: {// ++
  16. breadcrumbObj: {
  17. breadcrumb: item.breadcrumb,
  18. code: item.code
  19. }
  20. }
  21. }
  22. })
  23. }
  24. // ...

这样在组件里声明一个breadcrumbObj属性即可获取到面包屑数据,可以看到把code也一同传过去了,这是因为还要根据当前路由的code从用户接口获取的面包屑数据中取出该路由code对应的面包屑数据,然后把两部分的进行合并,这个工作为了避免让每个组件都要做一遍,我们可以写在一个全局的mixin里,修改main.js

  1. // ...
  2. Vue.mixin({
  3. props: {
  4. breadcrumbObj: {
  5. type: Object,
  6. default: () => null
  7. }
  8. },
  9. computed: {
  10. breadcrumb() {
  11. if (!this.breadcrumbObj) {
  12. return []
  13. }
  14. let {
  15. code,
  16. breadcrumb
  17. } = this.breadcrumbObj
  18. // 用户接口获取的面包屑数据
  19. let breadcrumbData = this.$store.state.userInfo.breadcrumb
  20. // 当前路由是否存在面包屑数据
  21. let firstBreadcrumb = breadcrumbData && Array.isArray(breadcrumbData[code]) ? breadcrumbData[code] : []
  22. // 合并两部分的面包屑数据
  23. return firstBreadcrumb.concat(breadcrumb || [])
  24. }
  25. }
  26. })
  27. // ...
  28. initApp()

最后我们在Hello.vue组件里面渲染一下面包屑:

  1. <template>
  2. <div class="container">
  3. <el-breadcrumb separator="/">
  4. <el-breadcrumb-item v-for="(item, index) in breadcrumb" :key="index">{{item}}</el-breadcrumb-item>
  5. </el-breadcrumb>
  6. // ...
  7. </div>
  8. </template>

当然,我们的面包屑是不需要支持点击的,如果需要的话可以修改一下面包屑的数据结构。

接口请求

接口请求使用的是axios,但是会做一些基础配置、拦截请求和响应,因为还是有一些场景需要直接使用未配置的axios,所以我们默认创建一个新实例,先安装:

  1. npm install axios

然后新建一个/src/api/目录,在里面新增一个httpInstance.js文件:

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

然后增加一个请求拦截器:

  1. // ...
  2. // 请求拦截器
  3. http.interceptors.request.use(function (config) {
  4. // 在发送请求之前做些什么
  5. return config;
  6. }, function (error) {
  7. // 对请求错误做些什么
  8. return Promise.reject(error);
  9. });
  10. // ...

其实啥也没做,先写出来,留着不同的项目按需修改。

最后增加一个响应拦截器:

  1. // ...
  2. import { Message } from 'element-ui'
  3. // ...
  4. // 响应拦截器
  5. http.interceptors.response.use(
  6. function (response) {
  7. // 对错误进行统一处理
  8. if (response.data.code !== '0') {
  9. // 弹出错误提示
  10. if (!response.config.noMsg && response.data.msg) {
  11. Message.error(response.data.msg)
  12. }
  13. return Promise.reject(response)
  14. } else if (response.data.code === '0' && response.config.successNotify && response.data.msg) {
  15. // 弹出成功提示
  16. Message.success(response.data.msg)
  17. }
  18. return Promise.resolve({
  19. code: response.data.code,
  20. msg: response.data.msg,
  21. data: response.data.data,
  22. })
  23. },
  24. function (error) {
  25. // 登录过期
  26. if (error.status === 403) {
  27. location.reload()
  28. return
  29. }
  30. // 超时提示
  31. if (error.message.indexOf('timeout') > -1) {
  32. Message.error('请求超时,请重试!')
  33. }
  34. return Promise.reject(error)
  35. },
  36. )
  37. // ...

我们约定一个成功的响应(状态码为200)结构如下:

  1. {
  2. code: '0',
  3. msg: 'xxx',
  4. data: xxx
  5. }

code不为0即使状态码为200也代表请求出错,那么弹出错误信息提示框,如果某次请求不希望自动弹出提示框的话也可以禁止,只要在请求时加上配置参数noMsg: true即可,比如:

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

请求成功默认不弹提示,需要的话可以设置配置参数successNotify: true

状态码在非[200,300)之间的错误只处理两种,登录过期和请求超时,其他情况可根据项目自行修改。

多语言

多语言使用vue-i18n实现,先安装:

  1. npm install vue-i18n@8

vue-i18n9.x版本支持的是Vue3,所以我们使用8.x版本。

然后创建一个目录/src/i18n/,在目录下新建index.js文件用来创建i18n实例:

  1. import Vue from 'vue'
  2. import VueI18n from 'vue-i18n'
  3. Vue.use(VueI18n)
  4. const i18n = new VueI18n()
  5. export default i18n

除了创建实例其他啥也没做,别急,接下来我们一步步来。

我们的总体思路是,多语言的源数据在/src/i18n/下,然后编译成json文件放到项目的/public/i18n/目录下,页面的初始默认语言也是和用户信息接口一起返回,页面根据默认的语言类型使用ajax请求public目录下的对应json文件,调用VueI18n的方法动态进行设置。

这么做的目的首先是方便修改页面默认语言,其次是多语言文件不和项目代码打包到一起,减少打包时间,按需请求,减少不必要的资源请求。

接下来我们新建页面的中英文数据,目录结构如下:

比如中文的hello.json文件内容如下(忽略笔者的低水平翻译~):

index.js文件里导入hello.json文件及ElementUI的语言文件,并合并导出:

  1. import hello from './hello.json'
  2. import elementLocale from 'element-ui/lib/locale/lang/zh-CN'
  3. export default {
  4. hello,
  5. ...elementLocale
  6. }

为什么是...elementLocale呢,因为传给Vue-i18n的多语言数据结构是这样的:

我们是把index.js的整个导出对象作为vue-i18n的多语言数据的,而ElementUI的多语言文件是这样的:

所以我们需要把这个对象的属性和hello属性合并到一个对象上。

接下来我们需要把它导出的数据到写到一个json文件里并输出到public目录下,这可以直接写个js脚本文件来做这个事情,但是为了和项目的源码分开我们写成一个npm包。

创建一个npm工具包

我们在项目的平级下创建一个包目录,并使用npm init初始化:

命名为-tool的原因是后续可能还会有类似编译多语言这种需求,所以取一个通用名字,方便后面增加其他功能。

命令行交互工具使用Commander.js,安装:

  1. npm install commander

然后新建入口文件index.js

  1. #!/usr/bin/env node
  2. const {
  3. program
  4. } = require('commander');
  5. // 编译多语言文件
  6. const buildI18n = () => {
  7. console.log('编译多语言文件');
  8. }
  9. program
  10. .command('i18n') // 添加i18n命令
  11. .action(buildI18n)
  12. program.parse(process.argv);

因为我们的包是要作为命令行工具使用的,所以文件第一行需要指定脚本的解释程序为node,然后使用commander配置了一个i18n命令,用来编译多语言文件,后续如果要添加其他功能新增命令即可,执行文件有了,我们还要在包的package.json文件里添加一个bin字段,用来指示我们的包里有可执行文件,让npm在安装包的时候顺便给我们创建一个符号链接,把命令映射到文件。

  1. // hello-tool/package.json
  2. {
  3. "bin": {
  4. "hello": "./index.js"
  5. }
  6. }

因为我们的包还没有发布到npm,所以直接链接到项目上使用,先在hello-tool目录下执行:

  1. npm link

然后到我们的hello world目录下执行:

  1. npm link hello-tool

现在在命令行输入hello i18n试试:

编译多语言文件

接下来完善buildI18n函数的逻辑,主要分三步:

1.清空目标目录,也就是/public/i18n目录

2.获取/src/i18n下的各种多语言文件导出的数据

3.写入到json文件并输出到/public/i18n目录下

代码如下:

  1. const path = require('path')
  2. const fs = require('fs')
  3. // 编译多语言文件
  4. const buildI18n = () => {
  5. // 多语言源目录
  6. let srcDir = path.join(process.cwd(), 'src/i18n')
  7. // 目标目录
  8. let destDir = path.join(process.cwd(), 'public/i18n')
  9. // 1.清空目标目录,clearDir是一个自定义方法,递归遍历目录进行删除
  10. clearDir(destDir)
  11. // 2.获取源多语言导出数据
  12. let data = {}
  13. let langDirs = fs.readdirSync(srcDir)
  14. langDirs.forEach((dir) => {
  15. let dirPath = path.join(srcDir, dir)
  16. // 读取/src/i18n/xxx/index.js文件,获取导出的多语言对象,存储到data对象上
  17. let indexPath = path.join(dirPath, 'index.js')
  18. if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) {
  19. // 使用require加载该文件模块,获取导出的数据
  20. data[dir] = require(indexPath)
  21. }
  22. })
  23. // 3.写入到目标目录
  24. Object.keys(data).forEach((lang) => {
  25. // 创建public/i18n目录
  26. if (!fs.existsSync(destDir)) {
  27. fs.mkdirSync(destDir)
  28. }
  29. let dirPath = path.join(destDir, lang)
  30. let filePath = path.join(dirPath, 'index.json')
  31. // 创建多语言目录
  32. if (!fs.existsSync(dirPath)) {
  33. fs.mkdirSync(dirPath)
  34. }
  35. // 创建json文件
  36. fs.writeFileSync(filePath, JSON.stringify(data[lang], null, 4))
  37. })
  38. console.log('多语言编译完成');
  39. }

代码很简单,接下来我们运行命令:

报错了,提示不能在模块外使用import,其实新版本的nodejs已经支持ES6的模块语法了,可以把文件后缀换成.mjs,或者在package.json文件里增加type=module字段,但是都要做很多修改,这咋办呢,有没有更简单的方法呢?把多语言文件换成commonjs模块语法?也可以,但是不太优雅,不过好在babel提供了一个@babel/register包,可以把babel绑定到noderequire模块上,然后可以在运行时进行即时编译,也就是当require('/src/i18n/xxx/index.js')时会先由babel进行编译,编译完当然就不存在import语句了,先安装:

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

然后新建一个babel配置文件:

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

最后在hello-tool/index.js文件里使用:

  1. const path = require('path')
  2. const {
  3. program
  4. } = require('commander');
  5. const fs = require('fs')
  6. require("@babel/register")({
  7. configFile: path.resolve(__dirname, './babel.config.js'),
  8. })
  9. // ...

接下来再次运行命令:

可以看到编译完成了,文件也输出到了public目录下,但是json文件里存在一个default属性,这一层显然我们是不需要的,所以require('i18n/xxx/index.js')时我们存储导出的default对象即可,修改hello-tool/index.js

  1. const buildI18n = () => {
  2. // ...
  3. langDirs.forEach((dir) => {
  4. let dirPath = path.join(srcDir, dir)
  5. let indexPath = path.join(dirPath, 'index.js')
  6. if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) {
  7. data[dir] = require(indexPath).default// ++
  8. }
  9. })
  10. // ...
  11. }

效果如下:

使用多语言文件

首先修改一下用户接口的返回数据,增加默认语言字段:

  1. // /src/store.js
  2. // ...
  3. async getUserInfo(ctx) {
  4. let userInfo = {
  5. // ...
  6. language: 'zh_CN'// 默认语言
  7. }
  8. ctx.commit('setUserInfo', userInfo)
  9. }
  10. // ...

然后在main.js里面获取完用户信息后立刻请求并设置多语言:

  1. // /src/main.js
  2. import { setLanguage } from './utils'// ++
  3. import i18n from './i18n'// ++
  4. const initApp = async () => {
  5. await store.dispatch('getUserInfo')
  6. await setLanguage(store.state.userInfo.language)// ++
  7. new Vue({
  8. i18n,// ++
  9. router,
  10. store,
  11. render: h => h(App),
  12. }).$mount('#app')
  13. }

setLanguage方法会请求多语言文件并切换:

  1. // /src/utils/index.js
  2. import axios from 'axios'
  3. import i18n from '../i18n'
  4. // 请求并设置多语言数据
  5. const languageCache = {}
  6. export const setLanguage = async (language = 'zh_CN') => {
  7. let languageData = null
  8. // 有缓存,使用缓存数据
  9. if (languageCache[language]) {
  10. languageData = languageCache[language]
  11. } else {
  12. // 没有缓存,发起请求
  13. const {
  14. data
  15. } = await axios.get(`/i18n/${language}/index.json`)
  16. languageCache[language] = languageData = data
  17. }
  18. // 设置语言环境的 locale 信息
  19. i18n.setLocaleMessage(language, languageData)
  20. // 修改语言环境
  21. i18n.locale = language
  22. }

然后把各个组件里显示的信息都换成$t('xxx')形式,当然,菜单和路由都需要做相应的修改,效果如下:

可以发现ElementUI组件的语言并没有变化,这是当然的,因为我们还没有处理它,修改很简单,ElementUI支持自定义i18n的处理方法:

  1. // /src/main.js
  2. // ...
  3. Vue.use(ElementUI, {
  4. i18n: (key, value) => i18n.t(key, value)
  5. })
  6. // ...

通过CLI插件生成初始多语言文件

最后还有一个问题,就是项目初始化时还没有多语言文件怎么办,难道项目创建完还要先手动运行命令编译一下多语言?有几种解决方法:

1.最终一般会提供一个项目脚手架,所以默认的模板里我们就可以直接加上初始的多语言文件;

2.启动服务和打包时先编译一下多语言文件,像这样:

  1. "scripts": {
  2. "serve": "hello i18n && vue-cli-service serve",
  3. "build": "hello i18n && vue-cli-service build"
  4. }

3.开发一个Vue CLI插件来帮我们在项目创建完时自动运行一次多语言编译命令;

接下来简单实现一下第三种方式,同样在项目同级新建一个插件目录,并创建相应的文件(注意插件的命名规范):

根据插件开发规范,index.jsService插件的入口文件,Service插件可以修改webpack配置,创建新的 vue-cli service命令或者修改已经存在的命令,我们用不上,我们的逻辑在generator.js里,这个文件会在两个场景被调用:

1.项目创建期间,CLI插件被作为项目创建preset的一部分被安装时

2.项目创建完成时通过vue addvue invoke单独安装插件时调用

我们需要的刚好是在项目创建时或安装该插件时自动帮我们运行多语言编译命令,generator.js需要导出一个函数,内容如下:

  1. const {
  2. exec
  3. } = require('child_process');
  4. module.exports = (api) => {
  5. // 为了方便在项目里看到编译多语言的命令,我们把hello i18n添加到项目的package.json文件里,修改package.json文件可以使用提供的api.extendPackage方法
  6. api.extendPackage({
  7. scripts: {
  8. buildI18n: 'hello i18n'
  9. }
  10. })
  11. // 该钩子会在文件写入硬盘后调用
  12. api.afterInvoke(() => {
  13. // 获取项目的完整路径
  14. let targetDir = api.generator.context
  15. // 进入项目文件夹,然后运行命令
  16. exec(`cd ${targetDir} && npm run buildI18n`, (error, stdout, stderr) => {
  17. if (error) {
  18. console.error(error);
  19. return;
  20. }
  21. console.log(stdout);
  22. console.error(stderr);
  23. });
  24. })
  25. }

我们在afterInvoke钩子里运行编译命令,因为太早运行可能依赖都还没有安装完成,另外我们还获取了项目的完整路径,这是因为通过preset配置插件时,插件被调用时可能不在实际的项目文件夹,比如我们在a文件夹下通过该命令创建b项目:

  1. vue create b

插件被调用时是在a目录,显然hello-i18n包是被安装在b目录,所以我们要先进入项目实际目录然后运行编译命令。

接下来测试一下,先在项目下安装该插件:

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

然后通过如下命令来调用插件的生成器:

  1. vue invoke vue-cli-plugin-i18n

效果如下:

可以看到项目的package.json文件里面已经注入了编译命令,并且命令也自动执行生成了多语言文件。

Mock数据

Mock数据推荐使用Mock,使用很简单,新建一个mock数据文件:

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

就这么简单,该请求即可被拦截:

规范化

有关规范化的配置,比如代码风格检查、git提交规范等,笔者之前写过一篇组件库搭建的文章,其中一个小节详细的介绍了配置过程,可移步:【万字长文】从零配置一个vue组件库-规范化配置小节

其他

请求代理

本地开发测试接口请求时难免会遇到跨域问题,可以配置一下webpack-dev-server的代理选项,新建vue.config.js文件:

  1. module.exports = {
  2. devServer: {
  3. proxy: {
  4. '^/api/': {
  5. target: 'http://xxx:xxx',
  6. changeOrigin: true
  7. }
  8. }
  9. }
  10. }

编译node_modules内的依赖

默认情况下babel-loader会忽略所有node_modules中的文件,但是有些依赖可能是没有经过编译的,比如我们自己编写的一些包为了省事就不编译了,那么如果用了最新的语法,在低版本浏览器上可能就无法运行了,所以打包的时候也需要对它们进行编译,要通过Babel显式转译一个依赖,可以在这个transpileDependencies选项配置,修改vue.config.js

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

环境变量

需要环境变量可以在项目根目录下新建.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方法:

  1. // generator/index.js
  2. module.exports = (api) => {
  3. // 扩展package.json
  4. api.extendPackage({
  5. "dependencies": {
  6. "axios": "^0.25.0",
  7. "element-ui": "^2.15.6",
  8. "vue-i18n": "^8.27.0",
  9. "vue-router": "^3.5.3",
  10. "vuex": "^3.6.2"
  11. },
  12. "devDependencies": {
  13. "mockjs": "^1.1.0",
  14. "sass": "^1.49.7",
  15. "sass-loader": "^8.0.2",
  16. "hello-tool": "^1.0.0"// 注意这里,不要忘记把我们的工具包加上
  17. }
  18. })
  19. }

添加了一些额外的依赖,包括我们前面开发的hello-tool

2.渲染模板

  1. module.exports = (api) => {
  2. // ...
  3. api.render('./template')
  4. }

render方法会渲染template目录下的所有文件。

创建一个自定义preset

插件都有了,最后让我们来创建一下自定义preset,新建一个preset.json文件,把我们前面写的template插件和i18n插件一起配置进去:

  1. {
  2. "plugins": {
  3. "vue-cli-plugin-template": {
  4. "version": "^1.0.0"
  5. },
  6. "vue-cli-plugin-i18n": {
  7. "version": "^1.0.0"
  8. }
  9. }
  10. }

同时为了测试这个preset,我们再创建一个空目录:

然后进入test-preset目录运行vue create命令时指定我们的preset路径即可:

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

效果如下:

远程使用preset

preset本地测试没问题了就可以上传到仓库里,之后就可以给别人使用了,比如笔者上传到了这个仓库:https://github.com/wanglin2/Vue_project_design,那么你可以这么使用:

  1. vue create --preset wanglin2/Vue_project_design project-name

总结

如果有哪里不对的或是更好的,评论区见~

基于Vue2.x的前端架构,我们是这么做的的更多相关文章

  1. 【react】使用 create-react-app 构建基于TypeScript的React前端架构----上

    写在前面 一直在探寻,那优雅的美:一直在探寻,那精湛的技巧:一直在探寻,那简单又直白,优雅而美丽的代码. ------ 但是在JavaScript的动态类型.有时尴尬的自动类型转换,以及 “0 == ...

  2. 基于vue2.0前端组件库element中 el-form表单 自定义验证填坑

    eleme写的基于vue2.0的前端组件库: http://element.eleme.io 我在平时使用过程中,遇到的问题. 自定义表单验证出坑: 1: validate/resetFields 未 ...

  3. 基于AngularJS的企业软件前端架构[转载]

    这篇是我参加QCon北京2014的演讲内容: 提纲: 企业应用在软件行业中占有很大的比重,而这类软件多数现在也都采用B/S的模式开发,在这个日新月异的时代,它们的前端开发技术找到了什么改进点呢? B/ ...

  4. 前端调用后端的方法(基于restful接口的mvc架构)

    1.前端调用后台: 建议用你熟悉的一门服务端程序,例如ASP,PHP,JSP,C#这些都可以,然后把需要的数据从数据库中获得,回传给客户端浏览器(其实一般就是写到HTML中,或者生成XML文件)然后在 ...

  5. 基于React的PC网站前端架构分析

    代码地址如下:http://www.demodashi.com/demo/12252.html 本文适合对象 有过一定开发经验的初级前端工程师: 有过完整项目的开发经验,不论大小: 对node有所了解 ...

  6. 基于vue2.0的一个系统

    前言 这是一个用vue做的单页面管理系统,这里只是介绍架子搭建思路 前端架构 沿用Vue全家桶系列开发,主要技术栈:vue2.x+vue-router+vuex+element-ui1.x+axios ...

  7. 基于 iframe 的微前端框架 —— 擎天

    vivo 互联网前端团队- Jiang Zuohan 一.背景 VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求.任务.缺陷等应用,使用敏捷迭代 ...

  8. 用“MEAN”技术栈开发web应用(一)AngularJs前端架构

    前言 不知何时突然冒出“MEAN技术栈”这个新词,听起来很牛逼的样子,其实就是我们已经熟悉了的近两年在前端比较流行的技术,mongodb.express.angularjs.nodejs,由于这几项技 ...

  9. Turtle Online:致力于打造超接地气的PC前端架构,组件+API,快速搭建前端开发

    架构创作初衷 每当新开一个项目时,都会绞尽脑汁去考虑采用哪种框架:requirejs/seajs.jquery/zepto.backbone.easeUI/Bootstrap/AngularJS……, ...

随机推荐

  1. ArcGIS使用技巧(一)——数据存储

    新手,若有错误还请指正! 日常接触ArcGIS较多,发现好多人虽然也在用ArcGIS,但一些基础的小技巧并不知道,写下来希望对大家有所帮助. ArcGIS默认的存储数据库是在C盘(图1),不修改存储数 ...

  2. jdk1.8中hashmap的扩容resize

    当hashmap第一次插入元素.元素个数达到容量阀值threshold时,都会扩容resize(),源码: (假设hashmap扩容前的node数组为旧横向node数组,扩容后的node数组为新横向n ...

  3. EF Core 的 Code First 模式

    0 前言 本文正文第一节,会对 Code First 进行基本的介绍,以及对相关名词进行说明,读者一开始可以不用在这里消耗过多时间,可以先操作一遍例子,再回过头理解. 第二节,以一个简单的例子,展示 ...

  4. netty系列之:netty中的自动解码器ReplayingDecoder

    目录 简介 ByteToMessageDecoder可能遇到的问题 ReplayingDecoder的实现原理 总结 简介 netty提供了一个从ByteBuf到用户自定义的message的解码器叫做 ...

  5. ucore lab6 调度管理机制 学习笔记

    这节虽叫调度管理机制,整篇下来主要就讲了几个调度算法.兴许是考虑到LAB5难,LAB6就仁慈了一把,难度大跳水.平常讲两节原理做一个实验,这次就上了一节原理.权当大战后的小憩吧. schedule函数 ...

  6. iptables系列教程(三)| iptables 实战篇

    一个执着于技术的公众号 实战1 服务器禁止ping iptables -A INPUT -p icmp --icmp-type 8 -s 0/0 -j DROP // 禁止任何人ping通本机 &qu ...

  7. Nginx代理websocket为什么要这样做?

    Nginx反向代理websocket 示例: http { map $http_upgrade $connection_upgrade { default upgrade; '' close; } s ...

  8. range内部代码

    def my_range(a, b=None, c=1): if not b: b = a a = 0 while a < b: yield a a += c

  9. 设计模式存在哪些关联关系,六种关系傻傻分不清--- UML图示详解

    前言 UML俗称统一建模语言.我们可以简单理解成他是一套符号语言.不同的符号对应不同的含义.在之前设计模式章节中我们文章中用到的就是UML类图,UML除了类图意外还有用例图,活动图,时序图. 关于UM ...

  10. 好客租房33-事件绑定this指向(总结)

    1推荐使用class的实例方法 //导入react import React from 'react'   import ReactDOM from 'react-dom' //导入组件   // 约 ...