小慕读书 我的项目开发

下载查看vue-elemen-admin源码

 git clone https://github.com/PanJiaChen/vue-element-admincd vue-element-adminnpm i npm run dev
我的项目精简
  • 删除src/views下的源码,保留:

    • dashboard:首页
    • error-page: 异样页面
    • login:登录
    • redirect:重定向
  • 对 src/router/index 进行相应批改
  • 删除 src/router/modules文件夹
  • 删除 src/vendor文件夹
WARNING

如果是线上我的项目,倡议将 components 的内容也进行清理,免得影响访问速度,或者间接应用查看 vue-admin-template 构建我的项目,课程抉择 vue-element-admin 初始化我的项目,是因为 vue-element-admin 实现了登录模块,包含 token 校验、网络申请等,能够简化咱们的开发工作

我的项目配置

通过src/settings.js进行局配置

  • title: 站点题目,进入某个页面后,格局为:

页面题目-站点题目

  • showSettings:是否显示右侧悬浮配置按钮
  • tagsView:是否显示页面标签性能条
  • fixedHeader:是否将头部布局固定
  • sidebarLogo:菜单栏中是否显示LOGO
  • errorLog:默认显示谬误日志的环境
源码调试

如需进行源码调试,批改vue.config.js

config  // https://webpack.js.org/configuration/devtool/#development  .when(process.env.NODE_ENV === 'development',    config => config.devtool('cheap-source-map')  )
我的项目构造
  • api:接口申请
  • assets:动态资源
  • components:通用组件
  • directive 自定义指令
  • filters:自定义过滤器
  • icons:图标组件
  • layout:布局组件
  • router:路由配置
  • store:状态治理
  • styles:自定义款式
  • utils:通用工具办法

    • auth.js:token存取
    • permission.js:权限查看
    • request.js:axios申请封装
    • index.js:工具办法
  • views:页面
  • permission.js:登录认证和路由跳转
  • setting.js:全局配置
  • main.js:全局入口文件
  • App.vue:全局入口文件
后端框架搭建
Node简介
Node 是一个基于 V8 引擎的 Javascript 运行环境,它使得 Javascript 能够运行在服务端,间接与操作系统进行交互,与文件管制、网络交互、过程管制等
express简介
express 是一个轻量级的 Node Web 服务端框架,同样是一个人气超高的我的项目,它能够帮忙咱们疾速搭建基于 Node 的 Web 利用
我的项目初始化

创立我的项目

mkdir admin-imooc-nodecd admin-imooc-nodenpm init -y

装置依赖
npm i -S express
创立app.js

const express = require('express')// 创立 express 利用const app = express()// 监听 / 门路的 get 申请app.get('/', function(req, res) {  res.send('hello node')})// 使 express 监听 5000 端口号发动的 http 申请const server = app.listen(5000, function() {  const { address, port } = server.address()  console.log('Http Server is running on http://%s:%s', address, port)})
Express三大根底概念
中间件

中间件是一个函数,在申请和响应周期中被顺序调用

const myLogger = function(req, res, next) {  console.log('myLogger')  next()}app.use(myLogger)

规定次要分为两局部

  • 申请办法:get、post
  • 申请门路:/ /user /*fly$/
异样解决

通过自定义中间件进行异样解决

app.get('/', function(req, res) {  throw new Error('something has error...')})const errorHandler = function (err, req, res, next) {  console.log('errorHandler...')  res.status(500)  res.send('down...')}app.use(errorHandler)

参数不能少 中间件要在申请之后援用

我的项目框架搭建

错误处理 装置boom依赖
`npm i -S boom
`
创立router文件夹 创立router/index.js

const express = require('express')const boom = require('boom')const userRouter = require('./user')const {  CODE_ERROR} = require('../utils/constant')// 注册路由const router = express.Router()router.get('/', function(req, res) {  res.send('欢送学习小慕读书治理后盾')})// 通过 userRouter 来解决 /user 路由,对路由解决进行解耦router.use('/user', userRouter)/** * 集中处理404申请的中间件 * 留神:该中间件必须放在失常解决流程之后 * 否则,会拦挡失常申请 */router.use((req, res, next) => {  next(boom.notFound('接口不存在'))})/** * 自定义路由异样解决中间件 * 留神两点: * 第一,办法的参数不能缩小 * 第二,办法的必须放在路由最初 */router.use((err, req, res, next) => {  const msg = (err && err.message) || '零碎谬误'  const statusCode = (err.output && err.output.statusCode) || 500;  const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message  res.status(statusCode).json({    code: CODE_ERROR,    msg,    error: statusCode,    errorMsg  })})module.exports = router

创立router/use.js

const express = require('express')const router = express.Router()router.get('/info', function(req, res, next) {  res.json('user info...')})module.exports = router

创立 utils/constant

module.exports = {  CODE_ERROR: -1}

验证/user/info:
"user info..."
验证/user/login

{"code":-1,"msg":"接口不存在","error":404,"errorMsg":"Not Found"}

我的项目需要剖析
我的项目技术架构

  • 小慕读书治理后盾
  • 小慕读书小程序
  • 小慕读书h5

我的项目指标

  • 齐全在本地搭建开发环境
  • 贴近企业实在实用场景
技术难点剖析
登录
  • 用户名明码校验
  • token生成、校验和路由过滤
  • 前端token校验和重定向
电子书上传
  • 文件上传
  • 动态资源服务器
电子书解析
  • epub原理
  • zip解压
  • xml解析
电子书增删改
  • mysql数据库利用
  • 前后端异样解决
epub电子书

实质是压缩zip文件

Nginx服务器搭建

装置nginx

  • windows通过下载官网安装包 下载地址:http://nginx.org/en/download.html
  • mac通过brew装置,参考 https://www.jianshu.com/p/c3294887c6b6
批改配置文件

关上配置文件nginx.conf

  • windows位于装置目录
  • macOS位于:/usr/local/etc/nginx/nginx.conf

批改一:增加以后登录用户为owner
user mac owner
批改二:在结尾大括号之前增加
include /Users/mac/upload/upload.conf
额定配置文件 用来增加https
批改/Users/mac/upload/upload.conf文件配置

server {  charset utf-8;  listen 8089;  server_name http_host;  root /Users/mac/upload/;  autoindex on;  add_header Cache-Control "no-cache, must-revalidate";  location / {    add_header Access-Control-Allow-Origin *;  }}server {  listen 443 default ssl;  server_name https_host;  root /Users/mac/upload/;  autoindex on;  add_header Cache-Control "no-cache, must-revalidate";  location / {    add_header Access-Control-Allow-Origin *;  }  ssl_certificate /Users/mac/Documents/https/budai.store.pem;  ssl_certificate_key /Users/mac/Documents/https/budai.store.key;  ssl_session_timeout 5m;  ssl_protocols SSLv3 TLSv1;  ssl_ciphers ALL:ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;  ssl_prefer_server_ciphers  on;}

配置证书

  • https证书:/Users/mac/Documents/https/budai.store.pem;
  • https私钥:/Users/mac/Documents/https/budai.store.key;
启动服务
启动nginx服务

sudo nginx

重启nginx

sudo nginx -s reload

进行nginx服务:

sudo nginx -s stop

查看配置文件是否谬误

sudo nginx -t
拜访地址

  • http:http://localhost:8089
  • https:https://localhost
MySQL数据库搭建
装置MySQL

