Skip to content
On this page

后台管理系统相关整理

布局 Layout

除部分 login、404、401 等页面,其他页面都基于 layout,内容在 app-mian 中显示

sidebar、navbar(breadcrumb、tabs-view)、app-main

transform

页面之间切换动画

keep-alive

(缓存 <router-view> 的,配合页面的 tabs-view 标签导航使用,如不需要可自行去除。)

router-view

比如创建和编辑页面,使用同一组件,默认情况下两个页面切换时并不会触发 vuecreatedmounted 钩子。

  • 可以通过 watch $route 的变化来进行处理

  • router-view上加上唯一的 key,来保证路由切换时都会重新渲染触发钩子

    js
    <router-view :key="key"></router-view>
    
    // 只要保证 key 唯一性就可以了,保证不同页面的 key 不相同 computed: {
      key() {
        return this.$route.fullPath }
    },
    
  • 声明两个不同的 view 但引入用一个组件

    vue
    <!-- created.vue -->
    <template>
      <article-detail is-edit="false" />
    </template>
    <script>
    import ArticleDetayl from './components/ArticleDetail'
    </script>
    
    <!-- edit.vue -->
    <template>
      <article-detail is-edit="true" />
    </template>
    <script>
    import ArticleDetayl from './components/ArticleDetail'
    </script>
    

路由和侧边栏

侧边栏和路由绑定在一起,在 router 中配置好路由,侧边栏会自动生成。

配置项

js
// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)

//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'

// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true

name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
  roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
  title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
  icon: 'svg-name' // 设置该路由的图标
  noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
  breadcrumb: false //  如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
  affix: true // 若果设置为true,它则会固定在tags-view中(默认 false)

  // 当路由设置了该属性,则会高亮相对应的侧边栏。
  // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list
  // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置
  activeMenu: '/article/list'
}

示例:

js
{
  path: '/permission',
  component: Layout,
  redirect: '/permission/index', //重定向地址,在面包屑中点击会重定向去的地址
  hidden: true, // 不在侧边栏线上
  alwaysShow: true, //一直显示根路由
  meta: { roles: ['admin','editor'] }, //你可以在根路由设置权限,这样它下面所以的子路由都继承了这个权限
  children: [{
    path: 'index',
    component: ()=>import('permission/index'),
    name: 'permission',
    meta: {
      title: 'permission',
      icon: 'lock', //图标
      roles: ['admin','editor'], //或者你可以给每一个子路由设置自己的权限
      noCache: true // 不会被 <keep-alive> 缓存
    }
  }]
}

#路由

路由

路由分为两种,constantRoutesasyncRoutes

constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404、等通用页面

asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。

注意点: 如果这里有一个需要非常注意的地方就是 404 页面一定要最后加载,如果放在 constantRoutes 一同声明了 404 ,后面的所以页面都会被拦截到 404

侧边栏

使用递归组件构建,(UI 元素:el-menu、submenu、el-menu-item)

当你一个路由下面的 children 声明的路由大于>1 个时,自动会变成嵌套的模式。如果子路由正好等于一个就会默认将子路由作为根路由显示在侧边栏中,若不想这样,可以通过设置在根路由中设置 alwaysShow: true 来取消这一特性。

unique-opened 你可以在 Sidebar/index.vue 中设置 unique-opened 来控制侧边栏,是否只保持一个子菜单的展开。

多级目录(嵌套路由)

如果你的路由是多级目录,如本项目 @/views/nested 那样, 有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>

如:@/views/nested/menu1/index.vue,原则上有多少级路由嵌套就需要多少个<router-view>

点击侧边栏 刷新当前路由

js
clickLink(path) {
  this.$router.push({
    path,
    query: {
      t: +new Date() //保证每次点击路由的query项都是不一样的,确保会重新刷新view
    }
  })
}

ps:不要忘了在 router-view 加上一个特定唯一的 key,如 <router-view :key="$route.path"></router-view>, 但这也有一个弊端就是 url 后面有一个很难看的 query 后缀如 xxx.com/article/list?t=1496832345025

你可以从前面的 issue 中知道还有很多其它方案。我本人在公司项目中,现在采取的方案是判断当前点击的菜单路由和当前的路由是否一致,但一致的时候,会先跳转到一个专门 Redirect 的页面,它会将路由重定向到我想去的页面,这样就起到了刷新的效果了。

面包屑

