一、电子书编辑
1.表单规则校验
修改src\views\book\components\Detail.vue
:
- 新增
:rules="rules
,为可编辑表单项新增prop
<template>
<el-form ref="postForm" :model="postForm" :rules="rules">
...
<div class="detail-container">
<el-row>
...
<el-col :span="24">
<el-form-item style="margin-bottom: 40px;" prop="title">
...
</el-form-item>
<div>
<el-row>
<el-col :span="12" class="form-item-author">
<el-form-item prop="author" :label-width="labelWidth" label="作者:">
...
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="publisher" :label-width="labelWidth" label="出版社:">
...
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item prop="language" :label-width="labelWidth" label="语言:">
...
</el-form-item>
</el-col>
...
</el-row>
...
</div>
</el-col>
</el-row>
</div>
</el-form>
</template>
创建表单规则校验逻辑:
const validateRequire = (rule, value, callback) => {
if (value === '') {
this.$message({
message: rule.field + '为必传项',
type: 'error'
})
callback(new Error(rule.field + '为必传项'))
} else {
callback()
}
}
const validateSourceUri = (rule, value, callback) => {
if (value) {
if (validURL(value)) {
callback()
} else {
this.$message({
message: '外链url填写不正确',
type: 'error'
})
callback(new Error('外链url填写不正确'))
}
} else {
callback()
}
}
return {
postForm: Object.assign({}, defaultForm),
rules: {
image_uri: [{ validator: validateRequire }],
title: [{ validator: validateRequire }],
content: [{ validator: validateRequire }],
source_uri: [{ validator: validateSourceUri, trigger: 'blur' }]
}
}
}
2.提交表单
提交表单时需要提供两个接口,createBook
和 updateBook
新建src\api\book.js
:
import request from '../utils/request'
export function createBook(book) {
return request({
url: '/book/create',
method: 'post',
data: book
})
}
修改src\views\book\components\Detail.vue
的submitForm方法:
submitForm() {
this.$refs.postForm.validate(valid => {
if (valid) {
this.loading = true
const book = Object.assign({}, this.postForm)
delete book.contents
if (!this.isEdit) {
createBook(book).then(response => {
this.loading = false
this.$notify({
title: '成功',
message: response.msg,
type: 'success',
duration: 2000
})
this.toDefault()
}).catch(() => {
this.loading = false
})
} else {
updateBook(book).then(response => {
console.log('updateBook', response)
this.loading = false
this.$notify({
title: '成功',
message: response.msg,
type: 'success',
duration: 2000
})
}).catch(() => {
this.loading = false
})
}
} else {
return false
}
})
}
在router\book.js
新建api:create:
const { decode } = require('../utils')
...
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)
}
)
在utils\constant.js
中新增:
UPDATE_TYPE_FROM_WEB: 1
在models\Book.js
中新增:
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
this.category = data.category || 99
this.categoryText = data.categoryText || '自定义'
}
新建services\book.js
:
const Book = require('../models/Book')
const db = require('../db')
function exists(book) {
return false
}
function removeBook(book) {}
function insertContents(book) {}
function insertBook(book) {
return new Promise(async (resolve, reject) => {
try {
if (book instanceof Book) {
const result = await exists(book)
if (result) {
await removeBook(book)
reject(new Error('电子书已存在'))
} else {
await db.insert(book, 'book')
await insertContents(book)
resolve()
}
} else {
reject(new Error('添加的图书对象不合法'))
}
} catch (e) {
reject(e)
}
})
}
module.exports = {
insertBook
}
修改router\book.js
:
const bookService = require('../services/book')
...
router.post(
'/create',
function(req, res, next) {
...
bookService.insertBook(book).then(() => {
new Result(book, '上传电子书成功').success(res)
}).catch(err => {
next(boom.badImplementation(err)) // 500, Internal Server Error
})
}
)
在db\index.js
中新增insert
方法:
function insert(model, tableName) {
return new Promise((resolve, reject) => {
if (!isObject(model)) {
reject(new Error('插入数据库失败,插入数据非对象'))
} else {
const keys = []
const values = []
Object.keys(model).forEach(key => {
if (model.hasOwnProperty(key)) { // 检查model自身是否包含key这个属性(继承来的不算)
keys.push(`\`${key}\``) // 避免key与sql的关键字重复
values.push(`'${model[key]}'`)
}
})
if (keys.length > 0 && values.length > 0) {
let sql = `INSERT INTO \`${tableName}\` (`
const keysString = keys.join(',')
const valuesString = values.join(',')
sql = `${sql}${keysString}) VALUES (${valuesString})`
debug && console.log(sql)
const conn = connect()
try {
conn.query(sql, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
} catch (e) {
reject(e)
} finally {
conn.end() // 注意一定要加,否则会造成内存泄漏
}
} else {
reject(new Error('插入数据库失败,对象中没有任何属性'))
}
}
})
}
类型判断小技巧:
models\Book.js
中新增方法toDb
,以免在插入数据库过程中book对象相对book表出现冗余项:
toDb() {
return {
fileName: this.fileName,
cover: this.cover,
title: this.title,
author: this.author,
publisher: this.publisher,
bookId: this.bookId,
updateType: this.updateType,
language: this.language,
rootFile: this.rootFile,
originalName: this.originalName,
filePath: this.path,
unzipPath: this.unzipPath,
coverPath: this.coverPath,
createUser: this.createUser,
createDt: this.createDt,
updateDt: this.updateDt,
category: this.category,
categoryText: this.categoryText
}
}
修改services\book.js
:(await db.insert(book.toDb(), 'book')
):
function insertBook(book) {
return new Promise(async (resolve, reject) => {
try {
if (book instanceof Book) {
...
if (result) {...} else {
await db.insert(book.toDb(), 'book')
await insertContents(book)
resolve()
}
} else {...}
} catch (e) {...}
})
}
重启服务端,上传一本电子书,点击新增电子书,在数据库中便可看到新增数据:
目前的问题是,新增电子书之后需要表单清空,调用toDefault
只是清空了表单内容,校验结果还保留:
查看官网说明:https://element.eleme.cn/#/zh-CN/component/form:
在toDefault()
中加入this.$refs.postForm.resetFields()
,去掉this.postForm = Object.assign({}, defaultForm)
,并为每个表单项(el-form-item
)新增prop
此时再点新增电子书之后表单就会直接清空。
在服务端的models\Book.js
中新增getContents
方法:
getContents() {
return this.contents
}
3.将目录存入数据库
操作之前我们需要先去掉前端中src\views\book\components\Detail.vue
的submitForm
方法中的这一句:delete book.contents
由于直接从文件中获取的目录数据有多个冗余字段,因此在存入数据库之前需要将其冗余字段去掉,为了方便使用我们在服务端安装lodash
库
cnpm i -S lodash
在services\book.js
中引入lodash
并新建insertContents
方法:
async function insertContents(book) {
const contents = book.getContents()
if (contents && contents.length > 0) {
for (let i = 0; i < contents.length; i++) {
// 项目内容有可能包含单引号,给它替换成空格,以免影响插入数据库
for (const key in contents[i]) {
contents[i][key] = contents[i][key].toString().replace('\'', ' ')
}
const content = contents[i]
// 剔除冗余项
const _content = _.pick(content, [
'fileName',
'id',
'text',
'href',
'order',
'level',
'label',
'pid',
'navId'
])
console.log('_content', _content)
await db.insert(_content, 'contents')
}
}
}
调试过程中,可能会遇到目录项比较多的书,会产生报错:413 Payload Too Large
,解决方法:在服务端的app.js
中修改如下两句(新增limit: '50mb'
):
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }))
app.use(bodyParser.json({limit: '50mb'}))
数据量大那肯定会有超时问题(timeout of 5000ms exceeded),因此修改src\utils\request.js
的如下部分(timeout: 20 * 1000):
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 20 * 1000
})
这样便可成功。对比控制台输出的content和经过处理的_content,发现冗余字段已经清理干净
点击新增电子书之后查看数据库已经成功插入数据
二、电子书在数据库中查重
判断电子书在数据库中是否已经存在,需要从三方面着手:书名、作者、出版社。在services\book.js
中修改如下内容:
function exists(book) {
const { title, author, publisher } = book
const sql = `select * from book where title='${title}' and author='${author}' and publisher='${publisher}'`
return db.queryOne(sql)
}
如果电子书在数据库中已存在,从book表和contents表中移除相关内容:
async function removeBook(book) {
if (book) {
book.reset()
if (book.fileName) {
const removeBookSql = `delete from book where fileName='${book.fileName}'`
const removeContentsSql = `delete from contents where fileName='${book.fileName}'`
await db.querySql(removeBookSql)
await db.querySql(removeContentsSql)
}
}
}
上面代码中用到了book对象的reset方法(models\Book.js
)用来删除服务端刚刚上传的已重复的电子书相关资源(epub
、cover
、unzip
),同时在models\Book.js
中新增pathExists
方法搭配fs.existsSync
用来判断文件路径是否存在:
reset() {
if (this.path && Book.pathExists(this.path)) {
fs.unlinkSync(Book.genPath(this.path))
}
if (this.filePath && Book.pathExists(this.filePath)) {
fs.unlinkSync(Book.genPath(this.filePath))
}
if (this.coverPath && Book.pathExists(this.coverPath)) {
fs.unlinkSync(Book.genPath(this.coverPath))
}
if (this.unzipPath && Book.pathExists(this.unzipPath)) {
// 注意node低版本将不支持第二个属性(迭代删除)
fs.rmdirSync(Book.genPath(this.unzipPath), { recursive: true })
}
}
static pathExists(path) {
if (path.startsWith(UPLOAD_PATH)) {
return fs.existsSync(path)
} else {
return fs.existsSync(Book.genPath(path))
}
}
fs.unlinkSync
用来删除对应路径的文件fs.rmdirSync
用来删除对应路径的文件夹
上传电子书,新增电子书,成功,再次上传同一本,上传成功,新增电子书,新增失败,报错电子书已存在。
三、电子书查询
在前端项目中修改src\router\index.js
,在路由/book/edit
后新加参数/:fileName
:
export const asyncRoutes = [
{
....
children: [
...
{
path: '/book/edit/:fileName', // 访问路径
...
},
...
]
}
]
此时,路由/book/edit
无法直接访问(404),必须填入参数fileName
。
在src\views\book\components\Detail.vue
中新增生命周期函数created
:
created() {
if (this.isEdit) {
const fileName = this.$route.params.fileName
this.getBookData(fileName)
}
},
创建方法getBookData
:
getBookData(fileName) {
getBook(fileName).then(res => {
this.setData(res.data)
})
}
在src\api\book.js
中新增:
export function getBook(fileName) {
return request({
url: '/book/get',
method: 'get',
params: { fileName }
})
}
post
用data
传参,get
使用params
此时访问http://localhost:9527/#/book/edit/f138cd4a0b5fb137b21dcbab021fe799,显示接口不存在,请求地址是:https://book.aimooc.top:18082/book/get?fileName=f138cd4a0b5fb137b21dcbab021fe799
在服务端的router\book.js
添加:
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))
})
}
})
在router\book.js
中添加:
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)
resolve(book)
})
}
重启服务端,刷新浏览页,其他信息查询成功,只差封面和目录:
在utils\constant.js
中新增OLD_UPLOAD_URL
,用来兼容之前项目的cover
url
:
const OLD_UPLOAD_URL = env === 'dev' ? 'https://book.aimooc.top/book/res/img' : 'https://book.gaowenda.cn/book/res/img'
在models\Book.js
新增:
static genCoverUrl(book) {
console.log('genCoverUrl', book)
if (+book.updateType === 0) {
const { cover } = book
if (cover) {
if (cover.startsWith('/')) {
return `${OLD_UPLOAD_URL}${cover}`
} else {
return `${OLD_UPLOAD_URL}/${cover}`
}
} else {
return null
}
} else {
if (book.cover) {
if (book.cover.startsWith('/')) {
return `${UPLOAD_URL}${book.cover}`
} else {
return `${UPLOAD_URL}/${book.cover}`
}
} else {
return null
}
}
}
在router\book.js
的getBook
中添加:
function getBook(fileName) {
return new Promise(async (resolve, reject) => {
...
const contents = await db.querySql(contentsSql)
if (book) { // new
book.cover = Book.genCoverUrl(book) // new
book.contentsTree = Book.genContentsTree(contents) // new
resolve(book) // 有修改
} else { // new
reject(new Error('电子书不存在')) // new
} // new
})
}
在models\Book.js
新增:
static genContentsTree(contents ) {
if (contents) {
const contentsTree = []
// 将目录转化为树状结构
contents.forEach(c => {
c.children = []
if (c.pid === '') {
contentsTree.push(c)
} else {
const parent = contents.find(_ => _.navId === c.pid)
parent.children.push(c)
}
})
return contentsTree
}
}
修改models\Book.js
中parseContents
方法的如下内容:
chapters.forEach(c => {
c.children = []
if (c.pid === '') {
chapterTree.push(c)
} else {
const parent = chapters.find(_ => _.navId === c.pid)
parent.children.push(c)
}
}) // 将目录转化为树状结构
修改为:
const chapterTree = Book.genContentsTree(chapters)
由于我们是直接从数据库中拿到的contentsTree
而不是contents
,因此需要修改前端src\views\book\components\Detail.vue
的目录展示部分(postForm.contents
修改为contentsTree
):
<el-row>
<el-col :span="24">
<el-form-item prop="contents" label-width="60px" label="目录:">
<div
v-if="contentsTree && contentsTree.length > 0"
class="contents-wrapper"
>
<el-tree :data="contentsTree" @node-click="onContentClick" />
</div>
<span v-else>无</span>
</el-form-item>
</el-col>
</el-row>
这样目录便可以显示出来了,点击也可以
四、更新电子书
先来修改前端:
在src\api\book.js
中新增如下内容(用来向服务端发起电子书更新请求):
export function updateBook(book) {
return request({
url: '/book/update',
method: 'post',
data: book
})
}
在src\views\book\components\Detail.vue
的submitForm
方法中新增如下内容:
submitForm() {
...
this.$refs.postForm.validate(valid => {
if (valid) {
...
if (!this.isEdit) {
createBook(book).then(res => {...}).catch(e => {...})
} else {
updateBook(book).then(res => { // new
console.log('updateBook', res) // new
this.$notify({ // new
title: '更新成功', // new
message: res.msg, // new
type: 'success', // new
duration: 2000 // new
}) // new
this.loading = false // new
}).catch(e => { // new
console.log('更新失败', e) // new
this.loading = false // new
}) // new
}
} else {...}
})
},
接下来修改服务端:
在router\book.js
中新增API路由:
router.post(
'/update',
function(req, res, next) {
const decode = decoded(req)
if (decode && decode.username) {
req.body.username = decode.username
}
const book = new Book(null, req.body) // 从data中拿到book对象的内容
bookService.updateBook(book).then(book => {
new Result('更新电子书成功').success(res)
}).catch(err => {
next(boom.badImplementation(err)) // 500, Internal Server Error
})
}
)
在services\book.js
中新增电子书更新逻辑:
function updateBook(book) {
return new Promise(async (resolve, reject) => {
try {
if (book instanceof Book) {
const result = await getBook(book.fileName)
if (result) {
const model = book.toDb()
if (+result.updateType === 0) {
reject(new Error('内置图书不能编辑'))
} else {
await db.update(model, 'book', `where fileName='${book.fileName}'`)
resolve()
}
} else {
reject(new Error('电子书不存在'))
}
} else {
reject(new Error('添加的图书对象不合法'))
}
} catch (e) {
reject(e)
}
})
}
在db\index.js
中新增数据库更新逻辑:
// 更新
function update(model, tableName, where) {
return new Promise((resolve, reject) => {
if (!isObject(model)) {
reject(new Error('插入数据库失败,插入数据非对象'))
} else {
const entry = []
Object.keys(model).forEach(key => {
if (model.hasOwnProperty(key)) {
entry.push(`\`${key}\`='${model[key]}'`)
}
})
if (entry.length > 0) {
let sql = `UPDATE \`${tableName}\` SET`
sql = `${sql} ${entry.join(',')} ${where}`
// debug && console.log(sql)
const conn = connect()
try {
conn.query(sql, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
} catch (e) {
reject(e)
} finally {
conn.end()
}
}
}
})
}
完成之后,访问http://localhost:9527/#/book/edit/6bc72db04f7c9c97cab00e3c400849d2即可更新对应已上传电子书的信息(6bc72db04f7c9c97cab00e3c400849d2
为后台上传过程中自动生成的电子书文件名)