地址:[https://dev.mysql.com/downloa...
](https://dev.mysql.com/downloa...

装置Navicat

[https://www.navicat.com.cn/pr...
](https://www.navicat.com.cn/pr...

启动mysql

windows参考:https://blog.csdn.net/ycxzuoxin/article/details/80908447
mac参考https://blog.csdn.net/ycxzuoxin/article/details/80908447

cd /usr/local/mysql-8.0.13-macos10.14-x86_64/bin./mysql

初始化数据库
创立数据库book,抉择utf-8,下载book.sql https://www.youbaobao.xyz/resource/admin/book.sql
执行 source book.sql导入数据

用户登录

登录流程剖析

界面简化

将login中的template改为

<template>  <div class="login-container">    <el-form      ref="loginForm"      :model="loginForm"      :rules="loginRules"      class="login-form"      autocomplete="on"      label-position="left"    >      <div class="title-container">        <h3 class="title">小慕读书</h3>      </div>      <el-form-item prop="username">        <span class="svg-container">          <svg-icon icon-class="user" />        </span>        <el-input          ref="username"          v-model="loginForm.username"          placeholder="请输出用户名"          name="username"          type="text"          tabindex="1"          autocomplete="on"        />      </el-form-item>      <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>        <el-form-item prop="password">          <span class="svg-container">            <svg-icon icon-class="password" />          </span>          <el-input            :key="passwordType"            ref="password"            v-model="loginForm.password"            :type="passwordType"            placeholder="明码"            name="password"            tabindex="2"            autocomplete="on"            @keyup.native="checkCapslock"            @blur="capsTooltip = false"            @keyup.enter.native="handleLogin"          />          <span class="show-pwd" @click="showPwd">            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />          </span>        </el-form-item>      </el-tooltip>      <el-button        :loading="loading"        type="primary"        style="width:100%;margin-bottom:30px;"        @click.native.prevent="handleLogin"      >登录      </el-button>    </el-form>  </div></template>
逻辑简化
  • 删除SocialSign组件援用
  • 删除src/views/login/components目录
  • 删除 afterQRScan
  • 删除created 和destoryed
路由解决实例
创立组件

创立组件 src/views/book/create.vue

配置路由

批改 src/router/index.js 的asyncRoutes 新退出页面路由

{    path: '/book',    component: Layout,    redirect: '/book/create',    meta: { title: '图书治理', icon: 'documentation', rules: ['admin'] },    children: [      {        path: '/book/create',        component: () => import('@/views/book/create'),        meta: { title: '上传图书', icon: 'edit', roles: ['admin'] }      }    ]  },
测试
  • 应用editor登录,无奈看到增加图书性能
  • 应用admin登录平台,能够看到增加图书性能

路由和权限校验

路由解决逻辑剖析

路由解决逻辑图如下

路由场景剖析
  • 已获取Token

    • 拜访/login,重定向到 /
    • 拜访/login?redirect=/xxx 重定向到/xxx
    • 拜访/login以外的间接拜访
  • 未获取Token

    • 拜访/login 间接拜访
    • 拜访login以外的 如/dashboard会拜访门路/login?redirect=%2Fdashboard 登录后间接重定向 /dashboard
路由逻辑源码

第一步、main.js 加载了全局路由守卫
import './permission' /
第二步、permission 定义全局路由守卫

router.beforeEach(async(to, from, next) => {  // 启动进度条  NProgress.start()  // 批改页面题目  document.title = getPageTitle(to.meta.title)  // 从 Cookie 获取 Token  const hasToken = getToken()  // 判断 Token 是否存在  if (hasToken) {    // 如果以后门路为 login 则间接重定向至首页    if (to.path === '/login') {      next({ path: '/' })      NProgress.done()    } else {      // 判断用户的角色是否存在      const hasRoles = store.getters.roles && store.getters.roles.length > 0      // 如果用户角色存在,则间接拜访      if (hasRoles) {        next()      } else {        try {          // 异步获取用户的角色          const { roles } = await store.dispatch('user/getInfo')          // 依据用户角色,动静生成路由          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)          // 调用 router.addRoutes 动静增加路由          router.addRoutes(accessRoutes)          // 应用 replace 拜访路由,不会在 history 中留下记录          next({ ...to, replace: true })        } catch (error) {          // 移除 Token 数据          await store.dispatch('user/resetToken')          // 显示谬误提醒          Message.error(error || 'Has Error')          // 重定向至登录页面          next(`/login?redirect=${to.path}`)          NProgress.done()        }      }    }  } else {    // 如果拜访的 URL 在白名单中,则间接拜访    if (whiteList.indexOf(to.path) !== -1) {      next()    } else {      // 如果拜访的 URL 不在白名单中,则间接重定向到登录页面,并将拜访的 URL 增加到 redirect 参数中      next(`/login?redirect=${to.path}`)      NProgress.done()    }  }})router.afterEach(() => {  // 进行进度条  NProgress.done()})

获取角色信息动静路由生成

动静路由剖析
动静路由流程图

动静路由源码剖析

生成动静路由的源码位于 src/store/modules/permission.js中的 generateRoutes办法

import { asyncRoutes, constantRoutes } from '@/router'generateRoutes({ commit }, roles) {  // 返回 Promise 对象  return new Promise(resolve => {    let accessedRoutes    if (roles.includes('admin')) {      // 如果角色中蕴含 admin,则间接跳过判断,间接将 asyncRoutes 全副返回      accessedRoutes = asyncRoutes || []    } else {      // 如果角色中没有蕴含 admin,则调用 filterAsyncRoutes 过滤路由      accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)    }    // 将路由保留到 vuex 中    commit('SET_ROUTES', accessedRoutes)    resolve(accessedRoutes)  })}

SET_ROUTES办法源码如下

SET_ROUTES: (state, routes) => {  // 将 routes 保留到 state 中的 addRoutes  state.addRoutes = routes  // 将 routes 集成到 src/router/index.js 的 constantRoutes 中  state.routes = constantRoutes.concat(routes)}

路由过滤的办法 filterAsyncRoutes源码如下

/** * @params routes - 异步加载的路由 * @params roles - 用户的角色,数组模式 */export function filterAsyncRoutes(routes, roles) {  const res = []  // 遍历全副路由  routes.forEach(route => {    // 对路由进行浅拷贝,留神 children 不会拷贝,因为不须要对 children 进行判断,所以能够应用浅拷贝    const tmp = { ...route }    // 检查用户角色是否具备拜访路由的权限    if (hasPermission(roles, tmp)) {      // 当路由具备拜访权限时,判断路由是否具备 children 属性      if (tmp.children) {        // 当路由蕴含 children 时,对 children 迭代调用 filterAsyncRoutes 办法        tmp.children = filterAsyncRoutes(tmp.children, roles)      }      // 当路由具备拜访权限时,将 tmp 保留到 res 中      res.push(tmp)    }  })  return res}

查看权限办法 hasPermission 源码如下

function hasPermission(roles, route) {  // 查看路由是否蕴含 meta 和 meta.roles 属性  if (route.meta && route.meta.roles) {    // 判断 route.meta.roles 中是否蕴含用户角色 roles 中的任何一个权限,如果蕴含则返回 true,否则为 false    return roles.some(role => route.meta.roles.includes(role))  } else {    // 如果路由没有 meta 或 meta.roles 属性,则视为该路由不须要进行权限管制,所有用户对该路由都具备拜访权限    return true  }}
相干库剖析
NProgress.start();NProgress.done();NProgress.configure({ showSpinner: false }) //showSpinner 能够管制右侧的环形进度条是否显示
总结
对于路由解决
  • vue-element-admin会对拜访的所有路由进行拦挡
  • 拜访路由时会从Cookies中获取Token,判断Token是否存在

    • 如果Token存在,将依据用户角色生成动静路由,而后拜访路由,生成对应的页面组件,有一个特例如果拜访/login 会重定向到 / 路由
    • 如果Token不存在,则会判断路由是否在白名单 在白名单会间接拜访,否则阐明须要登录能力拜访 此时路由会生成一个redirect参数传入login组件,理论拜访的路由为/login?redirect=/xxx
对于动静路由和权限校验
  • 路由分为constantRoutes 和 asyncRoutes
  • 用户登录零碎时,会动静生成路由,其中constantRoutes必然蕴含,而asyncRoutes会进行过滤
  • asyncRoutes过滤逻辑看路由下是否存在meta和meta.roles属性,如果没有此属性,是一个通用路由,不进行权限校验,如果roles属性存在会判断用户的角色是否命中路由中的任意一个权限,命中就保留路由,反之舍弃
  • asyncRoutes 处理完毕后,会和 constantRoutes 合并为一个新的路由对象,并保留到 vuex 的 permission/routes 中
  • 当用户登录时 侧边栏从vuex中获取state.permission.routes,依据该路由动静渲染用户菜单
侧边栏
如果让你实现一个侧边栏,你会如何设计
源码地位
  • sidebar援用自layout组件,layout组件位于 src/layout/index.vue
  • sidebar组件源码位于 src/layout/components/Sidebar/index,vue
el-menu 用法解析

侧边栏的外围是将依据权限过滤后的 router和 el-menu 组件进行映射,所以相熟el-menu是了解sidebar的终点

  <el-row class="tac">    <el-col :span="12">      <el-menu        default-active="1-1"        background-color="#545c64"        text-color="#fff"        active-text-color="#ffd04b"        mode="vertical"        unique-opened        :collapse="isCollapse"        :collapse-transition="false"        class="el-menu-vertical-demo"        @open="handleOpen"        @close="handleClose"        @select="handleSelect"      >        <el-submenu index="1">          <template slot="title">            <i class="el-icon-location"></i>            <span>导航一</span>          </template>          <el-menu-item-group>            <template slot="title">分组一</template>            <el-menu-item index="1-1">选项1</el-menu-item>            <el-menu-item index="1-2">选项2</el-menu-item>          </el-menu-item-group>          <el-menu-item-group title="分组2">            <el-menu-item index="1-3">选项3</el-menu-item>          </el-menu-item-group>          <el-submenu index="1-4">            <template slot="title">选项4</template>            <el-menu-item index="1-4-1">选项1</el-menu-item>          </el-submenu>        </el-submenu>        <el-submenu index="2">          <template slot="title">            <i class="el-icon-menu"></i>            <span slot="title">导航二</span>          </template>          <el-menu-item index="2-1">选项2-1</el-menu-item>        </el-submenu>        <el-menu-item index="3" disabled>          <i class="el-icon-document"></i>          <span slot="title">导航三</span>        </el-menu-item>        <el-menu-item index="4">          <i class="el-icon-setting"></i>          <span slot="title">导航四</span>        </el-menu-item>      </el-menu>    </el-col>    <el-col>      <el-button @click="isCollapse = !isCollapse">折叠</el-button>    </el-col>  </el-row></template><script>export default {  data() {    return {      isCollapse: false    }  },  methods: {    handleSelect(key, keyPath) {      console.log('handleSelect', key, keyPath)    },    handleOpen(key, keyPath) {      console.log('handleOpen', key, keyPath)    },    handleClose(key, keyPath) {      console.log('handleClose', key, keyPath)    }  }}</script>
el-menu

el-menu 示意菜单容器组件

  • default-active:激活的菜单,留神如果存在子菜单,须要填入子菜单ID
  • unique-opened: 是否保留一个菜单关上
  • mode:枚举值 分为vertical和 horizontal两种
  • collapse:是否程度折叠收起菜单
  • @select:点击菜单事件,keypath代表菜单的拜访门路,如 1-4-1 菜单的点击日志为:
handleSelect 1-4-1 (3) ["1", "1-4", "1-4-1"]

获取keypath 咱们能够获取1-4-1菜单的所有父级菜单的ID

  • @open:父菜单关上时触发事件
  • @close:父菜单敞开时触发事件
el-submenu

子菜单容器,el-submenu与el-menu不同,el-menu示意整个菜单,而el-submenu示意一个具体菜单,只是该菜单保含了子菜单
el-submenu 能够通过定制solt的title来自定义菜单款式

<el-submenu index="1">    <template slot="title">      <i class="el-icon-location"></i>      <span>导航一</span>    </template></el-submenu>

el-submenu 容器内的default的solt用来寄存子菜单,能够蕴含三种子菜单组件

  • el-menu-item-group:菜单分组,为一组菜单增加一个题目,el-menu-item-group容器内须要寄存el-menu-item组件,反对通过title的solt来定制题目款式
  • el-submenu:el-submenu 反对循环嵌套 el-submenu,这使得超过两级子组件得以实现
  • el-menu-item:子菜单组件
sidebar-item源码剖析

sidebar-item 组件源码如下:

<template>  <div v-if="!item.hidden" class="menu-wrapper">    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />        </el-menu-item>      </app-link>    </template>    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>      <template slot="title">        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />      </template>      <sidebar-item        v-for="child in item.children"        :key="child.path"        :is-nest="true"        :item="child"        :base-path="resolvePath(child.path)"        class="nest-menu"      />    </el-submenu>  </div></template><script>import path from 'path'import { isExternal } from '@/utils/validate'import Item from './Item'import AppLink from './Link'import FixiOSBug from './FixiOSBug'export default {  name: 'SidebarItem',  components: { Item, AppLink },  mixins: [FixiOSBug],  props: {    // route object    item: {      type: Object,      required: true    },    isNest: {      type: Boolean,      default: false    },    basePath: {      type: String,      default: ''    }  },  data() {    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237    // TODO: refactor with render function    this.onlyOneChild = null    return {}  },  methods: {    hasOneShowingChild(children = [], parent) {      const showingChildren = children.filter(item => {        if (item.hidden) {          return false        } else {          // Temp set(will be used if only has one showing child)          this.onlyOneChild = item          return true        }      })      // When there is only one child router, the child router is displayed by default      if (showingChildren.length === 1) {        return true      }      // Show parent if there are no child router to display      if (showingChildren.length === 0) {        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }        return true      }      return false    },    resolvePath(routePath) {      if (isExternal(routePath)) {        return routePath      }      if (isExternal(this.basePath)) {        return this.basePath      }      return path.resolve(this.basePath, routePath)    }  }}</script>
sidebar-item props剖析

sidebar-item的props如下:

  • item:路由对象
  • basepath:路由门路
sidebar-item 展现逻辑剖析

sidebar-item 最重要的是展现逻辑,次要分为以下几步:

  • 通过item.hidden 管制菜单是否展现
  • 通过 hasoneShowingChild(item.children,item)&&(!onlyOneCHild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow 逻辑判断template菜单是否展现,template代表繁多菜单

    • hasOnsShowingChild:判断是否只有一个须要展现的子路由
    • !onlyOneChild.children || onlyOneChild.noShowingChildren:判断须要展现的子菜单,是否蕴含children属性,如果蕴含,则阐明子菜单可能存在孙子菜单,此时须要在判断noShowingChildren属性
    • !item.alwaysShow:判断路由中是否存在alwaysShow属性,如果存在,则返回false,不展现template菜单,也就是说只有配置了alwaysShow属性就会间接进入el-submenu组件
hasOnsShowingChild 办法源码详解

入参:

  • children:router对象的children属性
  • item:router对象
hasOneShowingChild(children = [], parent) {  const showingChildren = children.filter(item => {    // 如果 children 中的路由蕴含 hidden 属性,则返回 false    if (item.hidden) {      return false    } else {      // 将子路由赋值给 onlyOneChild,用于只蕴含一个路由时展现       this.onlyOneChild = item      return true    }  })  // 如果过滤后,只蕴含展现一个路由,则返回 true  if (showingChildren.length === 1) {    return true  }  // 如果没有子路由须要展现,则将 onlyOneChild 的 path 设置空路由,并增加 noShowingChildren 属性,示意尽管有子路由,然而不须要展现子路由  if (showingChildren.length === 0) {    this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }    return true  }  // 返回 false,示意不须要展现子路由,或者超过一个须要展现的子路由  return false}
  • 如果展现template组件,首先会展现app-link组件,而后是el-menu-item,最外面嵌套的是item组件:

item组件须要路由meta中蕴含 title和icon属性,否则将渲染内容为空的vnode对象

<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">  <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">      <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />  </el-menu-item></app-link>
  • 如果template菜单不展现,则展现el-submenu菜单,el-submenu逻辑中采纳了嵌套组件的做法,将sidebar-item 嵌套在 el-submenu 中:
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>  <template slot="title">    <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />  </template>  <sidebar-item    v-for="child in item.children"    :key="child.path"    :is-nest="true"    :item="child"    :base-path="resolvePath(child.path)"    class="nest-menu"  /></el-submenu>

el-submenu 中的 sidebar-item有两点区别:

  • 第一是传入is-nest参数
  • 第二是传入base-path 参数
app-link 源码剖析

app-link 是一个动静组件,通过解析to参数,如果蕴含http前缀则变成a标签,否则变成一个router-link组件

<template>  <!-- eslint-disable vue/require-component-is -->  <component v-bind="linkProps(to)">    <slot />  </component></template><script>import { isExternal } from '@/utils/validate'export default {  props: {    to: {      type: String,      required: true    }  },  methods: {    linkProps(url) {      if (isExternal(url)) {        return {          is: 'a',          href: url,          target: '_blank',          rel: 'noopener'        }      }      return {        is: 'router-link',        to: url      }    }  }}</script>

isExternal函数通过一个正则表达式匹配http链接

export function isExternal(path) {  return /^(https?:|mailto:|tel:)/.test(path)}
item组件源码剖析

item组件通过定义render函数实现组件渲染

<script>export default {  name: 'MenuItem',  functional: true,  props: {    icon: {      type: String,      default: ''    },    title: {      type: String,      default: ''    }  },  render(h, context) {    const { icon, title } = context.props    const vnodes = []    if (icon) {      vnodes.push(<svg-icon icon-class={icon}/>)    }    if (title) {      vnodes.push(<span slot='title'>{(title)}</span>)    }    return vnodes  }}</script>
总结
  • sidebar:sidebar次要蕴含el-menu容器组件,el-menu中遍历vuex中的routes,生成sidebar-item 组件,sidebar次要配置项如下:

    • activeMenu:依据以后路由的meta.activeMenu 属性管制侧边栏中的高亮菜单
    • isCollapse:依据Cookie的sidebarStatus管制侧边栏是否折叠
    • variable:通过@/stylels/variables.scss 填充el-menu的根本款式
  • sidebar-item:sidebar-item 分为两局部:

    • 第一局部是当只须要展现一个children或者没有children时进行展现,展现的组件包含:

      • app-link:动静组件,path为链接时,显示为a标签,path为路由时,显示为router-link组件
      • el-menu-item:菜单项,当sidebar-item为非nest组件时,el-menu-item会减少submenu-title-noDropdown的class
      • item:el-menu-item里的内容,次要是icon和title,当title为空时,整个菜单项将不会展现
      • 第二局部是当children超过两项时进行展现,展现的组件包含:

        • el-submenu:子菜单组件容器,用于嵌套子菜单组件
        • sidebar-item:el-submenu迭代嵌套了sidebar-item组件,在sidebar-item组件中有两点变动

          * 设置is-nest属性为true* 依据child.path 生成了 base-path属性传入sidebar-item 组件
重定向
如何实现重定向性能
登录重定向

login.vue 中对 $route 进行监听

watch: {  $route: {    handler: function(route) {      const query = route.query      if (query) {        this.redirect = query.redirect        this.otherQuery = this.getOtherQuery(query)      }    },    immediate: true  }}

this.getOtherQuery(query)的用处是获取除redirect外的其余查问条件,登录胜利后:

this.$store.dispatch('user/login', this.loginForm).then(() => {  this.$router.push({ path: this.redirect || '/', query: this.otherQuery })  this.loading = false}).catch(() => {  this.loading = false})

实现重定向的代码为:
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })

重定向组件

vue-element-admin 提供了专门的重定向组件,源码如下:

<script>export default {  created() {    const { params, query } = this.$route    const { path } = params    this.$router.replace({ path: '/' + path, query })  },  render: function(h) {    return h() // avoid warning message  }}</script>

重定向组件配置了动静路由

{    path: '/redirect',    component: Layout,    hidden: true,    children: [      {        path: '/redirect/:path*',        component: () => import('@/views/redirect/index')      }    ]}

还有一个细节
path: '/redirect/:path*'
示意匹配零个或多个路由,比方路由为 /redirect 时,依然能匹配到redirect组件,如果将路由改为
path: '/redirect/:path'
此时路由 /redirect 将只能匹配到layout组件,而无奈匹配redirect组件

面包屑导航
如何实现面包屑导航
el-breadcrumb-item
  • el-breadcrumb-item:面包屑导航容器,separator管制面包屑导航文本中的分割线
  • el-breadcrumb-item:面包屑子项目,能够应用to属性切换路由,slot中能够蕴含a标签来跳转到外链
<el-breadcrumb separator="/">  <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>  <el-breadcrumb-item><a href="/">流动治理</a></el-breadcrumb-item>  <el-breadcrumb-item>流动列表</el-breadcrumb-item>  <el-breadcrumb-item>流动详情</el-breadcrumb-item></el-breadcrumb>

应用to属性和a标签切换路由的区别是:to属性切换路由是动静替换app.vue中的路由内容,而a标签切换路由会刷新界面

路由与面包屑导航映射

面包屑导航的最大难度在于如何将路由与面包屑导航进行映射
生成面包屑导航

getBreadcrumb() {  let matched = this.$route.matched.filter(item => item.meta && item.meta.title)  const first = matched[0]  if (!this.isDashboard(first)) {    matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)  }  this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)}

面包屑导航实现的逻辑如下

  • 获取this.$route.matched 并过滤其中不蕴含 item.meta.title的项,生成新的面包屑导航数组 matched
  • 判断 matched 第一项是否为dashboard,如果不是,则增加dashboard为面包屑导航第一项
  • 再次过滤matched中的item.meta.title为空的项和item.meta.breadcrumb为false的项
这里的要害是this.$route.matched 属性,它是一个数组,记录了路由匹配的过程,是面包屑导航实现的根底

isDashboard实现如下:

isDashboard(route) {  const name = route && route.name  if (!name) {    return false  }  return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()}
渲染面包屑导航

面包屑导航模板源码

<el-breadcrumb class="app-breadcrumb" separator="/">    <transition-group name="breadcrumb">      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>      </el-breadcrumb-item>    </transition-group></el-breadcrumb>

el-breadcrumb-item 内做了一个判断,如果最初一个元素或者路由的redirect属性指定为noRedirect 则不会生成链接,否则将应用 a标签生成链接,然而这里应用了 @click.prevent 阻止了默认a标签的事件触发,而应用自定义的 handlink 办法解决路由跳转,handlink办法源码如下:

handleLink(item) {  const { redirect, path } = item  if (redirect) {    this.$router.push(redirect)    return  }  this.$router.push(this.pathCompile(path))}

这里的 pathCompile 用于解决动静路由匹配问题

用户登录(中)

登录组件剖析

登录组件login.vue 布局要点如下:

  • el-form容器,蕴含username和password两个 el-form-item,el-form次要属性:

    • model为loginForm
    • rules为loginRules
  • password应用了el-tooltip提醒,当用户关上大小写时,会进行提醒,次要属性:

    • manual:手动管制模式,设置为true后,mouseenter和mouseleave事件将不会失效
    • placement:提醒呈现的地位
  • password对应el-input次要属性:

    • @keyup.native='checkCapsLock' 键盘按键时绑定 checkCapslock事件
    • @keyup.enter.native='handleLogin' 监听键盘enter按下后的事件
>  这里绑定@keyup 事件须要增加 .native 修饰符,这是因为咱们的事件绑定在el-input组件上,所以如果不增加.native修饰符,事件将无奈绑定到原生的input标签上
  • 蕴含一个el-button,点击时调用handleLogin办法,并触发loading成果
checkCapsLock办法

checkCapsLock 办法的主要用途是监听用户键盘输入,显示提醒文字的判断逻辑如下

  • 按住shift时输出小写字符
  • 未按shift时输出大写字符

当按下CapsLock按键时,如果按下后是小写模式,则会立刻打消提醒文字

checkCapslock({ shiftKey, key } = {}) {  if (key && key.length === 1) {    if (shiftKey && (key >= 'a' && key <= 'z') || !shiftKey && (key >= 'A' && key <= 'Z')) {      this.capsTooltip = true    } else {      this.capsTooltip = false    }  }  if (key === 'CapsLock' && this.capsTooltip === true) {    this.capsTooltip = false  }}
handleLogin 办法

handleLogin 办法解决流程如下:

  • 调用 el-form的validate办法对rules进行验证;
  • 如果验证通过,则会调用vuex的 user/login action 进行登录验证
  • 登录验证通过后,会重定向到redirect路由,如果redirect路由不存在,则间接重定向到 / 路由
这里须要留神:因为vuex中的user制订了namespaced为true,所以dispatch时须要加上namespace,否则将无奈调用vuex中的action
handleLogin() {  this.$refs.loginForm.validate(valid => {    if (valid) {      this.loading = true      this.$store.dispatch('user/login', this.loginForm)        .then(() => {          this.$router.push({ path: this.redirect || '/', query: this.otherQuery })          this.loading = false        })        .catch(() => {          this.loading = false        })    } else {      console.log('error submit!!')      return false    }  })}
user/login 办法

user/login 办法调用了login API,传入username和password参数,申请胜利后会从response中获取token,而后将token保留到cookies中,之后返回,如果申请失败,将调用reject办法,交由咱们自定义的request模块来解决异样

login({ commit }, userInfo) {    const { username, password } = userInfo    return new Promise((resolve, reject) => {      login({ username: username.trim(), password: password }).then(response => {        const { data } = response        commit('SET_TOKEN', data.token)        setToken(data.token)        resolve()      }).catch(error => {        reject(error)      })    })}

login API的办法如下:

import request from '@/utils/request'export function login(data) {  return request({    url: '/user/login',    method: 'post',    data  })}

这里应用request办法,它是一个基于axios封装的库,目前咱们的/user/login 接口是通过mock实现的,用户的数据位于 /mock/user.js

axios用法剖析

request 库应用了 axios的手动实例化办法create来封装申请,要了解其中的用法,咱们首先须要学习axios库的用法

axios根本案例

咱们先从一个一般的axios示例开始

import axios from 'axios'const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'axios.get(url).then(response => {  console.log(response)})

上述代码能够改为

import axios from 'axios'const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'axios.get(url).then(response => {  console.log(response)})

如果咱们在申请是须要在headr中增加一个token,须要将代码批改为:

const url = 'https://test.youbaobao.xyz:18081/book/home/v2'axios.get(url, {   params: { openId: '1234' },  headers: { token: 'abcd' }}).then(response => {  console.log(response)}).catch(err => {  console.log(err)})

这样改变能够实现咱们的需要,然而有两个问题

  • 每个须要传入token的申请都须要增加header对象 会造成大量反复代码
  • 每个申请都须要手动定义异样解决,而异样解决的逻辑大多是统一的,如果将其封装成通用的异样解决办法,呢么每个申请都有调用一遍
axios.create 示例

上面咱们应用axios.create 对整个申请进行重构

const url = '/book/home/v2'const request = axios.create({  baseURL: 'https://test.youbaobao.xyz:18081',  timeout: 5000})request({  url,   method: 'get',  params: {    openId: '1234'  }})

首先咱们通过 axios.create 生成一个函数,该函数是axios示例,通过执行该办法实现申请,它与间接调用axios.get 区别如下

  • 须要传入url参数,axios.get 办法的第一个参数是url
  • 须要传入method参数,axios.get 办法曾经示意发动get申请
axios申请拦截器

上述代码实现了根本申请的性能,上面咱们须要为http申请的headers中增加token,同时进行白名单校验,如 /login 不须要增加token,并实现异步捕捉和自定义解决:

const whiteUrl = [ '/login', '/book/home/v2' ]const url = '/book/home/v2'const request = axios.create({  baseURL: 'https://test.youbaobao.xyz:18081',  timeout: 5000})request.interceptors.request.use(  config => {    // throw new Error('error...')    const url = config.url.replace(config.baseURL, '')      if (whiteUrl.some(wl => url === wl)) {        return config      }    config.headers['token'] = 'abcd'    return config  },  error => {    return Promise.reject(error)  })request({  url,   method: 'get',  params: {    openId: '1234'  }}).catch(err => {  console.log(err)})

这里外围是调用了 request.interceptors.request.use 办法,即axios的申请拦截器,该办法须要传入两个参数,第一个参数为拦截器办法,蕴含config参数,咱们能够在这个办法中批改config并且进行回传,第二个参数是异样解决办法,咱们能够应用 Promise.reject(error)将异样返回给用户进行解决,所以咱们在request申请后能够通过catch捕捉异样进行自定义解决

axios响应拦截器

上面咱们进一步加强axios性能 咱们在理论开发中除了须要保障http statusCode为 200 ,还须要保障业务代码正确,上述案例中 我定义了 error_code 为0时,示意业务失常,如果返回值不为0 则阐明业务解决出错 此时咱们通过 request.interceptors.response.use 办法定义响应拦截器,它依然须要2个参数,与申请拦截器相似,留神第二个参数次要解决 statusCode 非200的异样申请,源码如下:

const whiteUrl = [ '/login', '/book/home/v2' ]const url = '/book/home/v2'const request = axios.create({  baseURL: 'https://test.youbaobao.xyz:18081',  timeout: 5000})request.interceptors.request.use(  config => {    const url = config.url.replace(config.baseURL, '')    if (whiteUrl.some(wl => url === wl)) {      return config    }    config.headers['token'] = 'abcd'    return config  },  error => {    return Promise.reject(error)  })request.interceptors.response.use(  response => {    const res = response.data    if (res.error_code != 0) {      alert(res.msg)      return Promise.reject(new Error(res.msg))    } else {      return res    }  },  error => {    return Promise.reject(error)  })request({  url,   method: 'get',  params: {    openId: '1234'  }}).then(response => {  console.log(response)}).catch(err => {  console.log(err)})
request 库源码剖析

有了上述根底后,咱们在看request库源码就非常容易了

const service = axios.create({  baseURL: process.env.VUE_APP_BASE_API,  timeout: 5000})service.interceptors.request.use(  config => {    // 如果存在 token 则附带在 http header 中    if (store.getters.token) {      config.headers['X-Token'] = getToken()    }    return config  },  error => {    return Promise.reject(error)  })service.interceptors.response.use(  response => {    const res = response.data    if (res.code !== 20000) {      Message({        message: res.message || 'Error',        type: 'error',        duration: 5 * 1000      })      // 判断 token 生效的场景      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {        // 如果 token 生效,则弹出确认对话框,用户点击后,清空 token 并返回登录页面        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {          confirmButtonText: 'Re-Login',          cancelButtonText: 'Cancel',          type: 'warning'        }).then(() => {          store.dispatch('user/resetToken').then(() => {            location.reload()          })        })      }      return Promise.reject(new Error(res.message || 'Error'))    } else {      return res    }  },  error => {    Message({      message: error.message,      type: 'error',      duration: 5 * 1000    })    return Promise.reject(error)  })export default service
登录细节剖析
细节一:页面启动后主动聚焦

查看用户名或明码是否为空,如果发现为空,则主动汇集:

mounted() {    if (this.loginForm.username === '') {      this.$refs.username.focus()    } else if (this.loginForm.password === '') {      this.$refs.password.focus()    }}
细节二:显示明码后主动汇集

切换明码显示状态后,主动汇集password输入框:

showPwd() {  if (this.passwordType === 'password') {    this.passwordType = ''  } else {    this.passwordType = 'password'  }  this.$nextTick(() => {    this.$refs.password.focus()  })}
细节三:通过reduce过滤对象属性
const query = {  redirect: '/book/list',  name: 'sam',  id: '1234'}// 间接删除 query.redirect,会间接改变 query// delete query.redirect// 通过浅拷贝实现属性过滤// const _query = Object.assign({}, query)// delete _query.redirectconst _query = Object.keys(query).reduce((acc, cur) => {    if (cur !== 'redirect') {      acc[cur] = query[cur]    }    return acc  }, {})console.log(query, _query)
敞开Mock接口

去掉main.js 中mock相干代码

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

删除 src/api目录下2个api文件

article.jsqiniu.js

删除 vue.config.js 中的相干配置

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

批改后咱们的我的项目就不能应用mock接口,会间接申请到http接口,咱们须要关上SwitchHosts配置host映射,让域名映射到本地node我的项目
127.0.0.1 budai.store

批改接口地址

咱们将公布到开发环境和生产环境,所以须要批改 .env.development 和 .env.production两个配置文件;
`VUE_APP_BASE_API = 'https://budai.store:18082'
`
有两点须要留神:

  • 这里我应用了域名 budai.store ,大家能够将其替换为你本人注册的域名,如果你还没注册域名,应用localhost,
  • 如果没有申请https证书,也能够采纳http协定,同样能够实现登录申请,但如果要公布到互联网倡议应用https协定安全性会更好

重新启动我的项目后,发现已能够指向指定的接口
https://budai.store:18082/user/login

用户登录下

后端API解决流程

搭建https服务器

首先须要将https证书拷贝到node我的项目中,而后增加下列代码

const fs = require('fs')const https = require('https')const privateKey = fs.readFileSync('https/budai.store.key', 'utf8')const certificate = fs.readFileSync('https/budai.store.pem', 'utf8')const credentials = { key: privateKey, cert: certificate }const httpsServer = https.createServer(credentials, app)const SSLPORT = 18082httpsServer.listen(SSLPORT, function() {  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT)})

启动https服务须要证书对象credentials,蕴含了私钥和证书,重新启动node服务

node app.js

在浏览器输出
https://budai.store:18082
能够看到
`欢送学习小慕读书治理后盾
`
阐明https服务启动胜利

创立 /user/login API

在 router/user.js 中填入以下代码:

router.post('/login', function(req, res, next) {  console.log('/user/login', req.body)  res.json({    code: 0,    msg: '登录胜利'  })})
$ curl https://budai.store:18082/user/login -X POST -d "username=sam&password=123456"{"code":0,"msg":"登录胜利"}

下面的命令能够简写为
`curl https://budai.store:18082/user/login -d "username=sam&password=123456"
`
这里咱们能够通过req.body 获取POST申请中的参数,然而没有获取胜利,咱们须要通过 body-parser 中间件来解决这个问题:
`npm i -S body-parser
`
在 app.js 中退出

const bodyParser = require('body-parser')// 创立 express 利用const app = express()app.use(bodyParser.urlencoded({ extended: true }))app.use(bodyParser.json())

返回前端按钮申请登录接口,发现控制台报错:
`Access to XMLHttpRequest at 'https://budai.store:18082/user/login' from origin 'http://localhost:9527' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
`
这是因为前端部署在 http://localhost:9527 而后端部署在 https://budai.store:18082,所以导致了跨域谬误,咱们须要在 node 服务中增加跨域中间件 cors:
`npm i -S cors
`
而后批改app.js:

const cors = require('cors')// ...app.use(cors())

再次申请即可胜利,这里咱们在Network中会发现发动了两次https申请,这是因为因为触发了跨域申请,所以会首先进行OPTIONS申请,判断服务器是否容许跨域申请,如果容许能力理论进行申请

响应后果封装

在 /user/login 咱们看到的返回值是

res.json({  code: 0,  msg: '登录胜利'})

之后咱们还要定义谬误返回值,但如果每个接口都编写以上代码就显得十分冗余,而且不易保护,所以咱们创立一个Result类来解决这个问题

node响应后果封装

创立 /models/Result.js 文件

const {  CODE_ERROR,  CODE_SUCCESS} = require('../utils/constant')class Result {  constructor(data, msg = '操作胜利', options) {    this.data = null    if (arguments.length === 0) {      this.msg = '操作胜利'    } else if (arguments.length === 1) {      this.msg = data    } else {      this.data = data      this.msg = msg      if (options) {        this.options = options      }    }  }  createResult() {    if (!this.code) {      this.code = CODE_SUCCESS    }    let base = {      code: this.code,      msg: this.msg    }    if (this.data) {      base.data = this.data    }    if (this.options) {      base = { ...base, ...this.options }    }    console.log(base)    return base  }  json(res) {    res.json(this.createResult())  }  success(res) {    this.code = CODE_SUCCESS    this.json(res)  }  fail(res) {    this.code = CODE_ERROR    this.json(res)  }}module.exports = Result

咱们还须要创立 /utils/constant.js

module.exports = {  CODE_ERROR: -1,  CODE_SUCCESS: 0}

Result 应用了 ES6 的Class,应用办法如下

// 调用胜利时new Result().success(res)new Result('登录胜利').success(res)// 调用胜利时,蕴含参数new Result({ token }, '登录胜利').success(res)// 调用失败时new Result('用户名或明码不存在').fail(res)

有了Result类当前咱们能够将登录API改为:

router.post('/login', function(req, res, next) {  const username = req.body.username  const password = req.body.password  if (username === 'admin' && password === '123456') {    new Result('登录胜利').success(res)  } else {    new Result('登录失败').fail(res)  }})
如果在响应前跑出error,此时的Error将被咱们自定义的异样解决捕捉,并返回500谬误
登录用户数据库查问

装置mysql库
` npm i -S mysql
`
创立db目录,新建两个文件

index.jsconfig.js

config.js源码如下

module.exports = {  host: 'localhost',  user: 'root',  password: '12345678',  database: 'book'}
连贯

连贯数据库须要提供应用mysql的

const {host,user,password,database} = require('./config')function connect() {  return mysql.createConnection({    host,    user,    password,    database,    multipleStatements: true  })}
查问

查问须要调用connection对象的query办法:

function querySql(sql) {  const conn = connect()  debug && console.log(sql)  return new Promise((resolve, reject) => {    try {      conn.query(sql, (err, results) => {        if (err) {          debug && console.log('查问失败,起因:' + JSON.stringify(err))          reject(err)        } else {          debug && console.log('查问胜利', JSON.stringify(results))          resolve(results)        }      })    } catch (e) {      reject(e)    } finally {      conn.end()    }  })}

咱们在 constant.js 创立一个debug参数管制打印日志:
const debug = require('../utils/constant').debug

这里须要留神conn对象应用结束后须要调用end进行敞开,否则会导致内存透露

调用办法如下:

db.querySql('select * from book').then(result => {  console.log(result)})

这里咱们须要基于mysql 查问库封装一层service ,用来协调业务逻辑和数据库逻辑,咱们不心愿间接把业务逻辑放在router中,创立 /service/user.js

const { querySql } = require('../db')function login(username, password) {  const sql = `select * from admin_user where username='${username}' and password='${password}'`  return querySql(sql)}module.exports = {  login}

革新 /user/login API:

router.post('/login', function(req, res, next) {  const username = req.body.username  const password = req.body.password  login(username, password).then(user => {    if (!user || user.length === 0) {      new Result('登录失败').fail(res)    } else {      new Result('登录胜利').success(res)    }  })})

此时即便咱们输出正确的用户名和明码依然无奈登录,这是因为明码采纳了MD5+SALT加密,所以咱们须要对明码进行对等加密,能力查问胜利。在/utils/constant.js 中退出SALT:

module.exports = {  // ...  PWD_SALT: 'admin_imooc_node',}

装置crypto库:
`npm i -S crypto
`
而后在 /utils/index.js 中创立md5办法

const crypto = require('crypto')function md5(s) {  // 留神参数须要为 String 类型,否则会出错  return crypto.createHash('md5')    .update(String(s)).digest('hex');}
      const password = md5(`${req.body.password}${PWD_SALT}`)

再次输出正确的用户名和明码,查问胜利:

select * from admin_user where username='admin' and password='91fe0e80d07390750d46ab6ed3a99316'查问胜利 [{"id":1,"username":"admin","password":"91fe0e80d07390750d46ab6ed3a99316","role":"admin","nicknamedmin","avatar":"https://www.youbaobao.xyz/mpvue-res/logo.jpg"}]{ code: 0, msg: '登录胜利' }
express-validator

express-validator 是一个功能强大的表单验证器,它是validator.js的中间件
应用express-validator 能够简化POST申请的参数验证,应用办法如下:
装置
`npm i -S express-validator
`
验证

const { body, validationResult } = require('express-validator')const boom = require('boom')router.post(  '/login',  [    body('username').isString().withMessage('username类型不正确'),    body('password').isString().withMessage('password类型不正确')  ],  function(req, res, next) {    const err = validationResult(req)    if (!err.isEmpty()) {      const [{ msg }] = err.errors      next(boom.badRequest(msg))    } else {      const username = req.body.username      const password = md5(`${req.body.password}${PWD_SALT}`)      login(username, password).then(user => {        if (!user || user.length === 0) {          new Result('登录失败').fail(res)        } else {          new Result('登录胜利').success(res)        }      })    }  })

express-validator 应用技巧:

  • 在 router.post 办法中应用body等办法判断参数类型,并指定出错时的提示信息
  • 应用 const err = validationResult(req) 获取错误信息,err.errors 是一个数组,蕴含所有错误信息,如果错误信息为空示意参数校验胜利
  • 如果发现错误咱们能够应用 next(boom.badRequest(msg)) 跑出异样,交给咱们自定义的异样解决办法进行解决

JWT基本概念

生成JWT Token

装置 jsonwebtoken
`npm i -S jsonwebtoken
`
应用

const jwt = require('jsonwebtoken')const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')login(username, password).then(user => {    if (!user || user.length === 0) {      new Result('登录失败').fail(res)    } else {      const token = jwt.sign(        { username },        PRIVATE_KEY,        { expiresIn: JWT_EXPIRED }      )      new Result({ token }, '登录胜利').success(res)    }})

这里须要定义jwt的私钥和过期工夫,过期工夫不宜过短,也不宜过长,课程里设置为1小时,理论业务中可依据场景来判断,通常倡议不超过24小时,保密性要求高的业务能够设置为1-2小时

module.exports = {  // ...  PRIVATE_KEY: 'admin_imooc_node_test_youbaobao_xyz',  JWT_EXPIRED: 60 * 60, // token生效工夫}

前端再次申请,后果如下

{  "code":0,  "msg":"登录胜利",  "data":{    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTc0NDk1NzA0LCJleHAiOjE1NzQ0OTkzMDR9.9lnxdTn1MmMbKsPvhvRHDRIufbMcUD437CWjnoJsmfo"  }}

咱们能够将该token在jwt.io网站上进行验证,能够失去如下后果:

{  "username": "admin",  "iat": 1574495704,  "exp": 1574499304}

能够看到username 被正确解析,阐明token生成胜利

前端登录申请革新

批改 src/utils/request.js

service.interceptors.response.use(  response => {    const res = response.data    if (res.code !== 0) {      Message({        message: res.msg || 'Error',        type: 'error',        duration: 5 * 1000      })      // 判断 token 生效的场景      if (res.code === -2) {        // 如果 token 生效,则弹出确认对话框,用户点击后,清空 token 并返回登录页面        MessageBox.confirm('Token 生效,请从新登录', '确认退出登录', {          confirmButtonText: '从新登录',          cancelButtonText: '勾销',          type: 'warning'        }).then(() => {          store.dispatch('user/resetToken').then(() => {            location.reload()          })        })      }      return Promise.reject(new Error(res.msg || '申请失败'))    } else {      return res    }  },  error => {    let message = error.message || '申请失败'    if (error.response && error.response.data) {      const { data } = error.response      message = data.msg    }    Message({      message,      type: 'error',      duration: 5 * 1000    })    return Promise.reject(error)  })
JWT 认证

装置express-jwt
`npm i -S express-jwt
`
创立 /router/jwt.js

const expressJwt = require('express-jwt');const { PRIVATE_KEY } = require('../utils/constant');const jwtAuth = expressJwt({  secret: PRIVATE_KEY,  credentialsRequired: true,// 设置为false就不进行校验了,游客也能够拜访  ,algorithms: ['HS256'] }).unless({  path: [    '/',    '/user/login'  ], // 设置 jwt 认证白名单});module.exports = jwtAuth;

在 /router/index.js中应用中间件

const jwtAuth = require('./jwt')// 注册路由const router = express.Router()// 对所有路由进行 jwt 认证router.use(jwtAuth)

在 /utils/constans.js 中增加

module.exports = {  // ...  CODE_TOKEN_EXPIRED: -2}

批改 /model/Result.js:

expired(res) {  this.code = CODE_TOKEN_EXPIRED  this.json(res)}

批改自定义异样:

router.use((err, req, res, next) => {  if (err.name === 'UnauthorizedError') {    new Result(null, 'token生效', {      error: err.status,      errorMsg: err.name    }).expired(res.status(err.status))  } else {    const msg = (err && err.message) || '零碎谬误'    const statusCode = (err.output && err.output.statusCode) || 500;    const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message    new Result(null, msg, {      error: statusCode,      errorMsg    }).fail(res.status(statusCode))  }})
前端传入 JWT Token

后端增加路由的jwt认证后,再次申请 /user/info 将抛出401 谬误,这是因为前端未传递正当的Token导致的,上面咱们就批改 /utils/request.js ,使得前端申请时能够传递Token:

service.interceptors.request.use(  config => {    // 如果存在 token 则附带在 http header 中    if (store.getters.token) {      config.headers['Authorization'] = `Bearer ${getToken()}`    }    return config  },  error => {    return Promise.reject(error)  })

前端去掉 /user/info 申请时传入的 token ,因为咱们曾经从header中传入,批改 src/api/user.js

export function getInfo() {  return request({    url: '/user/info',    method: 'get'  })}
用户查问 /user/info API

在 db/index.js 中增加:

function queryOne(sql) {  return new Promise((resolve, reject) => {    querySql(sql)      .then(results => {        if (results && results.length > 0) {          resolve(results[0])        } else {          resolve(null)        }      })      .catch(error => {        reject(error)      })  })}

在 /services/user.js 中增加

function findUser(username) {  const sql = `select * from admin_user where username='${username}'`  return queryOne(sql)}

此时有个问题,前端仅在http Header中传入了Token如果通过Token获取username呢?这里就须要通过对 JWTToken 进行解析,在 /utils/index.js 中增加 decode办法:

const jwt = require('jsonwebtoken')const { PRIVATE_KEY } = require('./constant')function decode(req) {  const authorization = req.get('Authorization')  let token = ''  if (authorization.indexOf('Bearer') >= 0) {    token = authorization.replace('Bearer ', '')  } else {    token = authorization  }  return jwt.verify(token, PRIVATE_KEY)}

批改 /router/user.js

router.get('/info', function(req, res) {  const decoded = decode(req)  if (decoded && decoded.username) {    findUser(decoded.username).then(user => {      if (user) {        user.roles = [user.role]        new Result(user, '获取用户信息胜利').success(res)      } else {        new Result('获取用户信息失败').fail(res)      }    })  } else {    new Result('用户信息解析失败').fail(res)  }})

此时在前端从新登录,终于登录胜利

批改Logout办法

批改 src/store/modules/user.js:

logout({ commit, state, dispatch }) {    return new Promise((resolve, reject) => {      try {        commit('SET_TOKEN', '')        commit('SET_ROLES', [])        removeToken()        resetRouter()        // reset visited views and cached views        // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485        dispatch('tagsView/delAllViews', null, { root: true })        resolve()      } catch (e) {        reject(e)      }    })}
对于 RefreshToken

如果你的场景须要受权给第三方app,呢么咱们通常须要在减少一个RefreshToken的API,该API的用处是依据现有的Token获取用户名,而后生成一个新的Token,这样做的目标是为了避免Token生效后退出登录,所以app个别会在关上时刷新一次Token,该api的实现办法比较简单,所需的技术之前都曾经介绍过,大家能够参考之前的文档进行实现

电子书上传

创立上传页面组件

电子书上传过程分为新增电子书和编辑电子书,新增:

<template>  <detail :is-edit="false" /></template><script>import Detail from './components/Detail'export default {  name: 'CreateBook',  components: { Detail }}</script>

编辑:

<template>  <article-detail :is-edit /></template><script>import Detail from './components/Detail'export default {  name: 'EditBook',  components: { Detail }}</script>

Detail组件比较复杂,咱们逐渐实现,首先实现Detail的大体布局,包含一个el-form和sticky导航栏 sticky导航栏在内容较多时会产生吸顶成果:

<div class="detail">    <el-form ref="postForm" :model="postForm" :rules="rules" class="form-container">      <sticky :z-index="10" :class-name="'sub-navbar ' + postForm.status">        <el-button v-if="!isEdit" @click.prevent.stop="showGuide">显示帮忙</el-button>        <el-button v-loading="loading" style="margin-left: 10px;" type="success" @click="submitForm">          {{ isEdit ? '编辑电子书' : '新增电子书' }}        </el-button>      </sticky>      <div class="detail-container">        <el-row>          <Warning />          <el-col :span="24">            <!-- 编写具体表单控件 -->                    </el-col>          <el-col :span="24">            <!-- 编写具体表单控件 -->                    </el-col>        </el-row>      </div>    </el-form></div><style lang="scss" scoped>  @import "~@/styles/mixin.scss";  .detail {    position: relative;    .detail-container {      padding: 40px 45px 20px 50px;      .preview-img {        width: 200px;        height: 270px;      }      .contents-wrapper {        padding: 5px 0;      }    }  }</style>
上传组件开发

这里咱们基于 el-upload封装了上传组件EbookUpload,基于EbookUpload咱们再实现上传组件就非常容易了:

  <div class="upload-container">    <el-upload      :action="action"      :headers="headers"      :multuple="false"      :limit="1"      :before-upload="beforeUpload"      :on-success="onSuccess"      :on-error="onError"      :on-remove="onRemove"      :file-list="fileList"      :on-exceed="onExceed"      :disabled="disabled"      drag      show-file-list      accept="application/epub+zip"      class="image-upload"    >      <i class="el-icon-upload"></i>      <div class="el-upload__text" v-if="fileList.length === 0">请将电子书拖入或 <em>点击上传</em></div>     <div v-else class="el-upload__text">图书已上传</div>      </el-upload>  </div>

上传失败事件

    onError(err) {      const errMsg = err.message && JSON.parse(err.message)      this.$message({        message: (errMsg && errMsg.msg && `上传失败,失败起因:${errMsg.msg}`) || '上传失败',        type: 'error'      })      this.$emit('onError', err)    },        onExceed() {      this.$message({        message: '每次只能上传一本电子书',        type: 'warning'      })    }
上传API开发

指定目标nginx上传门路,这样做的益处是一旦电子书拷贝到指定目录下后,,就能够通过nginx生成下载链接:
新建 /utils/env.js

module.exports = {    env:'dev'}const { env } = require('./env')const UPLOAD_PATH = env === 'dev' ?  '/Users/sam/upload/admin-upload-ebook' :  '/root/upload/admin-upload-ebook'

装置multer:上传
`const multer = require('multer')
`
上传API

router.post(  '/upload',  multer({ dest: `${UPLOAD_PATH}/book` }).single('file'),  function(req, res, next) {    if (!req.file || req.file.length === 0) {      new Result('上传电子书失败').fail(res)    } else {      const book = new Book(req.file)      book.parse()        .then(book => {          new Result(book.toJson(), '上传胜利').success(res)        })        .catch((err) => {          console.log('/book/upload', err)          next(boom.badImplementation(err))          book.reset()        })    }  })
上传组件表单

图书表单包含以下信息:

  • 书名
  • 作者
  • 出版社
  • 语言
  • 根文件
  • 文件门路
  • 解压门路
  • 封面门路
  • 文件名称
  • 封面
  • 目录
        <el-col :span="24">          <el-form-item prop="title">            <MdInput v-model="postFrom.title" :maxlength="100" name="name" required>书名</MdInput>          </el-form-item>          <el-row>            <el-col :span="12">              <el-form-item :label-width="labelWidth" label="作者:">                <el-input                  v-model="postFrom.author"                  placeholder="作者"                  style="width: 100%"                />              </el-form-item>            </el-col>            <el-col :span="12">              <el-form-item :label-width="labelWidth" label="出版社:">                <el-input v-model="postFrom.publisher" placeholder="出版社" style="width: 100%"/>              </el-form-item>            </el-col>          </el-row>          <el-row>            <el-col :span="12">              <el-form-item label="语言:" :label-width="labelWidth">                <el-input v-model="postFrom.language" placeholder="语言" />              </el-form-item>            </el-col>            <el-col :span="12">              <el-form-item label="根文件:" :label-width="labelWidth">                <el-input v-model="postFrom.rootFile" placeholder="根文件" disabled />              </el-form-item>            </el-col>          </el-row>          <el-row>            <el-col :span="12">              <el-form-item label="文件门路:" :label-width="labelWidth">                <el-input v-model="postFrom.filePath" placeholder="文件门路" disabled />              </el-form-item>            </el-col>            <el-col :span="12">              <el-form-item label="解压门路:" :label-width="labelWidth">                <el-input v-model="postFrom.unzipPath" placeholder="解压门路" disabled />              </el-form-item>            </el-col>          </el-row>          <el-row>            <el-col :span="12">              <el-form-item label="封面门路:" :label-width="labelWidth">                <el-input v-model="postFrom.language" placeholder="封面门路" />              </el-form-item>            </el-col>            <el-col :span="12">              <el-form-item label="文件名称:" :label-width="labelWidth">                <el-input v-model="postFrom.rootFile" placeholder="文件名称" />              </el-form-item>            </el-col>          </el-row>          <el-row>            <el-col :span="24">              <el-form-item label="封面:" :label-width="labelWidth">                <a v-if="postFrom.cover" :href="postFrom.cover" target="_blank">                  ![](postForm.cover)                </a>                <span v-else>无</span>              </el-form-item>            </el-col>          </el-row>          <el-row>            <el-col :span="24">              <el-form-item label="目录:" :label-width="labelWidth">                <div v-if="postFrom.contents && postFrom.contents.length > 0" class="contents_warpper">                  <el-tree class="" />                </div>                <span v-else>无</span>              </el-form-item>            </el-col>          </el-row>        </el-col>

电子书解析计划

构造函数
Book分为两种场景,第一种是间接从电子书文件中解析出Book对象,第二种是从data对象生成Book对象

constructor(file, data) {    if (file) {      this.createBookFromFile(file)    } else if (data) {      this.createBookFromData(data)    }}
从文件创建Book对象

从文件读取电子书后,初始化Book对象
createBookFromFile(file) {

const {  destination: des, // 文件本地存储目录  filename, // 文件名称  mimetype = MIME_TYPE_EPUB // 文件资源类型} = fileconst suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : ''const oldBookPath = `${des}/${filename}`const bookPath = `${des}/${filename}${suffix}`const url = `${UPLOAD_URL}/book/${filename}${suffix}`const unzipPath = `${UPLOAD_PATH}/unzip/${filename}`const unzipUrl = `${UPLOAD_URL}/unzip/${filename}`if (!fs.existsSync(unzipPath)) {  fs.mkdirSync(unzipPath, { recursive: true }) // 创立电子书解压后的目录}if (fs.existsSync(oldBookPath) && !fs.existsSync(bookPath)) {  fs.renameSync(oldBookPath, bookPath) // 重命名文件}this.fileName = filename // 文件名this.path = `/book/${filename}${suffix}` // epub文件门路this.filePath = this.path // epub文件门路this.url = url // epub文件urlthis.title = '' // 题目this.author = '' // 作者this.publisher = '' // 出版社this.contents = [] // 目录this.cover = '' // 封面图片URLthis.category = -1 // 分类IDthis.categoryText = '' // 分类名称this.language = '' // 语种this.unzipPath = `/unzip/${filename}` // 解压后的电子书目录this.unzipUrl = unzipUrl // 解压后的电子书链接this.originalName = file.originalname

}

从数据创立Book对象

从表单对象中创立Book对象

createBookFromData(data) {    this.fileName = data.fileName    this.cover = data.coverPath    this.title = data.title    this.author = data.author    this.publisher = data.publisher    this.bookId = data.fileName    this.language = data.language    this.rootFile = data.rootFile    this.originalName = data.originalName    this.path = data.path || data.filePath    this.filePath = data.path || data.filePath    this.unzipPath = data.unzipPath    this.coverPath = data.coverPath    this.createUser = data.username    this.createDt = new Date().getTime()    this.updateDt = new Date().getTime()    this.updateType = data.updateType === 0 ? data.updateType : UPDATE_TYPE_FROM_WEB    this.contents = data.contents}
电子书解析

初始化后,能够调用Book实例的parse 办法解析电子书,这里咱们应用了epub库,咱们间接将epub源码集成到我的项目中:

epub库集成

epub库源码 https://github.com/julien-c/epub
咱们间接将 epub.js 拷贝到 /utils/epub.js

epub库获取图片逻辑批改
getImage(id, callback) {    if (this.manifest[id]) {      if ((this.manifest[id]['media-type'] || '').toLowerCase().trim().substr(0, 6) != 'image/') {        return callback(new Error('Invalid mime type for image'))      }      this.getFile(id, callback)    } else {      const coverId = Object.keys(this.manifest).find(key => (        this.manifest[key].properties === 'cover-image'))      if (coverId) {        this.getFile(coverId, callback)      } else {        callback(new Error('File not found'))      }    }};
应用epub库解析电子书

parse() {

return new Promise((resolve, reject) => {  const bookPath = `${UPLOAD_PATH}${this.path}`  if (!this.path || !fs.existsSync(bookPath)) {    reject(new Error('电子书门路不存在'))  }  const epub = new Epub(bookPath)  epub.on('error', err => {    reject(err)  })  epub.on('end', err => {    if (err) {      reject(err)    } else {      let {        title,        language,        creator,        creatorFileAs,        publisher,        cover      } = epub.metadata      // title = ''      if (!title) {        reject(new Error('图书题目为空'))      } else {        this.title = title        this.language = language || 'en'        this.author = creator || creatorFileAs || 'unknown'        this.publisher = publisher || 'unknown'        this.rootFile = epub.rootFile        const handleGetImage = (error, imgBuffer, mimeType) => {          if (error) {            reject(error)          } else {            const suffix = mimeType.split('/')[1]            const coverPath = `${UPLOAD_PATH}/img/${this.fileName}.${suffix}`            const coverUrl = `${UPLOAD_URL}/img/${this.fileName}.${suffix}`            fs.writeFileSync(coverPath, imgBuffer, 'binary')            this.coverPath = `/img/${this.fileName}.${suffix}`            this.cover = coverUrl            resolve(this)          }        }        try {          this.unzip() // 解压电子书          this.parseContents(epub)            .then(({ chapters, chapterTree }) => {              this.contents = chapters              this.contentsTree = chapterTree              epub.getImage(cover, handleGetImage) // 获取封面图片            })            .catch(err => reject(err)) // 解析目录        } catch (e) {          reject(e)        }      }    }  })  epub.parse()  this.epub = epub})

}

电子书目录解析

电子书解析过程中咱们须要自定义电子书目录,第一步须要解压电子书:

unzip() {    const AdmZip = require('adm-zip')    const zip = new AdmZip(Book.genPath(this.path)) // 解析文件门路    zip.extractAllTo(      /*target path*/Book.genPath(this.unzipPath),      /*overwrite*/true    )}

genPath 是 Book 的一个属性办法,咱们能够应用 es6 的 static 属性来实现:

static genPath(path) {    if (path.startsWith('/')) {      return `${UPLOAD_PATH}${path}`    } else {      return `${UPLOAD_PATH}/${path}`    }}

电子书目录解析算法:

parseContents(epub) {    function getNcxFilePath() {      const manifest = epub && epub.manifest      const spine = epub && epub.spine      const ncx = manifest && manifest.ncx      const toc = spine && spine.toc      return (ncx && ncx.href) || (toc && toc.href)    }    /**     * flatten办法,将目录转为一维数组     *     * @param array     * @returns {*[]}     */    function flatten(array) {      return [].concat(...array.map(item => {        if (item.navPoint && item.navPoint.length) {          return [].concat(item, ...flatten(item.navPoint))        } else if (item.navPoint) {          return [].concat(item, item.navPoint)        } else {          return item        }      }))    }    /**     * 查问当前目录的父级目录及规定档次     *     * @param array     * @param level     * @param pid     */    function findParent(array, level = 0, pid = '') {      return array.map(item => {        item.level = level        item.pid = pid        if (item.navPoint && item.navPoint.length) {          item.navPoint = findParent(item.navPoint, level + 1, item['$'].id)        } else if (item.navPoint) {          item.navPoint.level = level + 1          item.navPoint.pid = item['$'].id        }        return item      })    }    if (!this.rootFile) {      throw new Error('目录解析失败')    } else {      const fileName = this.fileName      return new Promise((resolve, reject) => {        const ncxFilePath = Book.genPath(`${this.unzipPath}/${getNcxFilePath()}`) // 获取ncx文件门路        const xml = fs.readFileSync(ncxFilePath, 'utf-8') // 读取ncx文件        // 将ncx文件从xml转为json        xml2js(xml, {          explicitArray: false, // 设置为false时,解析后果不会包裹array          ignoreAttrs: false  // 解析属性        }, function(err, json) {          if (!err) {            const navMap = json.ncx.navMap // 获取ncx的navMap属性            if (navMap.navPoint) { // 如果navMap属性存在navPoint属性,则阐明目录存在              navMap.navPoint = findParent(navMap.navPoint)              const newNavMap = flatten(navMap.navPoint) // 将目录拆分为扁平构造              const chapters = []              epub.flow.forEach((chapter, index) => { // 遍历epub解析进去的目录                // 如果目录大于从ncx解析进去的数量,则间接跳过                if (index + 1 > newNavMap.length) {                  return                }                const nav = newNavMap[index] // 依据index找到对应的navMap                chapter.text = `${UPLOAD_URL}/unzip/${fileName}/${chapter.href}` // 生成章节的URL                // console.log(`${JSON.stringify(navMap)}`)                if (nav && nav.navLabel) { // 从ncx文件中解析出目录的题目                  chapter.label = nav.navLabel.text || ''                } else {                  chapter.label = ''                }                chapter.level = nav.level                chapter.pid = nav.pid                chapter.navId = nav['$'].id                chapter.fileName = fileName                chapter.order = index + 1                chapters.push(chapter)              })              const chapterTree = []              chapters.forEach(c => {                c.children = []                if (c.pid === '') {                  chapterTree.push(c)                } else {                  const parent = chapters.find(_ => _.navId === c.pid)                  parent.children.push(c)                }              }) // 将目录转化为树状构造              resolve({ chapters, chapterTree })            } else {              reject(new Error('目录解析失败,navMap.navPoint error'))            }          } else {            reject(err)          }        })      })    }}
电子书解析算法优化
        function getNcxFilePath() {            const spine = epub && epub.spine            const ncx = spine.toc && spine.toc.href            const id = spine.toc && spine.toc.id            if (ncx) {                return ncx            } else {                return manifest[id].href            }        }        function findParent(array, level = 0, pid = '') {            return array.map(item => {                item.level = level                item.pid = pid                if (item.navPoint && item.navPoint.length > 0) {                    item.navPoint = findParent(item.navPoint, level + 1, item['$'].id)                } else if (item.navPoint) {                    item.navPoint.level = level + 1                    item.navPoint.pid = item['$'].id                }                return item            })        }        function flatten(array) {            return [].concat(...array.map(item => {                if (item.navPoint && item.navPoint.length > 0) {                    return [].concat(item, ...flatten(item.navPoint))                } else if (item.navPoint) {                    return [].concat(item, item.navPoint)                }                return item            }))        }        const ncxFilePath = Book.genPath(`${this.unzipPath}/${getNcxFilePath()}`)        console.log(ncxFilePath)        if (fs.existsSync(ncxFilePath)) {            return new Promise((resolve, reject) => {                const fileName = this.fileName                const xml = fs.readFileSync(ncxFilePath, 'utf-8')                const dir = path.dirname(ncxFilePath).replace(UPLOAD_PATH,'')                parseString(xml, {explicitArray: false, ignoreAttrs: false}, function (err, json) {                    if (err) {                        reject(err)                    } else {                        const navMap = json.ncx.navMap                        if (navMap.navPoint && navMap.navPoint.length > 0) {                            navMap.navPoint = findParent(navMap.navPoint)                            const newNavMap = flatten(navMap.navPoint)                            const chapters = []                            newNavMap.forEach((chapter, index) => {                                const  src = chapter.content['$'].src                                console.log(src)                                chapter.text = `${UPLOAD_URL}${dir}/${src}`                                chapter.label = chapter.navLabel.text || ''                                chapter.navId = chapter['$'].id                                chapter.fileName = fileName                                chapter.order = index + 1                                chapters.push(chapter)                            })                            const chapterTree = []                            chapters.forEach(c => {                                c.children = []                                if (c.pid === '') {                                    chapterTree.push(c)                                } else {                                    const parent = chapters.find(_ => _.navId === c.pid)                                    parent.children.push(c)                                }                            })                            resolve({chapters, chapterTree})                        } else {                            reject(new Error('目录解析失败,目录数为0'))                        }                    }                })            });        } else {        }    }

电子书树状构造解析

  <el-tree :data="contentsTree" @node-click="onContentClick" />

点击关上章节信息

    onContentClick(data) {      console.log(data)      if (data.text) {        window.open(data.text)      }    },

电子书表单验证

    onContentClick(data) {      console.log(data)      if (data.text) {        window.open(data.text)      }    }    onContentClick(data) {      console.log(data)      if (data.text) {        window.open(data.text)      }    },

电子书新增逻辑

    submitForm() {      if (!this.loading) {        this.$refs.postForm.validate((valid, fields) => {          console.log(valid)          if (valid) {            this.loading = true            const book = Object.assign({}, this.postForm)            delete book.contentsTree            if (!this.isEdit) {              createBook(book).then(response => {                const { msg } = response                this.$notify({ title: '增加胜利', message: msg, type: 'success', duration: 2000 })                this.loading = false                this.setDefault()              }).catch(() => {                this.loading = false              })            } else {              // updateBook()            }          } else {            const message = fields[Object.keys(fields)[0]][0].message            this.$message({ message, type: 'error' })          }          // this.loading = false        })      }    }

在路由中减少/book/create 接口

router.post('/create',function (req,res,next) {    const  decode = decoded(req)    if (decode && decode.username){        req.body.username = decode.username    }    const book = new Book(null,req.body)    bookService.insertBook(book).then(()=>{        console.log('增加电子书胜利')        new Result('增加电子书胜利').success(res)    }).catch(err=>{        next(boom.badImplementation(err))    })})

解析电子书内容 应用db数据库插入电子书内容

function insertBook(book) {    return new Promise(async (resolve, reject) => {        try {            if (book instanceof Book) {                const result = await exists(book)                console.log('电子书----',result)                if (result) {                    await removeBook(book)                    reject(new Error('电子书已存在'))                } else {                    await db.insert(book.toDb(), 'book')                    await insertContents(book)                    resolve()                }            } else {                reject(new Error('增加的图书对象不非法'))            }        } catch (e) {            reject(e)        }    })}

如果电子书存在就删除此次上传的 同时查问是否已插入数据库 如果已插入则把数据删除

async function removeBook(book) {   if (book){       book.reset()    if (book.fileName){        const  removeBookSql = `delete from book where filename='${book.fileName}'`        const  removeContentsSql = `delete from book where filename='${book.fileName}'`        await  db.querySql(removeBookSql)        await  db.querySql(removeContentsSql)    }   }}
电子书查问

批改前端 /router/index中的edit路由 减少fileName路由参数

 {        name: 'bookEdit',        path: '/book/edit/:fileName',        component: () => import('@/views/book/edit'),        hidden: true,        meta: { title: '编辑图书', icon: 'edit', roles: ['admin'], activeMenu: '/book/list'        }      },

通过vue生命周期 created 申请book信息

  created() {    if (this.isEdit) {      const fileName = this.$route.params.fileName      this.getBookData(fileName)    }  }  export function getBook(fileName) {  return request({    url: '/book/get',    method: 'get',    params: { fileName: fileName }  })}

在后盾增加路由 /book/get

router.get('/get',function (req,res,next) {    const  {fileName} = req.query  if (!fileName){      next(boom.badRequest(new Error('参数fileName不能为空')))  }else {     bookService.getBook(fileName).then(book=>{         new Result(book,'获取图书信息胜利').success(res)     }).catch(err=>{         next(boom.badImplementation(err))     })  }})function getBook(fileName){   return new Promise(async (resolve, reject) => {       const  bookSql = `select  * from book where filename='${fileName}'`       const  contentsSql = `select * from contents where filename='${fileName}' order by \`order\``      const book = await  db.queryOne(bookSql)      const contents = await  db.querySql(contentsSql)       if (book){           book.cover = Book.getCoverUrl(book)       }       console.log('book',book)       resolve({book})   })}

敞开mysql MySQL GROUP BY 的问题

17-3
阿里云购买及环境搭建

生成ssh公钥 ssh-keygen -t rsa
拷贝到服务器 ssh-copy-id -i ~/.ssh/id_rsa.pub root@123.56.163.191
批改 ssh配置避免断链 vim /etc/ssh/sshd_config 增加 ClientAliveInterval 30
重启ssh配置项 restart sshd.service
`curl -o- https://raw.githubusercontent... | bash
`
配置

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completionsource ~/.bash_profile 

装置node nodejs环境搭建

nvm install nodenpm install -g cnpm --registry=https://registry.npm.taobao.org
nginx服务器

装置依赖

yum -y install pcre*yum -y install openssl

创立nginx默认配置

cd /usr/share/nginx/touch nginx.conf

批改nginx 将user改为root 并增加配置文件
include /usr/share/nginx/*.conf;
批改主配置文件端口号9000
nginx个性化配置

server {   listen 80:   server_name localhost;   root /usr/share/nginx/upload;   autoindex on;   add_header Cache-Control "no-cache,must-revalidate";   location / {   add_header Access-Control-Allow-Origin *;   }    }

应用fileZilla 上传文件到服务器

装置git

yum install -y git

创立imooc-ebook目录
`mkdir imooc-ebook
cd imooc-ebook
`
更新git版本
装置依赖

yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel asciidocyum install  gcc perl-ExtUtils-MakeMakerwget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.4.tar.xztar -xvf git-2.9.4.tar.xzcd git-2.9.4编译并链接源码gitmake prefix=/usr/local/git allmake prefix=/usr/local/git installcd /usr/binln -s  /usr/local/git/bin/git git

git免密配置
ssh-keygen -t rsa -C "243100134_gg"
增加秘钥到云服务期
装置mysql
mysql卸载
mysql装置
老手必备

wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm # rpm -ivh mysql-community-release-el7-5.noarch.rpm

yum -y install mysql-server
service nysqld restart
cat /var/log/mysqld.log |grep password
批改默认明码

USE mysql ; UPDATE user SET Password = password ( 'new-password' ) WHERE User = 'root' ; 

增加3306端口
linux 查找文件夹
find / -name 'admin-vue-imooc-book' -type d
linux 挪动文件夹

17-7