通过 watch $route 变化动态生成的。它和 menu 也一样,也可以通过之前那些配置项控制一些路由在面包屑中的展现。大家也可以结合自己的业务需求增改这些自定义属性。比如可以在路由中声明breadcrumb:false,让其不在 breadcrumb 面包屑显示。

侧边栏滚动问题

侧边栏 外链

js
{
  "path": "external-link",
  "component": Layout,
  "children": [
    {
      "path": "https://github.com/PanJiaChen/vue-element-admin",
      "meta": { "title": "externalLink", "icon": "link" }
    }
  ]
}

侧边栏默认展开

通过default-openeds来进行设置,首先找到 侧边栏代码

html
<el-menu
  :default-openeds="['/example','/nested']" // 添加本行代码
  :default-active="activeMenu"
  :collapse="isCollapse"
  :background-color="variables.menuBg"
  :text-color="variables.menuText"
  :unique-opened="false"
  :active-text-color="variables.menuActiveText"
  :collapse-transition="false"
  mode="vertical"
  >
    <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>

注意 :default-openeds="['/example','/nested']" 里面填写的是 submenu 的 route-path

权限验证

该项目中权限的实现方式是:通过获取当前用户的权限去比对路由表,生成当前用户具的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。

但其实很多公司的业务逻辑可能不是这样的,举一个例子来说,很多公司的需求是每个页面的权限是动态配置的,不像本项目中是写死预设的。但其实原理是相同的。如:你可以在后台通过一个 tree 控件或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。当用户登录后得到 roles,前端根据 roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原来是相同的,万变不离其宗。

只是多了一步将后端返回路由表和本地的组件映射到一起。

js
const map={
 login:require('login/index').default // 同步的方式
 login:()=>import('login/index')      // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
 { path: '/login', component: 'login', hidden: true }
]
// 之后遍历这个map,动态生成asyncRoutes
// 并将 component 替换为 map[component]

逻辑修改

现在路由层面权限的控制代码都在 @/permission.js 中,如果想修改逻辑,直接在适当的判断逻辑中 next() 释放钩子即可

指令权限

封装了一个指令权限,能简单快速的实现按钮级别的权限判断。 v-permission

使用

vue
<template>
  <!-- Admin can see this -->
  <el-tag v-permission="['admin']">admin</el-tag>

  <!-- Editor can see this -->
  <el-tag v-permission="['editor']">editor</el-tag>

  <!-- Editor can see this -->
  <el-tag v-permission="['admin', 'editor']">
    Both admin or editor can see this
  </el-tag>
</template>

<script>
// 当然你也可以为了方便使用,将它注册到全局
import permission from '@/directive/permission/index.js' // 权限判断指令
export default {
  directives: { permission },
}
</script>

局限(tab 等)

可以使用全局权限判断函数,用法和指令 v-permission 类似

vue
<template>
  <el-tab-pane v-if="checkPermission(['admin'])" label="Admin">
    Admin can see this
  </el-tab-pane>
  <el-tab-pane v-if="checkPermission(['editor'])" label="Editor">
    Editor can see this
  </el-tab-pane>
  <el-tab-pane
    v-if="checkPermission(['admin', 'editor'])"
    label="Admin-OR-Editor"
  >
    Both admin or editor can see this
  </el-tab-pane>
</template>

<script>
import checkPermission from '@/utils/permission' // 权限判断函数

export default {
  methods: {
    checkPermission,
  },
}
</script>

快捷导航(标题栏导航)

运用 keep-aliverouter-view 的结合。

html
<keep-alive :include="cachedViews">
  <router-view></router-view>
</keep-alive>

顶部标签栏导航实际作用相当于 nav 的另一种展现形式,其实说白了都是一个个 router-link,点击跳转到相应的页面。然后我们在来监听路由 $route 的变化,来判断当前页面是否需要重新加载或者已被缓存

visitedViews && cachedViews

目前 tags-view 维护了两个数组。

  • visitedViews : 用户访问过的页面 就是标签栏导航显示的一个个 tag 数组集合
  • cachedViews : 实际 keep-alive 的路由。可以在配置路由的时候通过 meta.noCache 来设置是否需要缓存这个路由 默认都缓存

Affix 固钉

当在声明路由是 添加了 Affix 属性,则当前 tag 会被固定在 tags-view 中(不可被删除)

js
 {
    path: '',
    component: Layout,
    redirect: 'dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: {
          title: 'dashboard',
          icon: 'dashboard',
          noCache: true,
          affix: true
        }
      }
    ]
  }

移除

如果没有标签导航栏需求的用户,建议移除此功能。

