共计 56006 个字符,预计需要花费 141 分钟才能阅读完成。
小慕读书 我的项目开发
下载查看 vue-elemen-admin 源码
git clone https://github.com/PanJiaChen/vue-element-admin
cd vue-element-admin
npm 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-node
cd admin-imooc-node
npm 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
批改一:增加以后登录用户为 owneruser 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.redirect
const _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.js
qiniu.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 = 18082
httpsServer.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.js
config.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 // 文件资源类型
} = file
const 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 文件 url
this.title = '' // 题目
this.author = '' // 作者
this.publisher = '' // 出版社
this.contents = [] // 目录
this.cover = '' // 封面图片 URL
this.category = -1 // 分类 ID
this.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_completion
source ~/.bash_profile
装置 node nodejs 环境搭建
nvm install node
npm 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 asciidoc
yum install gcc perl-ExtUtils-MakeMaker
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.4.tar.xz
tar -xvf git-2.9.4.tar.xz
cd git-2.9.4
编译并链接源码 git
make prefix=/usr/local/git all
make prefix=/usr/local/git install
cd /usr/bin
ln -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 restartcat /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