[万字左右]个人开源项目,我的一些开发笔记

前言

基于Vue2.x + Vue-cli4.x的后台管理系统,主要包含路由自动挂载、主题换肤、多语言国际化、表格操作、图表展示、全屏操作、字体图标、权限控制、路由嵌套和数据mock等功能。部分内容学习自vue-element-admin等优秀开源作品,个人做了部分取舍和优化。以下内容为后面总结的,可能存在部分错误和不足,欢迎指出!

  • 演示地址:juetan.github.io/management
  • 演示账号:管理账号:admin-admin;普通账号:juetan-juetan

技术栈

基于NodeJS、VS Code和vue-cli开发,技术栈主要以vue、axios、vuex、
vue-router和element-ui为主。此外,还包含以下依赖或包:

  • 主题修改基于webpack-theme-color-replacer
  • css预处理基于scss
  • 国际化基于vue-i18n
  • 数据mock基于mockjs
  • 图表展示基于echarts
  • markdown基于mavon-editor
  • 全屏展示基于screenfull
  • 计数动效基于countTo
  • 进度加载基于nprogress
  • 版本控制基于Git
  • 静态部署基于Github Pages和Github Action

主要功能

主题换肤

Vue-element-admin里提供了两种修改主题的思路,但是只支持element-ui的颜色,不支持vue组件内的变量颜色。恰好遇到一个webpack插件-webpack-theme-color-replacer,可以完美支持这些功能且体积更小,原理主要是在打包时将涉及到的颜色样式单独提取出来,更换主题时进行正则替换。

语言国际

安装完vue-i18n差不多就能上手使用,但这里需要注意的是与第三方框架的语言同步,如element-ui、echarts和mavon-editor等,好在这些开源包都有提供国际化的功能,这里注意下加入的内容即可。

CSS预处理

这里采用scss,一是scss功能和使用都广泛些,二是element-ui也是采用的scss,修改主题的时候比较方便。关于css文件和样式文件的规划,我在知乎看到一篇不错的文章,如果你感兴趣可以看一下。

状态管理

在这里使用vuex我觉得是很有必要的,不单是vue组件间的状态共享,还涉及到第三方依赖对状态的修改和读取。vuex采用模块化封装,读取统一从state读取,更新从actions中操作。

网络请求

由于是纯前端项目,数据MOCK多少是个问题,之前有尝试过Faskmock,但有时候感觉不是很稳定,最后我还是选择使用纯MOCKJS是生成数据。网络请求使用axios进行封装,以模块化的方式进行调用,这里与vuex结合得比较深。

按需加载

项目中用到不少大型的库,完全加载的话有些浪费,因此有必要进行按需加载。项目中涉及的大型库有element-ui、lodash和echarts等。

静态部署

个人开源项目一般都有部署到Github pages的需求,除了创建<username>.github.io仓库这样的方式,其实还可以通过在普通仓库创建gh-pages分支来挂载静态页面(上传后通过<username>.github.io/<repository>访问)。再结合Github Action,可以帮助我们进行CI构建和部署。

项目启动

开始前需要安装好NodeJS和vue-cli(4.x版本),代码编辑器我使用的是vs code。我曾尝试过Vue3+TS的方式,但使用过程感觉不太顺畅(主要是太菜),这里依旧使用2.x版本的vue,但cli是4.x的。

Vue-cli4.x有2种方式创建项目,一种是vue ui这样的GUI界面,另一种是vue create这样的CLI界面。这里我用的是CLI方式,毕竟在vs code终端里操作还是比较方便的。

// management 为项目名称([Vue 2] babel, eslint)
vue create management
 

题外话:Vue-cli使用的是npm run serve而不是npm run dev。之前有点奇怪为什么不是server而是serve,后来才知道serve是个动词,而server是个名词(~ ̄▽ ̄)~。

项目结构

Vue-cli为我们提供一个项目脚手架,但目录结构什么的仍需要根据我们根据需要自己需要设置。在这里,我将vue全家桶的封装,统一放在/src目录下(已经是种默认规则),目前有如下:

vuex                           # /src/store
axios                          # /src/api
vue-router                     # /src/router
vue-i18n                       # /src/lang
 

对于其他第三方库,我并不是直接import引用,而是统一放置在/src/plugins目录下,进行二次封装后再引用,目前有如下:

element-ui                     # /src/plugins/element-ui.js
driver.js                      # /src/plugins/driver.js
nprogress                      # /src/plugins/nprogress.js
vue-count-to                   # /src/plugins/count-to.js
webpack-theme-color-replacer   # /src/plugins/theme-replacer.js
 

最终项目结构如下:

├── .github                    # Github Action的配置目录
├── public                     # 公共目录
│   │── favicon.ico            # 网站图标
│   └── index.html             # 首页模板
├── /src                        # 源代码目录(/src#source)
│   ├── api                    # 网络请求
│   ├── assets                 # 静态资源(图片、字体和样式)
│   ├── components             # 公共组件
│   ├── config                 # 配置文件(角色配置、主题配置和vue指令等)
│   ├── helper                 # 助手函数
│   ├── lang                   # 语言国际(lang#language)
│   ├── pages                  # 路由页面
│   ├── plugins                # 第三方依赖封装(element-ui、nprogress、driver等二次封装)
│   ├── router                 # 路由封装
│   ├── store                  # 状态管理
│   └── main.js                # 入口文件
├── .env.development           # 开发环境变量
├── .env.production            # 生产环境变量
├── .gitignore                 # git配置
├── babel.config.j             # babel配置
├── package-lock.json          # package锁定配置
├── package.json               # package配置
├── README.md                  # readme
└── vue.config.js              # vue-cli的配置文件
 

项目配置

vue-cli提供有vue.config.js项目配置和.env环境变量配置功能,在开发之前有必要对项目做一下基本的配置。

配置vue-cli

首先,在项目根目录下,创建vue.config.js文件,写入一些基本的配置(更多配置可以参考这里)。

需要注意的是,导出方式需为commonJS的module.exports而不是ES module的export default,这是因为Node默认只支持commojs模块,而webpack才对es模块进行支持。

<!-- filename: /vue.config.js -->
module.exports = {
  // 因为要部署到GitHub pages上,所以将生产环境的值改成仓库名称
  publicPath: process.env.NODE_ENV === "production" ? "/management/" : "/",

  // 减少生产环境下的文件体积
  productionSourceMap: false,

  // 链式配置webpack
  chainWebpack: (config) => {
    // 初始化public/index.html中的title值
    config.plugin("html").tap((args) => {
      // 该值在.env.production和.env.development中配置
      args[0].title = `${process.env.VUE_APP_NAME} - ${process.env.VUE_APP_DESCRIPTION}`;
      return args;
    });
  },
};
 