首先找到 @/layout/components/AppMain.vue 然后移除 keep-alive

vue
<template>
  <section class="app-main" style="min-height: 100%">
    <transition name="fade-transform" mode="out-in">
      <router-view></router-view>
    </transition>
  </section>
</template>

然后移除整个 @/layout/components/TagsView.vue 文件,并在@/layout/components/index@/layout/Layout.vue 移除相应的依赖。最后把 @/store/modules/tagsView 相关的代码删除即可。

新增页面

js
{
  path: '/excel',
  component: Layout,
  redirect: '/excel/export-excel',
  name: 'excel',
  meta: {
    title: 'excel',
    icon: 'excel'
  },
  children: [
    { path: 'export-excel', component: ()=>import('excel/exportExcel'), name: 'exportExcel', meta: { title: 'exportExcel' }},
    { path: 'export-selected-excel', component: ()=>import('excel/selectExcel'), name: 'selectExcel', meta: { title: 'selectExcel' }},
    { path: 'upload-excel', component: ()=>import('excel/uploadExcel'), name: 'uploadExcel', meta: { title: 'uploadExcel' }}
  ]
}

多级目录(嵌套路由)

有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>

样式

CSS Modules

只要加上 <style scoped> 这样 css 就只会作用在当前组件内了。详细文档见 vue-loader

sh
├── styles
   ├── btn.scss                 # 按钮样式
   ├── element-ui.scss          # 全局自定义 element-ui 样式
   ├── index.scss               # 全局通用样式
   ├── mixin.scss               # 全局mixin
   ├── sidebar.scss             # sidebar css
   ├── transition.scss          # vue transition 动画
   └── variables.scss           # 全局变量

自定义 element-ui 样式

在它的父级加一个 class,用命名空间来解决问题

scss
.article-page {
  /* 你的命名空间 */
  .el-tag {
    /* element-ui 元素*/
    margin-right: 0px;
  }
}

父组件改变子组件样式 深度选择器

当你子组件使用了 scoped 但在父组件又想修改子组件的样式可以 通过 >>> 来实现

css
<style scoped>
.a >>> .b { /* ... */ }
</style>

/* 编译陈如下 */
/* .a[data-v-f3f3eg9] .b {

} */

sass 你可以通过 /deep/ 来代替 >>> 实现想要的效果。

和服务端进行交互

前端请求流程

完整的前端 UI 交互到服务端处理流程是这样的:

  1. UI 组件交互操作;
  2. 调用统一管理的 api service 请求函数;
  3. 使用封装的 request.js 发送请求;
  4. 获取服务端返回;
  5. 更新 data;

为了方便管理维护,统一的请求处理都放在 @/src/api 文件夹中,并且一般按照 model 纬度进行拆分文件

request.js

其中,@/src/utils/request.js 是基于 axios 的封装,便于统一处理 POSTGET 等请求参数,请求头,以及错误提示信息等。具体可以参看 request.js。 它封装了全局 request拦截器response拦截器统一的错误处理统一做了超时处理baseURL设置等。

js
// api/article.js
import request from '../utils/request';
export function fetchList(query) {
  return request({
    url: '/article/list',
    method: 'get',
    params: query
  })
}


// views/example/list
import { fetchList } from '@/api/article'
export default {
  data() {
    list: null,
    listLoading: true
  },
  methods: {
    fetchData() {
      this.listLoading = true
      fetchList().then(response => {
        this.list = response.data.items
        this.listLoading = false
      })
    }
  }

Mock Data

mock 新方案

v4.0 版本之后,在本地会启动一个 mock-server 来模拟数据,线上环境还是继续使用 mockjs 来进行模拟(因为本项目是一个纯前端项目,你也可以自己搭建一个线上 server 来提供数据)。不管是本地还是线上所有的数据模拟都是基于 mockjs 生成的,所以只要写一套 mock 数据,就可以在多环境中使用。

该方案的好处是,在保留 mockjs 的优势的同时,解决之前的痛点。由于我们的 mock 是完全基于 webpack-dev-serve 来实现的,所以在你启动前端服务的同时,mock-server 就会自动启动,而且这里还通过 chokidar 来观察 mock 文件夹内容的变化。在发生变化时会清除之前注册的 mock-api 接口,重新动态挂载新的接口,从而支持热更新。有兴趣的可以自己看一下代码 mock-server.js。由于是一个真正的 server,所以你可以通过控制台中的 network,清楚的知道接口返回的数据结构。并且同时解决了之前 mockjs 会重写 XMLHttpRequest 对象,导致很多第三方库失效的问题。

本项目的所有请求都是通过封装的 request.js 进行发送的,通过阅读源码可以发现所有的请求都设置了一个 baseURL,而这个 baseURL 又是通过读取 process.env.VUE_APP_BASE_API 这个环境变量来动态设置的,这样方便我们做到不同环境使用不同的 api 地址。

移除

如果你不想使用 mock-server 的话只要在 vue.config.js 中移除 webpack-dev-serverproxyafter 这个 Middleware 就可以了

js
proxy: {
  // change xxx-api/login => mock/login
  // detail: https://cli.vuejs.org/config/#devserver-proxy
  [process.env.VUE_APP_BASE_API]: {
    target: `http://localhost:${port}/mock`,
    changeOrigin: true,
    pathRewrite: {
      ['^' + process.env.VUE_APP_BASE_API]: ''
    }
  }
},
after: require('./mock/mock-server.js')

mock-server 只会在开发环境中使用,线上生产环境目前使用 MockJs 进行模拟。如果不需要请移除。具体代码:main.js

js
import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
  mockXHR()
}

