笔记视频:https://www.bilibili.com/video/BV1EE411B7SU?p=1

项目分析

功能

image.png

技术栈

前端
**

  • Vue
  • Vue-router
  • Element-UI
  • Axios
  • Echarts

后端
**

  • Node.js
  • Express
  • Jwt(状态保持)
  • Mysql
  • Sequelize(操作数据库的框架)

项目初始化

安装 Vue 脚手架

npm install -g @vue/cli

使用 GUI 创建项目

vue ui

手动配置项目预设
image.png

在仪表盘插件选项中安装 vue-cli-plugin-element
image.png
配置插件(此时会安装 babel-plugin-component)
image.png

在仪表盘依赖选项中安装 axios(运行依赖)
image.png

在仪表盘依赖选项中安装 less-loader(开发依赖)
image.png

在仪表盘依赖选项中安装 less(开发依赖)
image.png

与远程仓库建立连接(git 三连)

git remote add github https://github.com/Tumi0321/mall-admin.git

划分目录

  1. ├─assets 资源
  2. ├─css
  3. ├─img
  4. └─fonts
  5. ├─common 用于存放公共的js文件(变量、方法等)
  6. ├─components 用于存放公共的组件
  7. ├─common 能复用的组件
  8. └─content 能复用但只和内容相关的组件
  9. ├─network 网络请求
  10. ├─router 路由
  11. └─views 主要的视图
  12. ├─category
  13. └─home

配置别名

配置别后需要重启

// vue.config.js
module.exports = {
  configureWebpack: {
    resolve: {
      // 配置别名
      alias: {
        'assets': '@/assets',
        'common': '@/common',
        'components': '@/components',
        'network': '@/network',
        'views': '@/views',
      }
    }
  }
}

登录/退出

业务流程

1、在登录页面输入用户名和密码
2、调用后台接口进行验证
3、通过验证后根据后台响应状态跳转到项目主页

相关技术

  • http 是无状态的
  • 通过 cookie 在客户端记录状态(不存在跨域)
  • 通过 session 在服务端记录状态(不存在跨域)
  • 通过 token 方式维持状态(存在跨域)

—什么是跨域?

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

当前页面url 被请求页面url 是否跨域 原因
http://www.test.com/ http://www.test.com/index.html 同源(协议、域名、端口号相同)
http://www.test.com/ https://www.test.com/index.html 跨域 协议不同(http/https)
http://www.test.com/ http://www.baidu.com/ 跨域 主域名不同(test/baidu)
http://www.test.com/ http://blog.test.com/ 跨域 子域名不同(www/blog)
http://www.test.com:8080/ http://www.test.com:7001/ 跨域 端口号不同(8080/7001)

—token 登录原理
image.png

初始化页面

—创建 git 分支

创建分支

git checkout -b login

查看所有分支

git branch

—创建组件

  • 导入组件 ```javascript // plugins/element.js

import { Button, Form, FormItem, Input } from ‘element-ui’;

Vue.use(Button) Vue.use(Form) Vue.use(FormItem) Vue.use(Input)


- 设置样式

element-ui 的组件都有一个和组件名一样的类名

<a name="ExmZw"></a>
### 业务实现

**--数据绑定**

```html
/* 将数据对象绑定给表单 */
<el-form :model="loginForm">
  <el-form-item>
    /* v-model 双向绑定 */
    <el-input v-model="loginForm.username"></el-input>
  </el-form-item>
</el-form>

—表单数据验证

/* 将验证规则传入表单 */
<el-form :rules="loginFormRules">
  // prop 传入需要校验的字段名
  <el-form-item  prop="username">
    /* v-model 双向绑定 */
    <el-input v-model="loginForm.username"></el-input>
  </el-form-item>
</el-form>
export default {
  data () {
      // 表单验证规则
      loginFormRules: {
        // 验证用户名
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
        ],
      }
    }
  },
}

—表单重置

/* 设置 ref 属性 */
<el-form :model="loginForm" ref="loginFormRef">
  /* 监听事件 */
    <el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form>
resetLoginForm () {
  // 使用 element 的 resetFields 事件重置表单
    this.$refs.loginFormRef.resetFields();
},

—登录预验证

<el-form :model="loginForm" ref="loginFormRef">
  /* 监听事件 */
    <el-button type="primary" @click="login">登录</el-button>
</el-form>
login () {
  // callback: boolean(是否校验成功)
  // object: 校验失败的对象
  this.$refs.loginFormRef.validate((callback, object) => {

  })
}

—封装登录请求
**

// network/request.js

import axios from 'axios';

export function request (config) {
  // 1.创建 axios 实例
  const instance = axios.create({
    baseURL: 'http://39.97.105.222:8888/api/private/v1/',
    timeout: 50000
  })

  // 2.axios 拦截器
  instance.interceptors.request.use(config => {
    return config
  }, err => {
    console.log(err);
  })

  instance.interceptors.response.use(res => {
    return res
  }, err => {
    console.log(err);
  })

  // 3.发送真正的网络请求
  return instance(config)
}


—消息提示**

// element.js
import { Button, Form, FormItem, Input, Message } from 'element-ui';
// 全局挂载消息提示组件
Vue.prototype.$message = Message
login () {
  this.$refs.loginFormRef.validate(async valid => {
    if (!valid) return;
    // 保存登录信息
    const { data: res } = await getLogin(this.loginForm)
    // 判断状态使用 message 组件
    if (res.meta.status !== 200) return this.$message.error('登录失败!');
    this.$message.success('登录成功')
  })
}