配置环境变量

这是Vue-cli提供的环境变量功能,创建.env.development.env.production文件分别作用于开发环境和生产环境。关于env的更多信息,可以点击这里。

<!-- filename: /.env.development -->
# 系统名称
VUE_APP_NAME = '绝弹开发系统'

# 系统描述
VUE_APP_DESCRIPTION = '遇到路途上不知道的风景'

# 开发环境的API接口
VUE_APP_API_BASE_URL = ''

# 开发环境下的路由模式
VUE_APP_ROUTER_MODE = 'history'
 
<!-- filename: /.env.production -->
# 系统名称
VUE_APP_NAME = '绝弹管理系统'

# 系统描述
VUE_APP_DESCRIPTION = '遇到你不知道的风景'

# 开发环境的API接口
VUE_APP_API_BASE_URL = 'https://www.fastmock.site/mock/7522085e7af7a7b98bc42530bd1ddfb7/management'

# 开发环境下的路由模式
VUE_APP_ROUTER_MODE = 'hash'
 

网站图标

这个步骤当前可做可不做,主要看个人喜好,但我喜欢换个icon这样看起来顺眼些。将/public目录下的favicon.ico文件替换掉即可。

本地域名

这一步主要是将诸如localhost:8080/#/home这样的URL,换为www.w.com/home这样的URL,这步同样可做可不做,主要看个人习惯。

配置本地hosts

我使用的是windows系统,这里就以这个为例,打开C:\Windows\System32\drivers\etc目录下的hosts文件,在文件的末尾添加一个本地域名映射。推荐使用单数字或单字母(非QXZ).com域名,因为这些域名尚未启用,暂无冲突危险。

<!-- filename: C:\Windows\System32\drivers\etc\hosts -->
# IP和域名之间以空格分开
127.0.0.1 w.com
 

配置vue.config.js

以上配置好之后,在浏览器访问w.com会直接指向本地IP127.0.0.1。但vue-cli会默认使用端口8080,如果不想以w.com:8080的形式访问,则需要配置一下vue.config.js。同时,还需要将disableHostCheck设为true,否则在访问的时候回出现Invalid Host header的情况。

<!-- filename: vue.config.js -->
module.exports = {
  configureWebpack: {
    devServer: {
      // 在hosts文件中添加127.0.0.1 www.w.com记录,再配合以下两个参数设置可使用www.w.com代替localhost:8080访问
      host: "localhost",
      // 端口设为80,方便使用本地域名开发
      port: "80",
      // 因为使用本地域名,此值需设为true,避免出现Invalid Host header的情况
      disableHostCheck: true,
    },
  }
}
 

配置vue-router

以上已经可以正常使用,但还可以再尝试优化一下:在本地开发环境下使用history路由提升浏览体验,在实际生产环境下使用hash路由。

<!-- filename: /.env.development -->
# 开发环境下的路由模式
VUE_APP_ROUTER_MODE = 'history'
 
<!-- filename: /.env.production -->
# 生产环境下的路由模式
VUE_APP_ROUTER_MODE = 'hash'
 

这里使用.env进行环境变量配置,修改和配置都方便一些,不过要注意文件名不要以.local结尾,这些文件会被git忽略。

自动构建

版本控制是必不可少的,什么协作开发等暂且不谈。在这个项目里,主要是要发布到Github,配合Github Action以及Github Pages进行持续构建及自动部署。

创建仓库

直接3连操作,主要是先把之前改造的目录结构给记录下来。

git init
git add .
git commit -m "首次提交"
 

关联Github仓库

这里我用的的是github不一定非得是origin,这只是个别名而已,另外我这里用的是ssh链接。

git remote add github git@github.com:juetan/management.git
 

刚开始时,我用的是https链接,但由于某股神秘力量会遇到下面的错误:

// 提示问题
OpenSSL SSL_read: Connection was reset, errno 10054
 

谷歌一搜也有解决办法:

// 解决办法
git config --global http.sslVerify "false"
 

但实际效果并不是很理想,偶尔还是有这个问题,后面我想到可以用SSH连接,于是切换到这种方式,目前没有再遇到过这个问题。

CI构建

个人开源项目一般都有部署到Github pages的需求,除了创建<username>.github.io仓库这样的方式,其实还可以通过在普通仓库创建gh-pages分支来挂载静态页面(上传后通过<username>.github.io/<repository>访问)。再结合Github Action,可以帮助我们进行CI构建和部署。

本地配置

GitHub Action使用.yml文件作为配置文件,需要在项目根目录下创建.github/workflows目录,文件名可任意。网上有些教程可能会说要设置secret密钥什么的,我翻阅了一下文档,是可以直接使用secrets.GITHUB_TOKEN的,所以这一步可以省略。

<!-- filename: /.github/workflows/main.yml -->
# 工作流名称,可自定义
name: 通过GithubAction自动部署到GithubPages

# 事件监听,决定什么触发该工作流内的任务
on:
  # 在master分支推动到github时触发
  push:
    branches: [ master ]

# 任务集合,可包含多个任务
jobs:
  # 任务名称
  build:
    # 运行的操作系统
    runs-on: ubuntu-latest

    # 步骤集合,可包含多个步骤
    steps:
      # 单个步骤,没有名称,直接使用一个action
      - uses: actions/checkout@v2

      # 单个步骤,带有名称,带有参数
      - name: build and deploy
        run: |
          npm install
          npm run build
          cd dist
          # 用户名自定义修改
          git config --global user.name "juetan"
          # 邮箱自定义修改
          git config --global user.email "810335188@qq.com"
          git init
          git add -A
          git commit -m "通过Github Action自动部署"
          git push -f "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:gh-pages
 

除此之外,还要配置下vue.config.js,将公共目录指向仓库名。

<!-- filename: /vue.config.js -->
module.exports = {
  // 公共路径,因为要部署到GitHub pages上,所以将生产环境的值改成仓库名称
  publicPath: process.env.NODE_ENV === "production" ? "/management/" : "/",
}
 

远程配置

在GitHub仓库的setting中,将Pages中的分支切换为gh-pages,然后就可以看到下图中绿色背景框中的发布地址。

提交到github

本地和远程都配置好之后,就可以push到GitHub仓库了。

git push github master
 

然后在仓库中的Actions页面,可以看到我们的CI任务,点击进去可以看到详情。等待构建完成后(几十秒到几分钟不等),就可以通过<username>.github.io/<repository>访问了。此外,域名是可以用自己域名的而且支持https,如果是个人博客可以考虑用域名,这里不做细说。最后,贴一张成功的图。

数据模拟

对于数据模拟,如果有后端尽量使用后端提供的API,没有后端配合的话可以考虑使用以下2种途径来获得数据:

  • 使用Easy-Mock或Fast-Mock这样的在线MOCK平台
  • 本地环境使用Mock.js来实现。

