引言

这个项目是我第二次做了,第一次做的时候好多地方不是很懂,而现在学习了基础知识后,回过来再做一次,发现原来不是很懂的地方也弄明白了,然后又在B站找了项目实战撸了一把,想通过这篇文章,对其中的模块实现做一下总结,同时希望能够加深对项目的理解,不断发掘其中的亮点。如果你觉得本文还行,请毫不留情的点赞!

element-ui篇

element-ui 官方中文文档传送门

el-form 组件问题

el-form 组件默认是 content-box,我们设置其如下属性时,就会出现长度比我们想象中长一点的情况。

.login_form{
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
}


解决办法如下,直接设置 box-sizingborder-box 即可。

.login_form{
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
}

使用 iconfont 给输入框添加图标

main.js 中导入字体图标

然后在 el-input 组件中通过 prefix-icon 进行相关图标引用

最终效果图如下:

如何进行表单验证

Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Itemprop 属性设置为需校验的字段名即可。

通过 rules 属性传入约定的验证规则

Form-Itemprop 属性设置为需校验的字段名

如何重置表单

element-ui 对表单提供了如下方法 resetFields,我们只需要获取表单对象数据即可重置我们的表单。

获取表单对象方式如下:

直接在表单处添加 ref属性,如下 loginFormRef,此引用即为我们表单实例。

然后,在我们重置按钮处,通过 @click 给按钮绑定一个事件

下一步,添加方法,我们可以打印一下 this

不难发现,此时的 this 指向的就是我们当前登录组件 login.vue的实例对象。


观察上图,我们看到有一个 $refs 属性,这个就是我们的表单实例。

此时,我们的方法就可以改写成如下方式:

最后,我们点击重置按钮后,会发现输入框并不是完全清空了,原因是data数据里面一开始我们设置了默认值,因此resetFields()函数就是清空为默认值。

登录预校验

在我们点击登录按钮,不应该直接发起网络请求,而是应该对表单进行预校验,检验成功才会发送请求。


拿到表单引用对象,即可进行对表单的校验。

首先,给登录按钮通过 @click 来绑定一个事件

下一步,在 methods 中添加 login 方法,validate函数参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise

此时,我们点击登录按钮,查看是否进行了预校验:


此时,按照之前的校验规则,是存在问题的。所以返回 false。我们来一个正确示例吧:

配置弹框提示组件

首先,在 element.js 中导入弹框提示组件,但是它的配置与其它组件不一样,它需要进行全局挂载。

全局导入后,我们就可以在我们的登录组件 login.vue 中通过 this来访问弹框提示组件了。

测试一下,输入 admin123456,此时就会提示输入成功。

每一个组件都是一个类名

每一个组件都是一个类名,我们可以直接增加效果属性。

el-menu 默认会有一个 border-right


这样会导致我们的菜单栏右边会有一个若隐若现的线条凸起

解决办法如下,直接将 el-menuborder-right 设置为 none 即可。

表格展开页

只需要将 el-table-columntype 属性设为 expand 即能将表格进行展开。

实现效果如下:

使用树形控件 el-tree

对于分配权限一栏,我们需要如下效果:


相关配置属性:


其中 data 为我们的数据源,props 为我们在 tree组件显示的文本内容。通过设置 node-key可以让每个树节点作为唯一标识的属性,整棵树是唯一的;通过设置 default-expand-all 来默认展开我们的所有节点,不需要每次都让用户去打开,体验感好;通过设置 default-checked-keys勾选我们已有的权限。

最后,我们通过递归的方式,判断当前 node 节点是否存在 children属性,如果不存在的话,就包含了三级权限,然后添加到我们的数组中,然后通过 :default-checked-keys="defKeys" 来渲染选中当前已有的权限。

此外,当我们关闭对话框时还需要监听一下关闭事件,不然每次数组都会保存原来的权限数据,导致一些问题。因此,监听 close 事件后,每次对话框关闭时,都将数组进行清空。

获取选中节点的keys:

首先,给我们对话框的确定按钮绑定一个事件。



主要使用 tree 组件提供的两个方法 getCheckedKeysgetHalfCheckedKeys来分别返回目前被选中的节点的 key 所组成的数组和目前半选中的节点的 key 所组成的数组

使用动态编辑标签

实现效果如下:


通过 v-ifboolean值来动态变化是否显示文本框还是 button 按钮。通过 v-model 双向绑定来实现文本框内容的监听。

由于每一行需要设置文本框的显示与隐藏,而且数据是共享的。我们需要通过 scope来获取每一行的值来进行绑定,不然的话,就会导致某个文本框显示了,其它文本框也跟着显示,并且数据是共用的,都会同步变化。而设置 scope后,我们就能实现对一行数据进行更新渲染了。

为了实现当我们点击按钮后,让本文看自动获取焦点。

通过 this.$refs.saveTagInput.$refs.input.focus() 来获取 inputdom 对象,接着调用 focus函数来获取焦点。

$nextTick 作用是当页面元素被重新渲染之后才会执行回调函数中的代码。如果不这样做的话,我们直接通过点击按钮的形式,将该行的 inputVisible 设置为了 true,那么此时直接调用 this.$refs.saveTagInput.$refs.input.focus() 是没办法获取焦点的,此时就会报错。因为我们的 dom节点还没有进行渲染,而先调用 $nextTick 就是为了等待 dom节点渲染之后,我们再获取 input,然后再调用 focus 函数来获取焦点。

步骤条的使用与美化

以下就是步骤条使用的核心代码,其中 active 绑定的是每一个 step 的下标,默认从 0 开始。其次,我们可以设置 aligin-center 属性来让我们的步骤条进行居中。el-step就是每一个步骤进度。

<!-- 步骤条区域 -->
<el-steps :space="200" :active="activeIndex" finish-status="success" align-center>
<el-step title="基本信息"></el-step>
<el-step title="商品参数"></el-step>
<el-step title="商品属性"></el-step>
<el-step title="商品图片"></el-step>
<el-step title="商品内容"></el-step>
<el-step title="完成"></el-step>
</el-steps>

更改原本样式,得到我们想要的效果:

.el-steps{
margin: 15px 0;
}
.el-step__title{
font-size: 13px;
}

最终实现效果如下:

阻止页签tabs切换

主要函数如下:

在我们的 tabs 标签页添加一个 before-leave 函数

然后在 methods 中定义,根据第一个标签页的逻辑来阻止标签页的切换。

Upload 上传组件

通过点击或者拖拽上传文件

<!-- action表示图片上传后台api地址 -->
<el-upload
:action="uploadURL"
:on-preview="handlePreview"
:on-remove="handleRemove"
list-type="picture"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>

data--->
// 上传图片的url
uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload'

配置如下之后,并不代表就真正上传了,此时,我们打开 Network 查看 Preview ,发现 msg 提示我们 无效 token,状态码为 400 ,这代表并没有上传成功。

这是因为此时上传控件没有使用 axiosajax 请求,而是组件内部自动封装了一个 ajax,因此也就不会调用下述请求拦截器了。

解决办法是 Upload 组件有一个 headers 属性,可以设置上传的请求头部。


第一步,在组件处添加 headers 属性

第二步,在 data 中定义 headersObj ,然后添加请求头对象。

操作完成后,可以看到提示消息 msg 显示上传成功,并且此时状态码也变为了 200

监听图片上传成功事件



监听图片删除事件

// 处理图片移除的操作
handleRemove(file) {
// 1.获取将要删除的图片的临时路径
const filePath = file.response.data.tmp_path
// 2.从pics数组中,找到这个图片对应的索引值
const idx = this.addForm.pics.findIndex(x => x.pic === filePath)
// 3.调用数组的 splice 方法,把图片信息对象,从pics数组中移除
this.addForm.pics.splice(idx, 1)
},

处理图片预览操作


图片预览窗可以用 el-dialog 组件来做,然后通过 on-preview 函数来处理图片预览的操作。

<!-- 图片预览 -->
<el-dialog title="图片预览" :visible.sync="previewVisable" width="50%">
<img :src="previewPath" class="previewImg">
</el-dialog>

通过函数来获取上传图片的真实 url ,然后监听图片预览窗口的打开即可。

// 处理图片预览的操作
handlePreview(file) {
// 获取图片显示的url
this.previewPath = file.response.data.url
this.previewVisable = true
}

最终效果如下:

使用Timeline 时间线

可视化地呈现时间流信息。

由于 vue-cli-plugin-element 最后更新时间是 2019年1月,而 element-uiTimeline 时间线更新是在 3月份,因此我们没有办法直接进行引用。因此,我们直接通过手动导入的方式。