—保存 token
**
sessionStorage 用于临时保存数据(关闭网页后失效)

login () {
  this.$refs.loginFormRef.validate(async valid => {
    if (!valid) return;
    const { data: res } = await getLogin(this.loginForm)
    if (res.meta.status !== 200) return this.$message.error("登录失败!");
    this.$message.success("登录成功");
    // 保存 token 到 sessionStorage 中
    window.sessionStorage.setItem("token", res.data.token);
    // 跳转到 home
    this.$router.push("/home");
  })
}

—退出
**

<!-- 监听点点击事件 -->
<el-button type="info" @click="logout">退出</el-button>
logout () {
  // 销毁 sessionStorage
  window.sessionStorage.clear();
  // 跳转到 login
  this.$router.push("/login");
}

推送

切换到 master 分支

git checkout master

合并 login 分支

git merge login

推送到仓库

git push -u origin

切换到 login 分支

git checkout login

将 login 分支推送到仓库

git push -u origin login

主页

配置拦截器

需要授权的 API ,必须在请求头中使用 Authorization 字段提供 token 令牌

export function request(config) {
  // 1.创建 axios 实例
  const instance = axios.create({
    baseURL: 'http://39.97.105.222:8888/api/private/v1/',
    timeout: 50000
  })

  // 2.axios 拦截器
  instance.interceptors.request.use(config => {
    config.headers.Authorization = window.sessionStorage.getItem('token');
    return config
  }, err => {
    console.log(err);
  })

  // 3.发送真正的网络请求
  return instance(config)
}

保持 menu 状态

saveNavState(activePath) {
  // activePath:保存当前的路径
  window.sessionStorage.setItem('activePath', activePath);
  this.activePath = activePath;
},

created() {
  // 刷新时重新激活 
  this.activePath = window.sessionStorage.getItem('activePath');
},

商品分类

使用了树形表格组件 vue-table-with-tree-grid

JS 工具库

Lodash

深拷贝:.cloneDeep(value)
合并对象:
.merge(object, [sources])

富文本编辑器

Vue-Quill-Editor

数据可视化

ECharts

进度条

NProgress

项目优化

移除 console

babel-plugin-transform-remove-console

安装

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

使用

// babel.config.js

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  // 添加插件
  "plugins": ["transform-remove-console"]
}

只在发布阶段移除 console

// 项目发布阶段需要用到的 babel 插件
const prodPlugins = []

// 获取当前的编译模式
if (process.env.NODE_ENV === 'production') {
  prodPlugins.push('transform-remove-console')
}


module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  "plugins": [...prodPlugins]
}

生成打包报告

通过命令行

vue-cli-service build —report

通过可视化的 UI 面板直接查看报告
image.png

指定不同打包入口

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

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

在 vue.config.js 导出的配置对象中,新增 configureWebpack 或 chainWebpack 节点,来自定义 webpack
的打包配置(文档)。

区别:

  • chainWebpack 通过链式编程的形式,来修改默认的 webpack 配置
  • configureWebpack 通过操作对象的形式,来修改默认的 webpack 配置
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')
    })
  }
}

加载外部资源

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

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

// vue.config.js

module.exports = {
  chainWebpack: config => {
    // 发布模式
    config.when(process.env.NODE_ENV === 'production', config => {
      config.entry('app').clear().add('./src/main-prod.js')
      config.set('externals', {
        vue: 'Vue',
        'vue-router': 'VueRouter',
        axios: 'axios',
        lodash: '_',
        echarts: 'echarts ',
        nprogress: 'NProgress',
        'vue-quill-editor': 'VueQuillEditor'
      })

    })

    // 开发模式
    config.when(process.env.NODE_ENV === 'development', config => {
      config.entry('app').clear().add('./src/main-dev.js')
    })
  }
}

同时需要在 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/quil1/1.3.4/quill.snow.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" />

<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/1odash . 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/qui11/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editoL23.0.4/dist/vue-quill-editor.js"></script>

优化 ElementUI

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

具体操作流程如下:

  • 在 main-prod.js 中,注释掉 element-ui 按需加载的代码
  • 在 index.html 的头部区域中,通过 CDN 加载 element-ui 的 js 和 css 样式
<!-- 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资源-->
  ...
<% } %>

路由懒加载

const Welcome = () => import('components/Welcome')
const Users = () => import('views/users/Users')
const Rights = () => import('views/rights/Rights')
const Roles = () => import('views/roles/Roles')

Gzip 传输压缩

安装

npm install compression -D

配置

const express = require('express)
const compression = require('compression')
const app = express()

// 一定要把这一行代码写到静态资源托管之前
app.use(compression())
app.use(express.static('./dist'))

app.listen(80, () => {
  console.log('server open 80')
})

配置 HTTPS

申请证书

  • 进入 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)

pm2 管理应用

PM2 是node 进程管理工具,可以利用它来简化很多 node 应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。

安装

npm install pm2 -g

启动项目

pm2 start 脚本 —name 自定义名称

查看运行项目

pm2 ls

重启项目

pm2 restart 自定义名称

删除项目

pm2 delete 自定义名称