新增

如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的路由,对其进行拦截和模拟数据即可。

比如我现在在 src/api/article 中需要添加一个查询某篇文章下面评论数的接口 fetchComments,首先新建接口:

js
export function fetchComments(id) {
  return request({
    url: `/article/${id}/comments`,
    method: 'get',
  })
}

声明完接口之后,我们需要找到对应的 mock 文件夹 mock/article.js,在下面创建一个能拦截路由的 mock 接口

请注意,mock 拦截是基于路由来做的,请确 mock 数据一定能匹配你的 api 保路由,支持正则

js
// fetchComments 的 mock
{
  // url 必须能匹配你的接口路由
  // 比如 fetchComments 对应的路由可能是 /article/1/comments 或者 /article/2/comments
  // 所以你需要通过正则来进行匹配
  url: '/article/[A-Za-z0-9]/comments',
  type: 'get', // 必须和你接口定义的类型一样
  response: (req, res) => {
    // 返回的结果
    // req and res detail see
    // https://expressjs.com/zh-cn/api.html#req
    return {
      code: 20000,
      data: {
        status: 'success'
      }
    }
  }
}

修改

最常见的操作就是:你本地模拟了了一些数据,待后端完成接口后,逐步替换掉原先 mock 的接口。

我们以 src/api/role.js 中的 getRoles 接口为例。它原本是在 mock/role/index.jsmock 的数据。现在我们需要将它切换为真实后端数据,只要在 mock/role/index.js 找到对应的路由,之后将它删除即可。这时候你可以在 network 中,查看到真实的数据。

js
// api 中声明的路由
export function getRoles() {
  return request({
    url: '/roles',
    method: 'get'
  })
}

//找到对应的路由,并删除
{
    url: '/roles',
    type: 'get',
    response: _ => {
      return {
        code: 20000,
        data: roles
      }
    }
  },

启用纯前端 Mock

现在在 mock/index.js 也封装了一个纯前端 mock 的方法,你只需要在 src/main.js 中:

js
import { mockXHR } from '../mock'
mockXHR()

这样就会变成纯前端 mock 数据了和 v4.0 版本之前的 mock 方案是一样的,原理见上文。

引入外部模块

使用第三方库

js
import moment from 'moment'
Object.defineProperty(Vue.prototype, '$moment', { value: moment })

由于所有的组件都会从 Vue 的原型对象上继承它们的方法, 因此在所有组件/实例中都可以通过 this.$moment: 的方式访问 Moment 而不需要定义全局变量或者手动的引入。

js
export default {
  created() {
    console.log('The time is '.this.$moment().format('HH:mm'))
  },
}

在 Vue 中使用第三方库的方式

全局变量

在项目中添加第三方库的最简单方式是讲其作为一个全局变量, 挂载到 window 对象上:

entry.js

`window._ = require('lodash');`

MyComponent.vue

```js
export default {
  created() {
    console.log(_.isEmpty() ? 'Lodash everywhere!' : 'Uh oh..')
  },
}
```

这种方式不适合于服务端渲染, 因为服务端没有 window 对象, 是 undefined, 当试图去访问属性时会报错

在每个文件中引入

另一个简单的方式是在每一个需要该库的文件中导入:

MyComponent.vue

```js
import _ from 'lodash';

export default {
  created() {
    console.log(_.isEmpty() ? 'Lodash is available here!' : 'Uh oh..');
  }
}
```