然后,我们打开 element.js,进行导入

import Timeline from './timeline/index'
import TimelineItem from './timeline-item/index'

同时,我们也需要导入我们的 css 样式

<style lang="less" scoped>
@import '../../plugins/timeline/timeline.css';
@import '../../plugins/timeline-item/timeline-item.css';
.el-cascader{
width: 100%;
}
</style>
<!-- 展示物流进度的对话框 -->
<el-dialog title="物流进度" :visible.sync="progressVisible" width="45%" @close="addressDialogClosed">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in progressInfo"
:key="index"
:timestamp="activity.time">
{{activity.context}}
</el-timeline-item>
</el-timeline>
</el-dialog>

最终实现效果:

Vue篇

如何导入全局样式表

直接在 main.js将相关样式导入即可

// 导入全局样式表
import './assets/css/global.css'

如何重定向路由

router配置页中使用 redirect 属性

element-ui 如何按需导入

element.js 文件中,按照如下方式,进行按需导入即可。然后通过 Vue.use()方法进行引用。(优化点:也就是将组建导入都放在一起,如果组件过多的话,支持换行)

配置axios

main.js 文件内进行配置

那么,此时我们就可以通过 this访问登录组件原型上 $http 方法,来发起对后端的请求了。

保存token

将登录成功之后的 token,保存到客户端的 sessionStorage

  • 项目中除了登录之外的其它 api 接口,必须在登录之后才能访问
  • token 只应在当前网站打开期间生效,所以将 token 保存在 sessionStorage

通过编程式导航跳转到后台主页,路由地址是 /home

在我们进行登录请求时,我们不妨打印一下,请求过来的数据,如下,服务端那边会传过来一个 token 值。

下一步,我们在登录成功后,将服务端返回过来的 token值直接种植到 sessionStorage 中,然后通过编程式导航跳转到后台主页(/home),通过 $router.push()方法即可。

此时,我们打开浏览器 ,通过F12 查看Application属性,找到 sessionStorage 即可查看到我们刚刚种植的 token 值了。

路由导航守卫控制访问权限

如果用户没有登录,但是直接通过URL访问特定页面,需要重新导航到登录页面。

在之前的操作中,我们登录之后会种植一个 token 值,表示我们登录成功了,那么此时我们将 token 值去掉呢?

直接点击上述 clear all 按钮去掉之后,我们依旧是可以访问 /home的路由,没有登录也能访问我们的后台主页,这肯定不是我们想要的结果。

router/index.js 中进行相关配置,具体如下,调用路由实例 routerbeforeEach方法,即可挂载我们的路由导航守卫。
在这里插入图片描述

这里可能会有疑问,我们需要验证 token的正确性吗?
答案是不需要,因为前端只需要管要不要给用户呈现这个页面,而我们调用api时,后端会对我们的 token 值进行校验,前端不需要验证。

退出功能

基于 token 的方式实现退出比较简单,只需要销毁本地的 token 即可。这样,后续的请求就不会携带 token ,必须重新登录生成一个新的 token 之后才可以访问页面。

那么,我们仅需要为退出按钮绑定一个事件即可。

然后事件内容就是:先清空 token,然后跳转到我们的登录页面。

核心代码如下:

JSON格式的配置文件

在项目根目录创建名为 .prettierrc 的文件,增加如下代码:

{
"semi": false,
"singleQuote": true
}

semi设置 false去掉分号,singleQuote 设置 true使用单引号。

此外,还可以禁用 function 处空格报错,增加如下代码:

'space-before-function-paren': 0

通过axios请求拦截器添加 token

通过axios请求拦截器添加 token,保证拥有获取数据的权限。

原因是,后台那边除开登录的 api,其它都需要进行授权。

因此,我们可以利用 axiosinterceptors属性,其中有一个成员 request,此时我们可以通过 use 函数为请求拦截器挂载一个回调函数,只要向服务器端发送了一个 axios 请求,会优先调用 use 函数。

首先,在 main.js添加如下代码:

此时,打印 config,查看结果如下,发现我们headers 字段并没有一些授权字段。

此时,我们添加如下一行代码,通过 token 来进行授权。

此时,在Network就能查看到有一个 Authorization 字段了,但这里是为 null,因为我们进行的是登录请求,在登录期间,服务器是不会颁发令牌(token)。如果登录之后,调用其它 api 接口的话,就会颁发相应的token令牌。