我一开始使用的是fast-mock,但有时候不是特稳定,最后转向了mock.js。对于mock.js的话,其实也有2种使用方式:

  • 直接在src\main.js中引用,这会重写XHR并可能与其他库冲突,同时在浏览器的network面板中看不到网络请求;
  • 通过webpack的devServer配置,将匹配的请求导向mock.js。

目前使用的是方式1,因为本项目只是个纯前端项目,同时涉及的请求并不多。

MockJS安装

在终端里安装,如果使用方式1保存为dependencies,使用方式2保存为devDependencies

// 如果是方式2,保存为--save-dev
npm install mockjs --save
 

MockJS配置

mock文件夹一般放置在根目录下,我想这样做的原因是:方便数据源的切换,无论使用上面的哪种方式,都可以做出很好的处理。目前只是我个人猜测,这里遵循这种默认规则。关于MockJS的配置和语法这里不做介绍,详情可点击官网查看。

<!-- filename: /mock/server.js -->
import Mock from "mockjs";

// MOCKJS配置
Mock.setup({
  // 延迟返回数据
  timeout: 800,
});

// 表格数据
Mock.mock("/table", "get", {
  code: 2000,
  message: "数据返回成功",
  "data|80": [
    {
      "id|+1": 1,
      name: "@cname",
      number: "equip-@first",
      spec: "ws-@last",
      type: "@natural(1,3)",
      status: "@natural(1,3)",
      manufacturer: "@cname",
      buyDate: "@date",
      mark: "无",
    },
  ],
});
 

文件引入

直接在main.js中引入,即可使用axios等网络请求库进行数据请求。

<!-- filename: /src/main.js -->
import "/mock/server.js";
 

状态管理

关于vuex的使用,官方文档已经给出很好的实践,直接按照模块的方式进行封装即可。

vuex安装

直接在命令行里安装即可;

npm install vuex --save
 

实例创建

/src/store目录下,创建index.js文件;该文件的作用在于,自动引入./modules目录下的所有.js文件,并导出实例化的vuex。

<!-- filename: /src/store/index.js --> 
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

// 使用require.context自动引入modules目录下的所有文件
const moduleFiles = require.context("./modules", true, /\.js$/);

// moduleFiles既是函数也是对象,此处作对象调用
const modules = moduleFiles.keys().reduce((modules, modulePath) => {
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, "$1");
  // 此处作函数调用,类似于import
  const module = moduleFiles(modulePath);
  // 将所有模块放在一个数组内
  modules[moduleName] = module.default;
  return modules;
}, {});

// 创建vuex实例
const store = new Vuex.Store({
  modules
});

export default store;
 

模块创建

模块放在/src/store下的modules目录,每个模块命名一个.js文件,例如user.js;

<!-- /src/store/modules/user.js -->: 
const state = {
  // 用户名
  username: '',
};

const mutations = {
  // 设置用户名
  set_username: (state, name) => {
    state.username = name;
  },
};

export default {
  namespaced: true,
  state,
  mutations,
};
 

实例引入

/src/main.js中引入即可;

<!-- filename: /src/main.js --> 
import store from '@/store'

new Vue({
  store
}).$mount('#app');
 

使用

vue组件内使用:

<!-- filetype: .vue -->
export default {
  methods: {
    xxx() {
      this.$store.commit('user/set_name',name)
    }
  }
}
 

非vue组件内使用:

<!-- filetype: .js -->
import store from '@/store'

cosnt title = store.state.default.title;
 

网络请求

axios有2种封装的思路是不错的。一种是偏函数的封装,每个请求模块导出一个个请求函数,使用的时候按需引用就行;另一种是偏对象的封装,每个模块统一导出,汇总到index文件,最后挂在到Vue.prototype上。这里采用第2种,主要是和vuex配合进行状态管理,同时挂载$request方法到vue原型上方便调用。

axios安装

直接在命令行里安装axios即可;

npm install axios --save
 

实例创建

src/api目录下,新建index.js文件,配置基本参数、interceptors拦截器并实例化axios。具体代码不贴了可以去看源码,这里贴一下主代码并说下2个点:

  • baseURL:通过process.env.NODE_ENV === 'development' ? 'xx' : 'xx'配置是可以的,不过利用vue-cli提供的环境变量也是不错的。
  • loading: 这里封装了一个loading并加了一些处理逻辑,对于数据请求来说,知道数据在请求还是比较有体验的。
<!-- filename: /src/api/request.js -->
// 引入axios
import axios from "axios";

// 创建axio例
const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 3000,
});

// 设置请求拦截器
request.interceptors.request.use(
(config) => {
  // ...
},
(error) => {
  // ...
}
);

// 设置相应拦截器
request.interceptors.response.use(
(response) => {
  // ...
},
(error) => {
  // ...
}
);

export default request;
 

模块创建

src/api/modules,新建user.js(每个.js文件表示一个模块的网络请求)。这里可以对请求URL单独做一个单独的配置,不过我感觉目前没必要就暂时不做。

<!-- filename: /src/plugins/api/modules/user.js -->
import request from "../index";

// 登录用户
export function login_user(data) {
  return request.post("/login", data);
},

// 获取用户信息
export function get_userinfo() {
  return request.post("/userinfo");
},

 

实例挂载

src\main.js文件中,引入axios实例并挂载在vue原型上;

<!-- filename: /src/main.js -->
import Vue from "vue";

// 网络请求
import request from "@/api";
Vue.prototype.$request = request;
 

请求使用

在这里,一个axios的模块对应一个vue组件或vuex模块,因而存在vue组件和vuex模块2种使用场景。

在vue组件内使用:

<!-- filename: /src/pages/app/table/index.vue -->
import { getTableData } from '@/api/modules/table';
export default {
  created() {
    getTableData().then((data)=>{
      ...
    }
  }
}
 

在vuex模块内使用:

<!-- filename: /src/store/modules/user.js -->
import { login_user } from "@/api/modules/user";

const actions = {
  // 登录用户
  login_user(context, userData) {
    return login_user(userData).then((data) => {
      ...
    });
  },
}
 

语言国际

首先说下国际化中的i18nl10n,这是两个单词。

  • i18n,即internationalization(国际化),开头的i和结尾的n隔着18个字母,简称i18n
  • l10n,即localization(本地化),开头的l和结尾的n隔着10个字母,简称l10n

我最早了解到这个概念,是在接触wordpress主题的时候。在wordpress,国际化使用___x之类的函数进行不同语言的翻译输出,功能在于提供多种语言的切换;而本地化则使用插件、POEDITOR之类的工具,将首选语言翻译成不同的语言,如将英语翻译成中文,每种语言代表一个.po文件。

在wordpress里,国际化和本地化是分开的,但在vue-i18n里两者集成在一起,这是非常方便的。但在前端项目里,有个问题需要考虑,就是组件内的语言和外部依赖中的语言能否协调一致。

但好在一般的大型开源项目都会提供国际化的功能,这里以vue-i18n和element-ui的集成为例,记录下如何集成。

vue-i18n安装

直接在终端里安装即可。

npm install vue-i18n --save
 

实例创建

/src/lang目录下创建index.js,这里主要是导入./modules目录下的所有.js语言包文件,创建并导出vue-i18n实例。对于第三方库的语言,目前涉及3种情况:

  • 有的第三方库提供修改语言的方法,如mavon-editor;
  • 有的第三方库内置语言包(需要自己配置),如element-ui;
  • 有的第三方库没有提供语言接口或需要根据API返回数据修改语言,如echarts。

对于情况1,直接在组件内进行调用即可;对于情况2,可根据文档提示操作;对于情况3,目前将语言包放置在./modules下,数据返回时重新更新组件。

<!-- filename: src/lang/index.js-->
import Vue from "vue";
import VueI18n from "vue-i18n";
// element-ui的语言包
import locale_element_en from "element-ui/lib/locale/lang/en";
import locale_element_zh from "element-ui/lib/locale/lang/zh-CN";
// 本项目的语言包
import locale_zh from "./modules/zh";
import locale_en from "./modules/en";

Vue.use(VueI18n);

// 语言包合并
const messages = {
  zh: {
    ...locale_element_zh,
    ...locale_zh,
  },
  en: {
    ...locale_element_en,
    ...locale_en,
  },
};
// 创建实例
const lang = new VueI18n({
  locale: localStorage.getItem("language") || "zh",
  messages,
});

export default lang;
 

模块创建

/src/lang/modules目录,新建zh.js文件(每个.js文件代表一种语言)。该模块返回一个对象,建议使用2级结构:即1级表示vue组件或系统模块,2级表示具体的内容。示例如下,login表示vue登录组件,该对象下的属性表示具体内容,实际使用的时候可通过$t('login.title')方式调用。

<!-- filename: /src/lang/modules/zh.js -->
export default {
  login: {
    title: "账号登录",
    description: "Hello!,欢迎您登录后台管理系统!",
    userplaceholder: "请输入用户名",
    passplaceholder: "请输入密码",
    uservalidator: "用户名不能为空,请检查用户名!",
    passvalidator: "密码不能为空,请检查面慢",
    rememberme: "记住密码",
    forget: "忘记密码?",
    submit: "提交",
    ways: "其他登录方式",
    bymobile: "手机登录",
    byemail: "邮箱登录",
    loginedtip: "登录成功,即将跳转首页",
  },
};
 

引入和安装

element-ui的语言包除了在vue-i18n引入外,element-ui也需要做一些处理。因为我对element-ui做了单独封装,目前使用的是下面的方式1,如果你不想做单独封装的话,直接使用方式2即可。

方式一(目前使用):

<!-- filename: /src/plugins/element-ui.js -->
import Vue from "vue";
import i18n from "@/lang";

Vue.use(ElementUI, {
  i18n: (key, value) => i18n.t(key, value),
});
 
<!-- filename: /src/main.js -->
import i18n from "@/lang";

new Vue({
  i18n,
})
 

方式二(常规方法):

<!-- filename: /src/main.js -->
import i18n from "@/lang";

Vue.use(ElementUI, {
  i18n: (key, value) => i18n.t(key, value),
});

new Vue({
  i18n,
})
 

注意:无论使用哪种方法,都需要在new Vue的选项里传递i18n(vue-i18n实例)。vue-i18n需要读取这个参数,将$i18n属性和$t方法挂载到vue上面。

使用和修改语言

完成以上步骤后,就可以在vue里面正常使用了。虽然调用API是相同的,在vue组件里和在非vue组件里的调用姿势略有差别:

  • .vue文件中,语言翻译使用this.$t()方法,切换语言修改this.$i18n.locale属性。
  • 在其他文件中,语言翻译使用i18n.t()方法,切换语言修改i18n.locale属性

姿势一(在vue组件里使用):

<template>
 <div :title="$t('login.title')">
  // 在template里使用
  {{ $t('login.title') }} 
 </div>
</template>

<script>
export default {
  methods: {
    xxx(lang) {
      // 在methods里使用
      this.$i18n.locale = lang
    }
  }
};
</script>
 

姿势二(在非vue组件里使用):

<!-- Filename: /src/store/modules/user.js -->
import i18n from "@/lang";

const actions = {
  // 切换语言
  switch_language(context, language) {
    context.commit("set_language", language);
    i18n.locale = language;
    return Promise.resolve();
  },
}
 

路由管理

普通路由不怎么需要封装,但涉及权限控制及菜单自动生成等方面,就需要对路由进行一番改造。关于路由权限和菜单自动生成,这里暂且不谈,后面会单独说明。

vue-router安装

如果项目内还没有自动安装vue-router,需要先安装下。

npm install vue-router --save
 

实例创建

按照习惯,直接在/src/router下新建index.js文件,这一块涉及的内容较多,略去权限部分的逻辑,贴出主要逻辑代码。其中,主要涉及以下几个方面:

  • 使用.env变量配置路由模式;
  • 使用nprogress进行路由过渡;
  • 在后置守卫中更新网站标题;
import Vue from "vue";
import vueRouter from "vue-router";
import normalRoutes from "./modules/normal-routes";
import Nprogress from "@/plugins/nprogress";
import store from "@/store";
import { message } from "@/plugins/element-ui";
import i18n from "@/lang";

Vue.use(vueRouter);

// 创建实例
const router = new vueRouter({
  // 开发环境使用history模式,生产环境使用hash模式
  mode: process.env.VUE_APP_ROUTER_MODE,
  // 权限路由是嵌套在普通路由中的
  routes: normalRoutes,
});

// 全局前置守卫
router.beforeEach(function(to, from, next) {
  // 开始进度条
  Nprogress.start();

  // 权限处理代码...
});

// 全局后置守卫
router.afterEach((to) => {
  // 结束进度条
  Nprogress.done();
  // 如果有标题则更新标题
  if (to.meta && to.meta.title) {
    // 更新title
    document.title =
      i18n.t("router." + to.meta.title) + " - " + i18n.t("system.description");
  } else {
    document.title =
      i18n.t("system.title") + " - " + i18n.t("system.description");
  }
});

export default router;
 

模块创建

src/router/modules目录下,创建2个文件:normal-routes.jsauthed-routes.js文件,前者保存不需要访问权限的路由,后者保存需要权限的路由。

<!-- filename: /src/router/modules/normal-routes.js -->
export default [
  {
    path: "/login",
    name: "login",
    meta: { title: "login", icon: "xxx" },
    component: () =>
      import(/* webpackChunkName: "chunk-login" */ "@/pages/login"),
  },
  // ...
]
 

这里需要注意下import(),这是webpack提供的异步加载函数,如果将所有代码都打包到app chunk里会造成首屏渲染问题;同时还需要注意该函数内的注释,这是webpack提供的异步加载chunk注释,默认情况下每个异步加载的文件会独立打包成一个独立chunk,使用该功能,可将多个异步加载的文件打包到同一个chunk里面,该注释可以指定chunk的名字,这样不仅达到请求优化的目的,还便于文件识别和分析。

实例引入

/src/main.js中引入router。这里删除/src/app.vue,直接使用rendrer方法生成路由视图组件,这是1级路由视图,主要用于渲染applogin404等组件。

<!-- filename: /src/main.js -->
impot router from '@/router'

new Vue({
  router,
  render: (h) => h("div", { attrs: { id: "app" } },  [h("router-view")], 1),
})
 

路由视图

除了/src/main.js中的1级路由,/src/pages/layout/index.vue中的路由视图是承载整个应用的2级路由,包括hometablemarkdownadmin-page等组件。

<!-- filename: /src/pages/layout/index.vue -->
<template>
  ...
  <!-- 渐变效果,用以配合进度条 -->
  <transition name="fade-transform" mode="out-in">
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  </transition>
  ...
</template>
 

上面使用了2个内置组件:<transition><keep-alive>,前者用于配合nprogess插件进行视图过渡;后者用于缓存组件,目前组件不多暂不做限制,两者目的都在于提升用户体验。

<!-- filename: /src/router/index.js -->
import Nprogress from "@/plugins/nprogress";

// 全局前置守卫
router.beforeEach(function(to, from, next) {
  // 开始进度条
  Nprogress.start();
}

// 全局后置守卫
router.afterEach((to) => {
  // 结束进度条
  Nprogress.done();
}
 

静态资源

这里的静态资源包括图片、字体和样式文件等,目前我将所有的静态资源统一放置在/src/assets目录下。对于图片文件,我个人习惯放置于对应组件的目录下。这里主要说说样式的处理,以及图标字体的引入和更新等问题。

样式处理

CSS三大流行预处理器(less、scss和stylus),各有各的好处,这里不作讨论。这里说下我在这里使用scss的2个主要原因:

  • scss与css比较贴合,配合emmet体验良好(stylus偶尔遇过问题);
  • 方便修改element-ui的颜色变量(element-ui使用scss)

sass和scss是2代不同的版本,前者使用缩进语法,后者使用CSS语法,早前的sass是Ruby写的,后来移植到了nodejs。在nodejs,sass和scss是在同一个package里面的,通过.sass.scss后缀判断其版本。而这个package之前使用的是node-sass,这需要安装ruby。后面使用sass这个package,这是使用纯nodejs书写的package。

关于CSS样式和目录结构的处理,可以参考下这篇文章。

安装scss

这里需要注意的是scss和scss-loader之间的版本关系,最新的scss和scss-loader不一定匹配,强行安装会报错。目前,我是使用的scss为v1.26.2版本,sass-loader为v8.0.2版本,

npm install sass@1.26.2 sass-loader@8.0.2 --save-dev
 

样式类名

对于CSS样式类名的命名,目前在非完全地使用BEM命名规则。对于模块化,可以使用BEM配合文件分类,也可以使用官方给出的moduledscoped任意1种方式。这里使用的是scoped方式,使用方式也比较简单,在.vue组件里的style标签里加上scoped属性即可。

<!-- filetype: .vue -->
<style scoped>
  ...
</style>
 

目录结构

除去vue组件内的样式,我们还需要写一些其他的样式,涉及样式重置、通用样式、第三方样式重写等情况。以下是一个示例,更多可根据自己需要添加。

<!-- directory: /src/assets/styles -->
├── base-normalize.scss        # normalize.css库
├── base-typegraphy.scss       # 排版样式
├── base-variables             # 颜色变量
├── index.scss                 # 主文件
├── layout-app.scss            # 布局样式
├── layout-scrollbar.scss      # 滚动条样式
├── vendor-driver.scss         # driver.js库样式重写
├── vendor-element.scss        # element-ui库样式重写
└── vendor-nprogress.scss      # nprogress库样式重写
 

关于第三方样式,这里重写的主要原因是,配合webpack-theme-color-replacer进行主题换肤,尽量保证页面样式的一致性。

全局引入

对于颜色变量和重置等样式,需要提前全局插入,这里可以利用vue-cli提供的vue.config.js进行提前注入。需要注意的是,下面scss对象内的的属性分2种情况(官方文档):

  • sass-loader<v8,使用additionalData属性名;
  • sass-loader>v8,使用prependData属性名(当前使用版本:v8.0.2)。
<!-- filename: /vue.config.js -->
module.exports = {
  // css相关配置
  css: {
    loaderOptions: {
      scss: {
        prependData: `@import "~@/assets/styles/index.scss";`,
      },
    },
  },
}
 

图标字体

element-ui内置不少图标字体,但不一定满足实际需求,这时可以使用iconfont、fontawesome等字体库提供的图标。

引入iconfotn图标

对于iconfont上面的图标,可以下载到本地,也可以在线引用。这里直接在线引用,因为项目一开始需要的图标尚未确定,后面更新用这种方式非常方便。方式也非常简单,直接在/public/index.html文件中进行引入即可。

<!-- filename: public/index.html -->
<html lang="">
  <head>
    <!-- 注意这里 -->
    <link rel="stylesheet" href="//at.alicdn.com/t/font_2449987_nge8aeefn5.css">
  </head>
</html>
 

提前处理

不知道有没有人注意到,element-ui和iconfont的图标调用姿势是略有差别的:

  • 在element-ui里,通常以el-icon-xxx(1个类名)的形式调用图标;
  • 在iconfont里,通常以iconfont icon-xxx(2个类名)的形式调用图标。

为啥不一样?这里其实是element-ui提前做了处理,使用属性选择器来巧妙地规避多写这个问题,下面直接贴改造好的代码。

<!-- filename: src/assets/fonts/iconfont.scss -->
[class*=" icon-"], [class^=icon-] {
  font-family: iconfont!important;
  font-style: normal;
  font-weight: 400;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  vertical-align: baseline;
  display: inline-block;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale
}
 

注意:这里使用了2个选择器,主要是应对<element class="icon-xx a-class"><element class="a-class icon-xx">这2种情况。

主题切换

在vue-element-admin中,提到有2种方案对element-ui的主题色进行切换:

  • 使用颜色选择器,对品牌色进行正则替换;
  • 使用theme-chalk生成多套主题,切换主题时动态加载。

不过,以上2种方式功能都有限,且均未涉及vue组件内颜色的替换。经过一番探索,发现一款插件-webpack-theme-color-replacer非常好用:不仅可以满足vue组件的颜色替换,还可以满足第三方库的颜色替换(包括element-ui),同时还为element-ui提供相关支持。

这个插件的另一个优点在于,只抽离涉及特定颜色的样式,意味着导出体积很小。完整的element-ui样式有200k左右,而使用插件只有几十K。关于插件原理,可以点击这里查看。

插件安装

这里不需要安装在生产依赖下,直接安装在开发依赖下即可;

npm i webpack-theme-color-replacer --save-dev
 

主题配置

本项目在/src/assets/styles/base-variables.scss中已经预设一套颜色变量,配置文件中将以它作为默认主题进行配置。

<!-- /src/config/theme.config.js -->
const forElementUI = require("webpack-theme-color-replacer/forElementUI");

// 两个主题色 这里使用commonjs语法,因为在vue.config.js不能使用es module语法
module.exports = {
  default: [
    // element-ui主题色
    ...forElementUI.getElementUISeries("#10c599"),
    // element-ui功能色-成功色
    ...forElementUI.getElementUISeries("#3c9"),
    // element-ui功能色-警报色
    ...forElementUI.getElementUISeries("#f90"),
    // element-ui功能色-危险色
    ...forElementUI.getElementUISeries("#f66"),
    // element-ui功能色-信息色
    ...forElementUI.getElementUISeries("#959595"),
    // 侧边栏背景色
    "#324554",
  ],
  skyblue: [
    // element-ui主题色
    ...forElementUI.getElementUISeries("#409eff"),
    // element-ui功能色-成功色
    ...forElementUI.getElementUISeries("#67C23A"),
    // element-ui功能色-警报色
    ...forElementUI.getElementUISeries("#E6A23C"),
    // element-ui功能色-危险色
    ...forElementUI.getElementUISeries("#F56C6C"),
    // element-ui功能色-信息色
    ...forElementUI.getElementUISeries("#909399"),
    // 侧边栏背景色
    "#001529",
  ],
};
 

注意:颜色的16进制能简写尽量简写,例如#3c9不要写成#33cc99,否则会造成开发环境和生产环境下,样式不一致的问题。

功能重写

在实际使用过程中,我发现element-ui的功能色是不正常的。F12一番后发现是插件changeSelector函数的问题,于是对其重新封装,问题解决,但目前尚不明确带来的副作用。

<!-- filename: /src/helper/change-selector.js -->
function changeSelector(selector, util) {
  switch (selector) {
    case ".el-button:active":
    case ".el-button:focus,.el-button:hover":
      return util.changeEach(selector,".el-button--default:not(.is-plainnot(.el-button--primary)" );
    case ".el-button.is-active,.el-button.is-plain:active":
      // 注意这里
      return "useless";
    case ".el-button.is-plain:active":
    case ".el-button.is-plain:focus,.el-button.is-plain:hover":
      return util.changeEach(selector, ".el-button--default");
    case ".el-pagination button:hover":
      return selector + ":not(:disabled)";
    case ".el-pagination.is-background .el-pager li:not(.disabled):hover":
      return selector + ":not(.active)";
    case ".el-tag":
      return selector + ":not(.el-tag--dark)";
    default:
      return selector;
  }
}
module.exports = changeSelector;
 

vue配置

因为使用的是vue-cli4.x版本,所以直接在vue.config.js中配置到plugins即可。注意,vue.config.js不能使用import语句,需要使用require语句导入插件。此外,该插件对element-ui主题色提供了增强功能

<!-- filename: /vue.config.js -->
const webpackThemeColorRplacer = require("webpack-theme-color-replacer");
const themeConfig = require("./src/config/theme.config.js");
const changeSelector = require("./src/helper/change-selector");

module.exports = {
  // 这里我用的是链式配置,使用configureWebpack也是可以的
  chainWebpack: (config) => {
     config.plugin("webpackThemeColorRplacer").use(webpackThemeColorRplacer, [
      {
        matchColors: themeConfig["default"],
        fileName: "css/chunk-theme-[contenthash:8].css",
        // 使用element-ui必备,否则plain和普通按钮样式会有bug
        changeSelector: changeSelector,
        injectCss: false,
        isJsUgly: true,
      },
    ]);
  }
}
 

功能封装

以上完成css颜色样式的功能,接下来写颜色主题切换的函数。

<!-- filename: src/plugins/theme-replacer.js -->
import client from "webpack-theme-color-replacer/client";
import store from "@/store";
const themeConfig = require("@/config/theme.config.js");

// 替换主题色
export function replaceThemeColors(color) {
  // 选项
  const options = { newColors: themeConfig[color] };
  return client.changer.changeColor(options, Promise);
}

// 初始化主题色
export function initThemeColor() {
  // 获取当前主题
  const currentTheme = store.state.default.theme;
  if (currentTheme) {
    document.body.style.display = "none";
    // 替换颜色
    replaceThemeColors(currentTheme).finally(() => {
      document.body.style.display = "";
    });
  }
}

 

初始化和使用

首先在/src/main.js进行每次初始化,当前主题会保存在localStorage中,初始化时会读取该值进行初始化。

<!-- filename: src/main.js -->
import { initThemeColor } from "@/plugins/theme-replacer";

// 初始化主题
initThemeColor();
 

函数replaceThemeColors一般在vue组件内调用,但考虑到其他因素,这里将其与vuex进行结合,可根据个人习惯调用。

<!-- filename: src/store/modules/default.js -->
import { replaceThemeColors } from '@/plugins/theme-replacer';

const actions = {
  // 切换主题
  switch_theme(context, theme) {
    // 切换主题的主函数,传入的theme为string类型
    return replaceThemeColors(theme).then(() => {
      // 处理代码...
    });
  },
}
 

权限控制

项目里的权限控制,并不是基于角色(role)的访问控制,而是基于权限(capability或permission)。主要是对权限的颗粒控制更友好,且对于页面内的按钮控制同样适用。

具体做法是,在/src/config/role.config.js里保存所有权限配置,在vue-router的meta字段里指定capability(或permission)属性,

权限配置

/src/config/role.config.js会保存角色(role)及其权限(capability)配置,后续会根据这个进行权限的判断。

<!-- filename: /src/config/role.config.js -->
// 用户权限配置
export default {
  admin: {
    // 路由权限
    visitAdminPage: true,
    visitEditorPage: false,
    // 指令权限
    publishPost: true,
    editPost: true,
    deletePost: true,
    readPost: true,
  },
  editor: {
    // 路由权限
    visitAdminPage: false,
    visitEditorPage: true,
    // 指令权限
    publishPost: false,
    editPost: true,
    deletePost: false,
    readPost: true,
  },
};
 

权限函数

用户登陆后会将角色保存在vuexsessionStorage中,通过读取role.config.js中对应的权限配置,与访问目标需要的权限对比,这样就实现了权限判断。这里将其单独封装成1个助手函数,方便后续调用。

import store from "@/store";
import roleConfig from "@/config/role.config.js";

function hasCapability(capability) {
  // 是否有权限(boolean)
  let isShowedInMenu = false;
  // 如果需要权限则判断一下权限
  if (capability) {
    // 遍历当前用户角色
    store.state.user.role.forEach((userRole) => {
      // 获取角色的权限表
      const roleCapabilities = roleConfig[userRole];
      // 如果对应的权限在用户权限表上为true则返回true
      if (
        roleCapabilities[capability] &&
        roleCapabilities[capability] === true
      ) {
        isShowedInMenu = true;
      }
    });
  } else {
    // 没有声明权限的话默认为公共权限,任何角色都可访问
    isShowedInMenu = true;
  }
  return isShowedInMenu;
}

export default hasCapability;
 

页面权限

src/router/modules目录下,创建2个文件:normal-routes.jsauthed-routes.js文件,前者保存不需要访问权限的路由,后者保存需要权限的路由。

<!-- filename: /src/router/modules/authed-routes.js -->
export default [
  {
    path: "/admin-page",
    name: "adminPage",
    meta: {
      title: "adminCapability",
      icon: "icon-cap",
      capability: "visitAdminPage",
    },
    component: () => import("@/pages/app/capability"),
  },
}
 

与普通路由相比,权限路由在meta属性多了capability属性,表示访问该路由需要的权限。在生成菜单时,判断权限决定是否渲染。

<!-- filename: /src/pages/layout/components/nav-item/index.vue -->
<template>
  ...
  <!-- 情况[1]:单个路由 -->
  <el-menu-item v-if="!route.children && !route.hidden && !route.meta.link && isCapabilited(route.meta.capability)" :key="route.path" :index="route.path" class="ment-item">
    <!-- 路由图标 -->
    <i :class="route.meta.icon"></i>
    <!-- 路由标题 -->
    <span slot="title">{{ $t('router.'+route.meta.title) }}</span>
  </el-menu-item>
  ...
</template>

<script>
export default {
  methods: {
    // 判断当前路由是否需要权限,无权限则不显示在菜单上
    isCapabilited(capability) {
      return hasCapability(capability)
    }
  }
}
</script>
 

对于页面路由,需要在全局前置守卫进行处理。

<!-- filename: /src/router/index.js -->
// 全局前置守卫
router.beforeEach(function(to, from, next) {
  // 获取登录令牌(token)
  let token = store.state.user.token;

  // 用户已登录
  if (token) {
    // 如果访问的是login,重定向回首页
    if (to.path === "/login") {
      // 弹窗提示已登录并返回首页
      message({
        message: i18n.t("router.loginedinfo"),
        type: "success",
        duration: 2000,
        onClose() {
          next({ path: "/" });
        },
      });
    } else {
      // 检查是否已获取用户角色等信息
      const roled = store.state.user.role && store.state.user.role.length !== 0;
      if (!roled) {
        // 获取用户信息并跳转路由
        store
          .dispatch("user/get_userinfo")
          .then(() => {
            next();
          })
          .catch(() => {
            // 获取失败的话返回登录页
            store.commit("user/remove_token");
            Nprogress.done();
            next("/login");
          });
      } else {
        // 判断是否有访问该页面的权限
        if (hasCapability(to.meta.capability)) {
          next();
        } else {
          message({
            type: "error",
            message: i18n.t("router.hasNoCapability"),
            duration: 2000,
            onClose() {
              next("/");
            },
          });
        }
      }
    }
  } else {
    // 是否在白名单内
    if (whiteList.includes(to.path)) {
      next();
    } else {
      next({ path: "/login" });
    }
  }
});
 

指令权限

对于页面内的按钮权限,统一通过vue指令进行判断,这里封装1个权限指令。

<!-- filename: /src/config/v-cap.js -->
import hasCapability from "@/helper/hasCapability";

const cap = {
  inserted(el, bind) {
    // 获取传入的权限值
    const capability = bind.value;
    // 如果为空直接返回
    if (!capability) return;
    const capabilitied = hasCapability(capability);
    // 如果没有权限,直接移除
    if (!capabilitied) {
      el.parentNode && el.parentNode.removeChild(el);
    }
  },
};

export default cap;
 

然后在vue组件内,传入相对应的权限即可根据权限决定是否渲染元素。

<!-- filename: /src/pages/app/capability/index.vue -->
<template>
  ...
  <el-button type="primary" v-cap="'publishPost'">{{ $t('capability.publishPost') }}</el-button>
  ...
</template>
 

导航菜单

这里的导航菜单,指将route自动挂载到el-menu中,避免手动挂载。这里的route可分为3种情况:

  • 普通路由组件,即path指向.vue组件;
  • 嵌套路由,即当前路由存在children子路由;
  • 外部链接,即path指向外部链接(例如https://www.juetan.cn)。

路由容器

这里的实现思路是,使用<el-menu>作为外部容器,定义<vue-navitem>组件作为递归组件,处理上面的情况。这里主要做了以下2件事:

  • 折叠变量collapsed使用vuex,这是因为这个值会在其他组件中用到(如nav-top组件)。
  • 添加主题切换的相关支持,这里用的是<el-menu>提供的属性接口进行处理,用纯css也是可以的,不过比较麻烦。
<!-- filename: /src/pages/layout/components/nav-menu.vue  -->
<template>
  <el-scrollbar id="navmenu">
    <!-- 如需路由正确工作,需添加router属性 -->
    <el-menu 
      router
      :default-active="$route.path" 
      :collapse="collapsed"
      :unique-opened="false"
      :collapse-transition="false"
      text-color="rgba(255,255,255,.65)" 
      :active-text-color="activeColor" 
      :background-color="backgroundColor"
      class="aside" 
    >
      <!-- 多少层routes就多少层router-view -->
      <vue-navitem :data="routes"></vue-navitem>
    </el-menu>
  </el-scrollbar>
</template>

<script>
import vueNavitem from '../nav-item'
import routes from '@/router/modules/normal-routes'

export default {
  name: "navmenu",
  data() {
    return {
      routes: null
    }
  },
  computed: {
    collapsed() {
      return this.$store.state.default.collapsed
    },
    backgroundColor() {
      return this.$store.state.default.theme==='skyblue'?'#001529':'#324554'
    },
    activeColor() {
      return this.$store.state.default.theme==='skyblue'?'#fff':'#ffd04b'
    }
  },
  created() {
    this.routes = routes[0].children
  },
  components: {
    vueNavitem
  }
}
</script>

<style lang="scss" scoped>
  .aside.el-menu {
    border-right: none;
  }
  ::v-deep .el-menu-item:not(.is-active):hover {
    color: $--color-white!important;
    i {
      color: $--color-white!important;
    }
  }
</style>
 

上面,部分内容为主题切换的功能,对于element-ui组件的样式渗透需要使用::v-deep作前缀。

路由组件

这是个递归组件,遇到子路由的情况下,会调用自身组件,因此需要指定name属性。此外,这里使用hasCapability进行权限判断,实现路由的可视和挂载,这里只是个简单的实现,没有考虑更多复杂的情况。

<!-- filename: /src/pages/layout/components/nav-item.vue  -->
<template>
  <div class="navitem">
    <!-- 因为用的是template,所以key放在里面 -->
    <template v-for="route in data">
      <!-- 情况[1]:单个路由 -->
      <el-menu-item v-if="!route.children && !route.hidden && !route.meta.link && isCapabilited(route.meta.capability)" :key="route.path" :index="route.path" class="ment-item">
        <i :class="route.meta.icon"></i>
        <span slot="title">{{ $t('router.'+route.meta.title) }}</span>
      </el-menu-item>

      <!-- 情况[2]: 外部链接 -->
      <a v-else-if="!route.children && !route.hidden && route.meta.link" :href="route.path" :key="route.path" target="_blank">
        <el-menu-item  class="ment-item">
            <i :class="route.meta.icon"></i>
            <span slot="title">{{ $t('router.'+route.meta.title) }}</span>
        </el-menu-item>
      </a>

      <!-- 情况[3]: 嵌套路由 --> 
      <el-submenu v-else-if="route.children" ref="subMenu" :key="route.path" :index="route.path" popper-append-to-body>
        <template slot="title">
          <i :class="route.meta.icon"></i>
          <span slot="title">{{ $t('router.'+route.meta.title) }}</span>
        </template>
        <nav-item :data="route.children"/> 
      </el-submenu>
    </template>
  </div>
</template>

<script>
import hasCapability from '@/helper/hasCapability';

export default {
  name: "navItem",
  props:{
    data: {
      type: Array,
      required: true
    }
  },
  methods: {
    // 判断当前路由是否需要权限,无权限则不显示在菜单上
    isCapabilited(capability) {
      return hasCapability(capability)
    }
  }
}
</script>

<style lang="scss">
  // 样式处理
</style>
 

以上,逻辑基本写完,上面css样式没贴出来,主要是想说2个样式问题:

  1. 使用非element-ui的图标,需要做一点样式处理(如下)。注意:上面<style>标签没有加scoped属性,也可以使用::v-deep进行样式渗透。
.el-menu-item [class^=icon-],.el-submenu [class^=icon-] {
    margin-right: 5px;
    width: 24px;
    text-align: center;
    font-size: 18px;
    vertical-align: middle;
}
 
  1. 嵌套路由在折叠的时候,会将标题显露出来,这是由于el-menu子组件应为el-submenu、el-menu-item或el-menu-item-group。我的子组件最外层为div,有路由嵌套时会出现样式问题,此处添加处理样式。
// 隐藏标题
.el-menu--collapse  .el-submenu__title span{
display: none;
}
// 隐藏小箭头
.el-menu--collapse  .el-submenu__title .el-submenu__icon-arrow{
display: none;
}
 

体积优化

对于某些库来说(如element-ui、echarts等),完全打包进去是没必要的,此时可以通过按需加载进行体积优化。

体积分析

analyzer安装

安装到开发依赖下即可;

npm install analyzer-webpack-plugin --save-dev
 

webpack配置

该包为webpack插件,需要在webpack下配置,对于vue-cli4,可以在/vue.config.js中进行配置。这里需要注意一点:如果有使用Github Action之类的需求,需要将该插件取消掉,否则会导致构建挂起。

<!-- filename: /vue.config.js -->
module.exports = {
    // 链式配置webpack
    chainWebpack: (config) => {
    config.plugin("BundleAnalyzerPlugin")
    .use(require("webpack-bundle-analyzer").BundleAnalyzerPlugin, [
        {
        //  在默认浏览器中自动打开
        openAnalyzer: false,
        },
    ]);
    }
}
 

Gzip压缩

关于Gzip压缩,这里分2种:静态Gzip和动态Gzip压缩。静态Gzip压缩,指Nginx服务器直接读取网站目录下的.gz文件,然后返回给浏览器;动态Gzip压缩,指Nginx服务器读取网站目录下的.js.css.html文件,将其压缩成.gz文件,再返回给浏览器。

对于Github Pages,使用的是动态Gzip压缩,即使将文件压缩成.gz文件也不会读取。但如果要部署到自己服务器,使用静态Gzip压缩还是很有必要的,毕竟牺牲一点内存换返回速度还是很划算的。

compression安装

如果安装最新版本出错,可以尝试回退到之前的某个版本。

// 安装最新的7.1.2版本会报错
npm i compression-webpack-plugin@5.0.1 -D
 

webpack配置

一般在生产环境下使用,

<!-- filename: /vue.config.js -->
module.exports = {
    // 链式配置webpack
    config.plugin("compression").use(ComporessionPlugin, [
        {
          // 正则匹配文件后缀
          test: /.js$|.html$|.css$/,
          // 对超过10KB的文件进行压缩
          threshold: 1,
          // 不删除源文件,主要是兼容不支持gzip的服务器
          deleteOriginalAssets: false,
        },
    ]);
}
 

异步加载

一次性加载完所有异步组件是没必要的,此时可以通过import()函数来进行异步加载:它会将其单独打包成一个chunk,在使用的时候进行请求。

在路由中使用

注释中的webpackChunkName,指打包后的Chunk名字。如果有多个小的路由组件,又不想每个都单独请求,此时可以通过指定webpackChunkName,将多个路由组件打包到同一个chunk中。

component: () => import(/* webpackChunkName: "chunk-home" */ "@/pages/app/home"),
 

最后

以上内容为后面总结,可能存在错误和不足,欢迎指出!如果你觉得有用的话可以帮忙点个star(项目地址),另外借此求职一份前端工作,本人19年普通本科毕业,在一家外企子公司做了一年半(非前端,目前已离职),目前想转前端。非零基础,从大学就开始接触,技术栈vue(毕设就用的vue),基础还可以,具备一定的英语读写能力和自学能力,详情可看我博客:www.juetan.cn/65

(0)
上一篇 2021年5月27日 上午12:08
下一篇 2021年5月27日 上午12:22

相关推荐

发表评论

登录后才能评论