这种方式是允许的, 但是比较繁琐, 并且带来的问题是: 你必须记住在哪些文件引用了该库, 如果项目不再依赖这个库时, 得去找到每一个引用该库的文件并删除该库的引用. 如果构建工具没设置正确, 可能导致该库的多份拷贝被引用.

优雅的方式

Vuejs 项目中使用 JavaScript 库的一个优雅方式是讲其代理到 Vue 的原型对象上去. 按照这种方式, 我们引入 Moment 库:

entry.js

```js
import moment from 'moment';

Object.defineProperty(Vue.prototype, '$moment', { value: moment });
```

由于所有的组件都会从 Vue 的原型对象上继承它们的方法, 因此在所有组件/实例中都可以通过 this.$moment: 的方式访问 Moment 而不需要定义全局变量或者手动的引入.

MyComponent.vue

```js
export default {
  created() {
    console.log(_.isEmpty() ? 'Lodash everywhere!' : 'Uh oh..')
  },
}
```
Object.defineProperty

一般而言, 可以按照下面的方式来给对象设置属性:

Vue.prototype.$moment = moment;

可以这样做, 但是 Object.defineProperty 允许我们通过一个 descriptor 来定义属性. Descriptor 运行我们去设置对象属性的一些底层(low-level)细节, 如是否允许属性可写? 是否允许属性在 for 循环中被遍历.

通常, 我们不会为此感到困扰, 因为大部分时候, 对于属性赋值, 我们不需要考虑这样的细节. 但这有一个明显的优点: 通过 descriptor 创建的属性默认是只读的.

这就意味着, 一些处于迷糊状态的(coffee-deprived)开发者不能在组件内去做一些很愚蠢的事情, 就像这样:

this.$http = 'Assign some random thing to the instance method';this.$http.get('/'); // TypeError: this.$http.get is not a function 此外, 试图给只读实例的方法重新赋值会得到 TypeError: Cannot assign to read only property 的错误.

$

你可能会注意到, 代理第三库的属性会有一个 $ 前缀, 也可能看到其它类似 $refs, $on, $mount 的属性和方式, 它们也有这个前缀.

这个不是强制要求, 给属性添加 $ 前缀是提供那些处于迷糊状态(coffee-deprived)的开发者这是一个公开的 API, 和 Vuejs 的一些内部属性和方法区分开来.

this

你还可能注意到, 在组件内是通过 this.libraryName 的方式来使用第三方库的, 当你知道它是一个实例方法时就不会感到意外了. 但与全局变量不同, 通过 this 来使用第三方库时, 必须确保 this 处于正确的作用域. 在回调方法中 this 的作用域会有不同, 但箭头式回调风格能保证 this 的作用域是正确的:

js
this.$http.get('/').then((res) => {
  if (res.status !== 200) {
    this.$http.get('/') // etc
    // Only works in a fat arrow callback.
  }
})

插件

如果你想在多个项目中使用同一个库, 或者想将其分享给其他人, 可以将其写成一个插件:

js
import MyLibraryPlugin from 'my-library-plugin'
Vue.use(MyLibraryPlugin)

在应用的入口引入插件之后, 就可以在任何一个组件内像使用 Vue Router, Vuex 一样使用你定义的库了.

写一个插件

首先, 创建一个文件用于编写自己的插件. 在示例中, 我会将 Axios 作为插件添加到项目中, 因而我给文件起名为 axios.js. 其次, 插件要对外暴露一个 install 方法, 该方法的第一个参数是 Vue 的构造函数:

js
// 编写插件
import axios from 'axios'

export default {
  install: function (Vue) {
    // Do stuff
    Object.defineProperty(Vue.prototype, '$http', { value: axios })
  },
}

// 注册插件
import AxiosPlugin from './axios.js'
Vue.use(AxiosPlugin)

new Vue({
  created() {
    console.log(this.$http ? 'Axios works!' : 'Uh oh..')
  },
})
彩蛋: 插件的可选参数

插件的 install 方法可以接受可选参数. 一些开发可能不喜欢将 Axios 实例命名为 $http, 因为这是 Vue Resource 提供的一个通用名字. 因而可以提供一个可选的参数允许他们随意命名:

js
import axios from 'axios'

export default {
  // 默认名称
  install: function (Vue, name = '$http') {
    Object.defineProperty(Vue.prototype, name, { value: axios })
  },
}
js
import AxiosPlugin from './axios.js'
Vue.use(AxiosPlugin, '$axios')