左侧菜单栏

关于这一块的话,由于后台数据已经写好,我们只需要通过双层 for 循环即可渲染我们的菜单栏,注意点:

:index 只能绑定字符串,因此将后台请求过来的 id值与空字符串进行拼接达到此效果,解决了点击某个菜单栏导致其它菜单栏同时打开的问题。

最终实现效果如下:

左侧菜单栏图标设计

我们想要的效果如下:

如何设计?

首先,我们可以在 data 中定义一个对象,对应于每一个菜单选项的 id

然后通过 for 循环遍历每一个菜单选项的 id即可。

保持一个子菜单的展开


直接在侧边栏区域添加上述属性即可

让菜单栏展开与折叠

通过点击一个按钮,让侧边栏进行展开与折叠。通过 isCollapse 的值来动态变化侧边栏的宽度。




其中 cursor: pointer 设置是为了让鼠标放在折叠与展开处会有一个手指指向。
letter-spacing: 0.2em 是为了让 ||| 有一定间隔

实现首页路由重定向

我们想要在登录之后重定向到 /welcome 路径,于是需要添加子路由children 属性,然后设置 redirect重定向到我们的子路由。


然后在我们组件 Home.vue 的右侧主体区域,放置一个路由占位符 router-view 即可。

侧边栏路由改造


通过给侧边栏设置 router 属性为 true,我们就能开启 vue-router 路由模式,然后将我们二级菜单的 index 属性绑定我们的数据 path 即可。

在sessionStorage保存左侧菜单栏的激活状态

为了让我们点击二级菜单时,会有一个激活效果,而且是保持状态,我们需要将利用 element-ui 菜单栏相关 default-active 属性,来让菜单保持激活。

由于每个二级菜单都有一个路由链接,不妨将路由路径 path 存储在 sessionStorage 中,具体做法就是给二级菜单绑定一个事件:


每次触发点击事件,我们需要更新当前路由路径,才能让点击的那个按钮高亮。

然后在我们创建这个组件时,就获取当前的激活状态的路径即可。

查看 sessionStorage ,可以看到我们存储的路由路径。

slot插槽使用

项目需求:由于用户列表状态后台返回的是 true/false,无法进行渲染,而我们需要的是有一个Switch开关来控制我们的状态。添加一个template 模板后,此时就可以用 slot-scope 作用域插槽来获取我们数据列表中的每一行数据,具体如下:


最后,我们就可以得到如下效果图了:

通过switch开关更改用户状态


首先,在 switch 开关添加一个 change 事件,并且通过作用域插槽的形式,将该行数据作为参数传入,目的是为了后续的修改。

在函数内我们将传递过来的参数作为我们请求的参数,通过 put 方式修改我们的后台数据。

input输入框优化


通过增加 clearable 属性,我们的输入框就可以多一个 x,然后通过绑定 clear 事件,当我们进行清除 (即点击由 clearable 属性生成的清空按钮时触发)时,就会重新获取我们的用户列表,不再需要用户再次点击搜索按钮去获取了。

正则表达式验证邮箱和手机号码

// 验证邮箱的规则
var checkEmail = (rule, value, cb) => {
const regEmail = /^([a-zA-Z]|[0-9])(\w|-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/
if (regEmail.test(value)) {
// 合法的邮箱
return cb()
}
cb(new Error('请输入合法的邮箱'))
}
// 验证手机号码的规则
var checkMobile = (rule, value, cb) => {
const regMobile = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/
if (regMobile.test(value)) {
// 合法的手机号码
return cb()
}
cb(new Error('手机号码格式不正确'))
}

使用vue-table-with-tree-grid

安装新的依赖 vue-tabel-with-tree-grid

vue-tabel-with-tree-grid 官方文档

安装完成后,在 main.js 入口文件内先导入 tree-tabel

然后全局注册组件 tree-tabel

页面中,我们使用了如下属性:

data 确定我们的数据源,columns定义我们的指定列,selection-type 设置是否打开多选框,expand-type 设置是否展示展开栏,show-index 添加索引列, border 给表格添加边框。

使用模板列:

通过在columns 设置 type 属性值为 template,即可设置模板列。

然后在我们界面表格区域,添加一个 template 模板,然后设置作用域插槽 slot 为在数据块定义的模板名称 isOk

自定义格式化时间的全局过滤器

main.js 入口文件全局注册格式化时间的过滤器,代码如下所示:

// 自定义格式化时间的全局过滤器
Vue.filter('dataFormat', function(originVal) {
const dt = new Date(originVal)
const year = dt.getFullYear()
const mon = (dt.getMonth() + 1 + '').padStart(2, '0')
const day = (dt.getDate() + '').padStart(2, '0')
const hh = (dt.getHours() + '').padStart(2, '0')
const mm = (dt.getMinutes() + '').padStart(2, '0')
const ss = (dt.getSeconds() + '').padStart(2, '0')
return `${year}-${mon}-${day} ${hh}:${mm}:${ss}`
})

在我们的组件当中,通过 | 来给我们的时间添加一个过滤器

最终实现效果如下图所示:

添加富文本编辑器

vue-quill-editor 官方文档传送门

通过 vue-ui 界面,可以安装我们所需要的依赖,或者使用下文 npmyarn安装。

NPM

npm install vue-quill-editor --save

# or
yarn add vue-quill-editor

Mount with global

import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'

import 'quill/dist/quill.core.css' // import styles
import 'quill/dist/quill.snow.css' // for snow theme
import 'quill/dist/quill.bubble.css' // for bubble theme

Vue.use(VueQuillEditor, /* { default global options } */)

打开 main.js 入口文件,进行全局导入和注册:

// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器对应的样式
import 'quill/dist/quill.core.css' // import styles
import 'quill/dist/quill.snow.css' // for snow theme
import 'quill/dist/quill.bubble.css' // for bubble theme

// 将富文本编辑器进行全局注册
Vue.use(VueQuillEditor/* { default global options } */)

使用

通过 quill-editor 标签,然后通过 v-model 双向绑定到 form表单的商品详情描述变量 goods_introduce 上。

最终实现效果如下:

使用 lodash 中 cloneDeep(obj) 来实现深拷贝

lodash 官方文档传送门

Using npm

$ npm i -g npm
$ npm i --save lodash

_.cloneDeep(value) 使用


项目中使用 cloneDeep 需求:由于 addForm 中商品所属的分类数组 goods_cat在商品分类模块需要使用 v-model 进行双向绑定数组形式,而我们调用添加商品 api 时,需要字符串形式的,这就产生了冲突,于是我们可以通过深拷贝操作来达到我们的需要。

首先,导入 lodash

然后,直接调用 lodashcloneDeep 方法实现对对象的深拷贝。

使用 echarts

echarts 官方文档传送门

根据官方文档示例,我们先展示测试数据。

<template>
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>数据统计</el-breadcrumb-item>
<el-breadcrumb-item>数据报表</el-breadcrumb-item>
</el-breadcrumb>
<!-- 卡片视图 -->
<el-card>
<!-- 2.为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px;height:400px;"></div>
</el-card>
</div>
</template>

<script>
// 1.导入echarts
import echarts from 'echarts'
export default {
data() {
return {}
},
created() {},
mounted() {
// 3.基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'))
// 4.准备数据和配置项
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
}
// 5.使用刚指定的配置项和数据显示图表。
myChart.setOption(option)
},
computed: {},
methods: {}
}
</script>

<style lang="less" scoped>
</style>

最终实现效果:

由于通过 $http获取得到的数据缺少鼠标跟随的效果,于是需要将 options 进行合并操作,此时使用了 lodashmerge 函数。核心代码如下:

// 4.准备数据和配置项(res.data)
const result = _.merge(res.data, this.options)

在此提供数据报表折线图绘制的源代码:

<template>
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>数据统计</el-breadcrumb-item>
<el-breadcrumb-item>数据报表</el-breadcrumb-item>
</el-breadcrumb>
<!-- 卡片视图 -->
<el-card>
<!-- 2.为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 750px;height:400px;"></div>
</el-card>
</div>
</template>

<script>
// 1.导入echarts
import echarts from 'echarts'
import _ from 'lodash'
export default {
data() {
return {
/* 需要合并的数据 */
options: {
title: {
text: '用户来源'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#E9EEF3'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
boundaryGap: false
}
],
yAxis: [
{
type: 'value'
}
]
}
}
},
created() {},
async mounted() {
// 3.基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'))
const { data: res } = await this.$http.get('reports/type/1')
if (res.meta.status !== 200) {
return this.$message.error('获取折线图数据失败!')
}
// 4.准备数据和配置项(res.data)
const result = _.merge(res.data, this.options)
// 5.使用刚指定的配置项和数据显示图表。
myChart.setOption(result)
},
computed: {},
methods: {}
}
</script>