new Vue({
  created() {
    console.log(this.$axios ? 'Axios works!' : 'Uh oh..')
  },
})

构建和发布

环境变量

所有测试环境或者正式环境变量的配置都在 .env.development.env.xxxx 文件中。

它们都会通过 webpack.DefinePlugin 插件注入到全局。

环境变量必须以VUE_APP_为开头。如:VUE_APP_APIVUE_APP_TITLE

分析构建文件体积

如果你的构建文件很大,你可以通过 webpack-bundle-analyzer 命令构建并分析依赖模块的体积分布,从而优化你的代码

npm run preview -- --report

发布

对于发布来讲,只需要将最终生成的静态文件,也就是通常情况下 dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径

部署时可能会发现资源路径不对 ,只需修改 vue.config.js 文件资源路径即可

publicPath: './' //请根据自己路径来配置更改

前端路由与服务端的结合

如果你有对应的后台服务器,那么我们推荐采用 browserHistory,只需要在服务端做一个映射,比如:

Apache

sh
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

nginx

sh
location / {
  try_files $uri $uri/ /index.html;
}

GIT HOOK

husky

最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 husky 或者 pre-commit 在本地提交之前先做一次 Lint 校验。

npm install husky -D -S

然后修改 package.json,增加配置:

json
"husky": {
    "hooks": {
      "pre-commit": "eslint --ext .js,.vue src"
    }
  }

最后尝试 Git 提交,你就会很快收到反馈:

git commit -m "Keep calm and commit"

但这样会有一个问题,就是这次提交,我可能只修改了一个文件,比如我就修改了 foo.js 的内容,但它依然会校验所有 src 下面的.js 文件,非常的不友好。导致的问题就是:每次我提交我写的代码,还先要帮人的代码问题解决了才能顺利提交,而且当项目大了之后,检查速度也会变得越来越慢了。

lint-staged

它只会校验检查你提交或者说你修改的部分内容。

npm install lint-staged -D -S

然后,修改 package.json 配置:

json
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
    "src/**/*.{js,vue}": [
      "eslint --fix",
      "git add"
    ]
  }

风格规范

Component

所有的 Component 文件都是以大写开头 (PascalCase)

JS 文件

所有的.js 文件都遵循横线连接 (kebab-case)。

Views

views 文件下,代表路由的.vue 文件都使用横线连接 (kebab-case),代表路由的文件夹也是使用同样的规则

  • @/src/views/svg-icons/index.vue
  • @/src/views/svg-icons/require-icons.js

路由懒加载

当打包构建应用时,Javascript 包会变得非常大,影响页面加载速度。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了

结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。如:

const Foo = () => import('./Foo.vue')

当你觉得你的页面热更新速度慢的时候,才需要往下看

方案

使用 babelplugins babel-plugin-dynamic-import-node。它只做一件事就是将所有的 import()转化为 require(),这样就可以用这个插件将所有异步组件都用同步的方式引入,并结合 BABEL_ENV 这个 babel 环境变量,让它只作用于开发环境下,在开发环境中将所有 import()转化为 require(),这种方案解决了之前重复打包的问题,同时对代码的侵入性也很小,你平时写路由的时候只需要按照官方文档路由懒加载的方式就可以了,其它的都交给 babel 来处理,当你不想用这个方案的时候,也只要将它从 babelplugins 中移除就可以了。

具体代码: 首先在 package.json 中增加 BABEL_ENV

"dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"

接着在.babelrc 只能加入 babel-plugin-dynamic-import-node 这个 plugins,并让它只有在 development 模式中才生效。

json
{
  "env": {
    "development": {
      "plugins": ["dynamic-import-node"]
    }
  }
}

图表

Echarts

js
// 按需引入 引入 ECharts 主模块
var echarts = require('echarts/lib/echarts')
// 引入柱状图
require('echarts/lib/chart/bar')
// 引入提示框和标题组件
require('echarts/lib/component/tooltip')
require('echarts/lib/component/title')

//全部引入
var echarts = require('echarts')

。因为 ECharts 初始化必须绑定 dom,所以我们只能在 vuemounted 生命周期里进行初始化。

js
mounted() {
  this.initCharts();
},
methods: {
  initCharts() {
    this.chart = echarts.init(this.$el);
    this.setOptions();
  },
  setOptions() {
    this.chart.setOption({
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      xAxis: {
        data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
      },
      yAxis: {},
      series: [{
        name: '销量',
        type: 'bar',
        data: [5, 20, 36, 10, 10, 20]
      }]
    })
  }
}