<style lang="less" scoped>
</style>

最终实现效果如下:

项目优化上线

优化Vue项目,部署Vue项目

项目优化

添加进度条

nprogress 进度条官方文档传送门

打开 vue-ui 面板,选择依赖,输入 nprogress 进行安装相关依赖。

npm
npm install --save  nprogress

main.js 入口文件中,进行相关配置。首先先导入包对应的jscss

// 导入NProgress包对应的js和css
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

配置 request 请求拦截器 和 response 响应拦截器,这样我们就能在每次网络请求时能够展示进度条了。

// 在 request 拦截器中,显示进度条 NProgress.start()
// 设置axios请求拦截器
axios.interceptors.request.use(config => {
NProgress.start()
config.headers.Authorization = window.sessionStorage.getItem('token')
// console.log(config)
return config
})
// 在 response 拦截器中,隐藏进度条 NProgress.done()
axios.interceptors.response.use(config => {
NProgress.done()
return config
})

执行build 时报错

Error: No module factory available for dependency type: CssDependency

解决办法:

参考:解决webpack打包报错 No module factory available for dependency type: CssDependency

在执行 build 命令期间移除所有的 console

babel-plugin-transform-remove-console 官方文档传送门

可以通过 vue-ui 面板,选择开发依赖,然后输入 babel-plugin-transform-remove-console 进行安装

npm

npm install babel-plugin-transform-remove-console --save-dev

安装依赖之后,打开 babel.config.js文件,在 plugins 插件处,添加一个字符串,'transform-remove-console',如下图所示位置。

module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
],
'transform-remove-console'
]
}

上述设置好了之后,还存在一个问题,我们只需要在发布阶段才会去除 console ,在开发和测试环境下如果去除了 console,对于我们调试方面不太好。因此,我们需要设置只在生产环境下去除 console


通过上下两个图比较,我们可以发现,在开发环境下 mode 值为 development,而在生产环境下mode 值为 production

因此,上述代码就需要进行些微修改,如下所示:

// 项目发布阶段需要用到的 babel 插件
const prodPlugins = []
// 判断编译模式为生产环境
if (process.env.NODE_ENV === 'production') {
prodPlugins.push('transform-remove-console')
}

module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
],
...prodPlugins
]
}

生成打包报告

打包时,为了直观地发现项目中存在的问题,可以在打包时生成报告。生成报告的方式有两种:

① 通过命令行参数的形式生成报告

// 通过 vue-cli 的命令选项可以生成打包报告
// --report 选项可以生成 report.html 以帮助分析包内容
vue-cli-service build --report

② 通过可视化的UI面板直接查看报告(推荐

在可视化的UI面板中,通过控制台分析面板,可以方便地看到项目中所存在的问题。

通过 vue.config.js 修改 webpack 的默认配置

通过 vue-cli 3.0 工具生成的项目,默认隐藏了所有 webpack 的配置项,目的是为了屏蔽项目的配置过程,让程
序员把工作的重心,放到具体功能和业务逻辑的实现上。

如果程序员有修改 webpack 默认配置的需求,可以在项目根目录中,按需创建 vue.config.js 这个配置文件,从
而对项目的打包发布过程做自定义的配置

(具体配置参考https://cli.vuejs.org/zh/config/#vue-config-js)。

// vue.config.js
// 这个文件中,应该导出一个包含了自定义配置选项的对象
module.exports = {
// 选项...
}

为开发模式与发布模式指定不同的打包入口

默认情况下,Vue项目的开发模式发布模式,共用同一个打包的入口文件(即 src/main.js)。为了将项目
的开发过程与发布过程分离,我们可以为两种模式,各自指定打包的入口文件,即:

  • 开发模式的入口文件为 src/main-dev.js
  • 发布模式的入口文件为 src/main-prod.js

configureWebpackchainWebpack

vue.config.js 导出的配置对象中,新增 configureWebpackchainWebpack节点,来自定义 webpack 的打包配置。

在这里, configureWebpackchainWebpack 的作用相同,唯一的区别就是它们修改 webpack 配置的方式不同:

chainWebpack 通过链式编程的形式,来修改默认的 webpack 配置
configureWebpack 通过操作对象的形式,来修改默认的 webpack 配置

两者具体的使用差异,可参考如下网址:

传送门

通过 chainWebpack 自定义打包入口

示例代码如下:

module.exports = {
chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
config.entry('app').clear().add('./src/main-prod.js')
})
config.when(process.env.NODE_ENV === 'development', config => {
config.entry('app').clear().add('./src/main-dev.js')
})
}
}