就这样简单,ECharts 就配置完成了,这时候你想说我的 data 是远程获取的,或者说我动态改变 ECharts 的配置该怎么办呢?我们可以通过 watch 来触发 setOptions 方法

js
//第一种 watch options变化 利用vue的深度 watcher,options 一有变化就重新setOption
watch: {
  options: {
    handler(options) {
      this.chart.setOption(this.options)
    },
    deep: true
  },
}
//第二种 只watch 数据的变化 只有数据变化时触发ECharts
watch: {
  seriesData(val) {
    this.setOptions({series:val})
  }
}

其实都差不多,还是要结合自己业务来封装。后面就和平时使用 ECharts 没有什么区别了。题外话 ECharts 的可配置项真心多,大家使用的时候可能要花一点时间了解它的 api 的。知乎有个问题:百度还有什么比较良心的产品?答案:ECharts,可见 ECharts 的强大与好用。

图标

推荐单独导出 svg 的引入使用方式。

下载完成之后将下载好的 .svg 文件放入 @/icons/svg 文件夹下之后就会自动导入。

使用方式

<svg-icon icon-class="password" /> // icon-class 为 icon 的名字

改变颜色

svg-icon 默认会读取其父级的 color fill: currentColor;

你可以改变父级的 color 或者直接改变 fill 的颜色即可。

CDN

使用 CDN 外链的方式引入这些第三方库,这样能大大增加构建的速度(通过 CDN 引入的资源不会经 webpack 打包)。如果你的项目没有自己的 CDN 服务的话,使用一些第三方的 CDN 服务,如 unpkg 等是一个很好的选择,它提供过了免费的资源加速,同时提供了缓存优化,由于你的第三方资源是在 html 中通过 script 引入的,它的缓存更新策略都是你自己手动来控制的,省去了你需要优化缓存策略功夫。

采用这种引入方式,或者使用 webpack dll 的方式进行优化。如果你觉得 CDN 引入对于的项目有益处,你可以遵循如下方法进行修改:

使用方式

  1. 先找到 vue.config.js, 添加 externalswebpack 不打包 vueelement

    js
    externals: {
      vue: 'Vue',
      'element-ui':'ELEMENT'
    }
    
  2. 然后配置那些第三方资源的 CDN,请注意先后顺序。

    js
    const cdn = {
      css: [
        // element-ui css
        'https://unpkg.com/element-ui/lib/theme-chalk/index.css',
      ],
      js: [
        // vue must at first!
        'https://unpkg.com/vue/dist/vue.js',
        // element-ui js
        'https://unpkg.com/element-ui/lib/index.js',
      ],
    }
    
  3. 之后通过 html-webpack-plugin 注入到 index.html 之中:

    js
    config.plugin('html').tap((args) => {
      args[0].cdn = cdn
      return args
    })
    
  4. 找到 public/index.html。通过你配置的CND Config 依次注入 cssjs

    html
    <head>
      <!-- 引入样式 -->
      <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
      <link rel="stylesheet" href="<%=css%>" />
      <% } %>
    </head>
    
    <!-- 引入JS -->
    <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%=js%>"></script>
    <% } %>
    
  5. 之后还有一个小细节是如果你用了全局对象方式引入 vue,就不需要 手动 Vue.use(Vuex) ,它会自动挂载

    js
    // src/store/index.js
    // 在此文件中可删除这两行
    import Vue from 'vue'
    
    Vue.use(Vuex)
    

更换主题

样式覆盖

scss
/* 你的命名空间 */
.article-page {
  /* element-ui 元素 */
  .el-tag {
    margin-right: 0px;
  }
}

动态换肤

element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json 拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style 标签来覆盖原有的 css 样式。

代码地址:@/src/components/ThemePicker

js
const version = require('element-ui/package.json').version

const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
this.getCSSString(url, chalkHandler, 'chalk')

getCSSString(url, callback, variable) {
  const xhr = new XMLHttpRequest()
  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4 && xhr.status === 200) {
      this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
      callback()
    }
  }
  xhr.open('GET', url)
  xhr.send()
}

使用方式

在项目中引入 ThemePicker 组件即可

import ThemePicker from '@/components/ThemePicker'

  • 优点:无需准备多套主题,可以自由动态换肤
  • 缺点:自定义不够,只支持基础颜色的切换

国际化

国际化 i18n 方案。通过 vue-i18n 而实现。