第三方库使用CDN

通过 externals 加载外部 CDN 资源

默认情况下,通过 import 语法导入的第三方依赖包,最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大的问题。

例如上述 chunk-vendors.js 体积很大,原因是全部 import 所依赖的包进行了合并给它。

为了解决上述问题,可以通过 webpackexternals 节点,来配置并加载外部的 CDN 资源。凡是声明在externals 中的第三方依赖包,都不会被打包。

具体配置代码如下

config.set('externals', {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_',
echarts: 'echarts',
nprogress: 'NProgress',
'vue-quill-editor': 'VueQuillEditor'
})

同时,需要在 public/index.html 文件的头部,添加如下的 CDN 资源引用:

<!-- nprogress 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" />
<!-- 富文本编辑器 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.core.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.snow.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" />

同时,需要在 public/index.html 文件的头部,添加如下的 CDN 资源引用:

<script src="https://cdn.staticfile.org/vue/2.5.22/vue.min.js"></script>
<script src="https://cdn.staticfile.org/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.staticfile.org/echarts/4.1.0/echarts.min.js"></script>
<script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script>
<!-- 富文本编辑器的 js 文件 -->
<script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script>

Element-UI 组件按需加载

通过 CDN 优化 ElementUI 的打包

虽然在开发阶段,我们启用了 element-ui 组件的按需加载,尽可能的减少了打包的体积,但是那些被按需加载的组件,还是占用了较大的文件体积。此时,我们可以将 element-ui 中的组件,也通过 CDN 的形式来加载,这样能够进一步减小打包后的文件体积。


具体操作流程如下:
① 在 main-prod.js 中,注释掉 element-ui 按需加载的代码
② 在 public/index.html 的头部区域中,通过 CDN 加载 element-uijscss 样式

<!-- element-ui 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-
chalk/index.css" />
<!-- element-ui 的 js 文件 -->
<script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>

首页内容定制

不同的打包环境下,首页内容可能会有所不同。我们可以通过插件的方式进行定制,插件配置如下:

chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
config.plugin('html').tap(args => {
args[0].isProd = true
return args
})
})
config.when(process.env.NODE_ENV === 'development', config => {
config.plugin('html').tap(args => {
args[0].isProd = false
return args
})
})
}

public/index.html 首页中,可以根据 isProd 的值,来决定如何渲染页面结构:

<!–- 按需渲染页面的标题 -->
<title><%= htmlWebpackPlugin.options.isProd ? '' : 'dev - ' %>电商后台管理系统</title>
<!–- 按需加载外部的 CDN 资源 -->
<% if(htmlWebpackPlugin.options.isProd) { %>
<!—- 通过 externals 加载的外部 CDN 资源-->
<% } %>

路由懒加载

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

具体需要 3 步:

  • 安装 @babel/plugin-syntax-dynamic-import包。
  • babel.config.js 配置文件中声明该插件。
  • 将路由改为按需加载的形式,示例代码如下:
// /* webpackChunkName: "group-foo" */表示路由分组
// './Foo.vue' 表示路由存放路径
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-boo" */ './Baz.vue')

分在同一个组内的组件,当请求其中某一个组件时,组内其它组件也会同时请求。

关于路由懒加载的详细文档,可参考如下链接:

https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

@babel/plugin-syntax-dynamic-import 插件介绍传送门

npm

npm install --save-dev @babel/plugin-syntax-dynamic-import

打开 vue-ui 面板,选择开发依赖,输入 @babel/plugin-syntax-dynamic-import 进行安装。

然后打开 babel.config.js 文件,在 plugins 数组内添加我们安装的依赖: '@babel/plugin-syntax-dynamic-import',具体位置见下文所示:

// 项目发布阶段需要用到的 babel 插件
const prodPlugins = []
// 判断编译模式为生产环境
if (process.env.NODE_ENV === 'production') {
prodPlugins.push('transform-remove-console')
}

module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
],
...prodPlugins,
'@babel/plugin-syntax-dynamic-import'
]
}

最后,将每一个组件更改为懒加载的形式:

项目上线

  1. 通过 node 创建 web 服务器。
  2. 开启 gzip 配置。
  3. 配置 https 服务。
  4. 使用 pm2 管理应用。

通过 node 创建 web 服务器

创建 node 项目,并安装 express,通过 express 快速创建 web 服务器,将 vue 打包生成的 dist 文件夹,托管为静态资源即可,关键代码如下:

const express = require('express')
// 创建 web 服务器
const app = express()

// 托管静态资源
app.use(express.static('./dist'))

// 启动 web 服务器
app.listen(80, () => {
console.log('web server running at http://127.0.0.1')
})

第一步,先创建一个文件夹,这里命名为 vue_shop_server,然后通过 Code 打开。
新建一个终端,输入 npm init -y 执行,初始化包管理配置文件。


初始化完成后,执行 npm install express -S 安装 express

接下来,将我们 vue 项目通过 build 打包得到的 dist 文件夹复制一份,粘贴到我们的 vue_shop_server文件夹内,然后再新建一个 app.js 入口文件,结构图如下图所示。

测试服务器是否能够运行,新建终端,执行 node .\app.js 命令。如果发现输出了 web server running at http://127.0.0.1 则表示服务已经启动了。

开启 gzip 配置

配置完之前服务之后,我们打开网站后,发现请求网络中文件体积依旧是很大,如下图所示。

使用 gzip 可以减小文件体积,使传输速度更快。

可以通过服务器端使用 Expressgzip 压缩。其配置如下:

// 安装相应包
npm install compression -S
// 导入包
const compression = require('compression');
// 启用中间件
app.use(compression());

此时的 app.js 文件代码如下所示,注意,一定要把启用中间件放在托管静态资源之前! 不然 gzip 压缩不会生效。

const express = require('express')
// 导入包
const compression = require('compression');
// 创建 web 服务器
const app = express()
// 启用中间件
app.use(compression());
// 托管静态资源
app.use(express.static('./dist'))


// 启动 web 服务器
app.listen(80, () => {
console.log('web server running at http://127.0.0.1')
})

配置 HTTPS 服务

为什么要启用 HTTPS 服务?
  • 传统的 HTTP 协议传输的数据都是明文,不安全
  • 采用 HTTPS 协议对传输的数据进行了加密处理,可以防止数据被中间人窃取,使用更安全
申请 SSL 证书(https://freessl.org)
  • 进入 https://freessl.cn/ 官网,输入要申请的域名并选择品牌。
  • 输入自己的邮箱并选择相关选项。
  • 验证 DNS(在域名管理后台添加 TXT 记录)。
  • 验证通过之后,下载 SSL 证书( full_chain.pem 公钥;private.key 私钥)。
在后台项目中导入证书
const https = require('https');
const fs = require('fs');
const options = {
cert: fs.readFileSync('./full_chain.pem'),
key: fs.readFileSync('./private.key')
}
https.createServer(options, app).listen(443);

此时 app.js 配置代码如下:

const express = require('express')
// 导入包
const compression = require('compression')
const https = require('https')
const fs = require('fs')
// 创建 web 服务器
const app = express()

const options = {
cert: fs.readFileSync('./full_chain.pem'),
key: fs.readFileSync('./private.key')
}

// 启用中间件
app.use(compression());
// 托管静态资源
app.use(express.static('./dist'))


// 启动 web 服务器
// app.listen(80, () => {
// console.log('web server running at http://127.0.0.1')
// })
https.createServer(options, app).listen(443);

使用 pm2 管理应用

① 在服务器中安装 pm2npm i pm2 -g
② 启动项目:pm2 start 脚本 --name 自定义名称
③ 查看运行项目:pm2 ls
④ 重启项目:pm2 restart 自定义名称
⑤ 停止项目:pm2 stop 自定义名称
⑥ 删除项目:pm2 delete 自定义名称

解决Error: ENOENT: no such file or directory, scandir ‘xxx\node-sass\vendor’

解决方案是执行以下方法:

npm rebuild node-sass

项目学习地址