全局 lang

同时在 @/lang/index.js 中引入了 element-ui 的语言包

js
// $t 是 vue-i18n 提供的全局方法,更多信息请查看其文档
$t('login.title')

异步 lang

有一些某些特定页面才需要的 lang,比如 @/views/i18n-demo 页面

js
import local from './local'

this.$i18n.mergeLocaleMessage('en', local.en)
this.$i18n.mergeLocaleMessage('zh', local.zh)

js 中使用 $t

如果你使用如 select 组件,它的值是通过 v-for 而来,如:

html
<el-select v-model="value">
  <el-option
    v-for="item in options"
    :key="item.value"
    :label="item.label"
    :value="item.value"
  />
</el-select>
js
this.options = [
  {
    value: '1',
    label: this.$t('i18nView.one'),
  },
  {
    value: '2',
    label: this.$t('i18nView.two'),
  },
  {
    value: '3',
    label: this.$t('i18nView.three'),
  },
]

这种情况下,国际化只会执行一次,因为在 js 中的 this.options 只会在初始化的时候执行一次,它的数据并不会随着你本地 lang 的变化而变化,所以需要你在 lang 变化的时候手动重设 this.options。

js
export default {
  watch: {
    lang() {
      this.setOptions()
    },
  },
  methods: {
    setOptions() {
      this.options = [
        {
          value: '1',
          label: this.$t('i18nView.one'),
        },
        {
          value: '2',
          label: this.$t('i18nView.two'),
        },
        {
          value: '3',
          label: this.$t('i18nView.three'),
        },
      ]
    },
  },
}

移除国际化

src/main.js 中移除 import i18n from './lang' 并且删除 src/lang 文件夹。

并在 src/layout/components/Levelbarsrc/layout/components/SidebarItemsrc/layout/components/TabsView 等文件夹中 移除 this.\$t('route.xxxx') 使用国际化的方式。

v4.1.0+版本之后,默认 master 将不再提供国际化。因为大部分用户其实是用不到国际化的,但移除国际化工作量又相当的大。

如果你有国际化需求的请使用 i18n 分支,它与 master 同步更新。

错误处理

页面

  • 404

    页面级的错误处理由 vue-router 统一处理,所有匹配不到正确路由的页面都会进 404 页面。

    { path: '*', redirect: '/404' }

  • 401

    @/permission.js做了权限控制,所有没有权限进入该路由的用户都会被重定向到 401页面

请求

项目里所有的请求都会走@/utils/request.js里面创建的的 axios 实例,它统一做了错误处理

js
service.interceptors.response.use(
  (response) => {
    /**
     * code为非20000是抛错 可结合自己业务进行修改
     */
    const res = response.data
    if (res.code !== 20000) {
      Message({
        message: res.data,
        type: 'error',
        duration: 5 * 1000,
      })

      // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        MessageBox.confirm(
          '你已被登出,可以取消继续留在该页面,或者重新登录',
          '确定登出',
          {
            confirmButtonText: '重新登录',
            cancelButtonText: '取消',
            type: 'warning',
          }
        ).then(() => {
          store.dispatch('FedLogOut').then(() => {
            location.reload() // 为了重新实例化vue-router对象 避免bug
          })
        })
      }
      return Promise.reject('error')
    } else {
      return response.data
    }
  },
  (error) => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

因为所有请求返回的是 promise,所以你也可以对每一个请求通过 catch 错误,从而进行单独的处理。

js
getInfo()
  .then((res) => {})
  .catch((err) => {
    xxxx
  })

代码

  • 代码层面的错误处理,如果你开启了 eslint 在编写代码的时候就会提示错误
  • 当然还有很多不能被 eslint 检查出来的错误,vue 也提供了全局错误处理钩子 errorHandler,相对应的错误收集

监听错误:@/errorLog.js

错误展示组件:@/components/ErrorLog

相关问题

浏览器兼容性问题

兼容性需求可使用 babel-polyfill

  • 下载依赖 npm install --save babel-polyfill

  • 在入口文件中引入 import 'babel-polyfill'或者require('babel-polyfill)

  • webpack.config.js 中加入 babel-polyfill 到你的入口数组:

    js
    module.exports = {
      entry: ['babel-polyfill', './app/js'],
    }
    

我用了 axios , 为什么 IE 浏览器不识别(IE9+)

IE 不支持 promise,所有需要单独引入 polyfill

js
npm install es6-promise

// 在 main.js 首行引入即可
require("es6-promise").polyfill();