读书后台管理信息系统
0.1
实现登录
设置主页
搭建目录结构基本框架
0.2
实现文件上传
连通数据库
图书列表显示
基本字段的增删查改
0.3
实现文件解析
后端补齐20个页面(包括图表、图标、友链、换肤、反馈、指南等边缘功能)
0.4
实现嵌套目录解析
实现多语言解析
实现大文件解析
前端文件目录(仅展开到三级目录并只显示文件夹):
├─plop-templates
│ ├─component
│ └─view
├─public
├─src
│ ├─api
│ ├─assets
│ │ ├─401_images
│ │ ├─404_images
│ │ └─custom-theme
│ │ └─fonts
│ ├─components
│ │ ├─BackToTop
│ │ ├─Breadcrumb
│ │ ├─Charts
│ │ ├─DndList
│ │ ├─DragSelect
│ │ ├─Dropzone
│ │ ├─EbookUpload
│ │ ├─ErrorLog
│ │ ├─GithubCorner
│ │ ├─Hamburger
│ │ ├─HeaderSearch
│ │ ├─ImageCropper
│ │ ├─JsonEditor
│ │ ├─Kanban
│ │ ├─MarkdownEditor
│ │ ├─MDinput
│ │ ├─Pagination
│ │ ├─PanThumb
│ │ ├─RightPanel
│ │ ├─Screenfull
│ │ ├─Share
│ │ ├─SizeSelect
│ │ ├─Sticky
│ │ ├─SvgIcon
│ │ ├─TextHoverEffect
│ │ ├─ThemePicker
│ │ ├─Tinymce
│ │ ├─Upload
│ │ └─UploadExcel
│ ├─directive
│ │ ├─clipboard
│ │ ├─el-drag-dialog
│ │ ├─el-table
│ │ ├─permission
│ │ └─waves
│ ├─filters
│ ├─icons
│ │ └─svg
│ ├─layout
│ │ ├─components
│ │ └─mixin
│ ├─router
│ │ └─modules
│ ├─store
│ │ └─modules
│ ├─styles
│ ├─utils
│ ├─vendor
│ └─views
└─tests
└─unit
├─components
└─utils
后端文件目录:
D:.
│ .gitignore
│ app.js
│ package-lock.json
│ package.json
│
├─.idea
│ admin-imooc-node.iml
│ cd
│ misc.xml
│ modules.xml
│ tree
│ workspace.xml
│
├─db
│ config.js
│ index.js
│
├─https
│ 6607692_simony.xyz.key
│ 6607692_simony.xyz.pem
│
├─models
│ Book.js
│ Result.js
│
├─node_modules
│
├─router
│ book.js
│ index.js
│ jwt.js
│ user.js
│
├─services
│ book.js
│ user.js
│
└─utils
constant.js
env.js
epub.js
index.js
项目中的技术难点如下:登录模块的用户名密码校验、token 生成、校验和路由过滤、前端 token 校验和重定向,电子书上传模块的文件上传和静态资源服务器配置,电子书解析模块的epub 原理、zip 解压、xml 解析,电子书增删改模块的mysql 数据库应用、前后端异常处理。
后端API处理流程如下。
响应过程封装完毕后,我们需要在数据库中查询用户信息来验证用户名和密码是否准确。这里需要基于 mysql 查询库封装一层 service,用来协调业务逻辑和数据库查询,我们不希望直接把业务逻辑写在 router 中,创建 /service/user.js
电子书上传过程分为新增电子书和编辑电子书。
上传组件开发基于 el-upload 封装了上传组件 EbookUpload,上传图书表单中包括以下信息:
- 书名
- 作者
- 出版社
- 语言
- 根文件
- 文件路径
- 解压路径
- 封面路径
- 文件名称
- 封面
- 目录
上传API开发通过指定目的 nginx 上传路径实现。一旦电子书拷贝到指定目录下后,就可以通过 nginx 生成下载链接。
上传通过node的multer组件实现,包括基本的异常处理。
前端逻辑为上传成功时,会将解析的电子书内容填入表单;删除电子书时,会将电子书表单内容复原;而因为需要将表单复原,所以需要表单的默认值。电子书编辑提交表单时需要提供创建书籍和更新书籍两个接口。
电子书核心解析的核心在于对Book对象的创建和后续处理。
构建Book 对象分为两种场景,第一种是直接从电子书文件中解析出 Book 对象,第二种是从 data 对象中生成 Book 对象。
逻辑分别为从文件读取电子书后,初始化 Book 对象。
createBookFromFile(file) { const { destination, filename, mimetype = MIME_TYPE_EPUB, path, originalname } = file // 电子书的文件后缀名 const suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : '' // 电子书的原有路径 const oldBookPath = path // 电子书的新路径 const bookPath = ${destination}/${filename}${suffix} // 电子书的下载URL const url = ${UPLOAD\_URL}/book/${filename}${suffix} // 电子书解压后的文件夹路径 const unzipPath = ${UPLOAD\_PATH}/unzip/${filename} // 电子书解压后的文件夹URL 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 this.unzipPath = /unzip/${filename} // epub解压后相对路径 this.url = url // epub文件下载链接 this.title = ''// 书名 this.author = ''// 作者 this.publisher = ''// 出版社 this.contents = [] // 目录 this.contentsTree = [] // 树状目录结构 this.cover = ''// 封面图片URL this.coverPath = ''// 封面图片路径 this.category = -1// 分类ID this.categoryText = ''// 分类名称 this.language = ''// 语种 this.unzipUrl = unzipUrl // 解压后文件夹链接 this.originalName = originalname // 电子书文件的原名 } |
---|
和从表单对象中创建 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 : 1 this.category = data.category || 99 this.categoryText = data.categoryText || '自定义' this.contents = data.contents || [] } | | --- |
创建Book对象后,初始化后,可以调用 Book 实例的 parse 方法解析电子书,这里使用了 github上的epub 库,直接将 epub 库源码集成到项目中。epub 库源码为https://github.com/julien-c/epub,并使用epub库解压电子书。解压过程中包括基础的异常处理。
| parse() { returnnew Promise((resolve, reject) => { const bookPath = ${UPLOAD\_PATH}${this.filePath}
if (!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 { const { language, creator, creatorFileAs, title, cover, publisher } = epub.metadata 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 = (err, file, mimeType) => { if (err) { reject(err) } 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, file, '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 (e) { reject(e) } } } }) epub.parse() }) } |
| --- |
电子书解析过程中我们需要自定义电子书目录解析,第一步需要对电子书进行解压,转变为解压后的zip文件。
通过递归式访问来实现嵌套目录解析算法。
图书上传:
大文件:
嵌套目录解析:
多语言:
异常处理:
图书列表:
读书前端系统
0.1
书城、书架页面的基本实现,包括布局、基本结构(使用静态模拟数据进行测试)。实现的页面有:书城首页、搜索页面、搜索结果页面、分学科展示页面、书架页面、听书页、书籍详情页面、精选推荐页面、随机推荐页面、编辑书架内容界面。
0.2
页面UI优化;搜索功能实现;书架书籍添加删除功能实现;各页面切换跳转的检查、调整与优化。
0.3
联调阅读器和书籍详情,使用户可以通过书籍详情页跳转到阅读器并进行阅读(前端内容合并);联调前端和后端,使项目基本成型。
0.4
通过科大讯飞提供的API,实现听书功能。
项目前端书城、书架页使用vue-cli 3.0、HTML5、CSS、Javascript进行开发,项目的重难点在于:UI设计、与后端的交互(发送请求、处理返回数据)。在开发过程中,实现整个项目的流程是:书城首页框架页面+路由配置书城首页标题+搜索框 布局、交互热门搜索布局、交互实现书城首页推荐页面实现;书架标题组件布局、交互书架搜索框布局、交互书架数据获取实现书架图书列表实现书架图书编辑功能开发;听书页布局、交互听书API接入。
在开发过程中,页面的内容布局和交互实现的方法和路径代码较为简单,困难的是思路上的突破,UI设计上的和谐。此处以搜索栏组件为例,展示省略CSS的代码:
| <template> <div> <divclass="search-bar" :class="{'hide-title': !titleVisible, 'hide-shadow': !shadowVisible}"> <transitionname="title-move"> <divclass="search-bar-title-wrapper"v-show="titleVisible"> <divclass="title-text-wrapper"> <spanclass="title-text title">{{$t('home.title')}}</span> </div> <divclass="title-icon-shake-wrapper" @click="showFlapCard"> <spanclass="icon-shake icon"></span> </div> </div> </transition> <divclass="title-icon-back-wrapper" :class="{'hide-title': !titleVisible}" @click="back"> <spanclass="icon-back icon"></span> </div> <divclass="search-bar-input-wrapper" :class="{'hide-title': !titleVisible}"> <divclass="search-bar-blank" :class="{'hide-title': !titleVisible}"></div> <divclass="search-bar-input"> <spanclass="icon-search icon"></span> <inputclass="input" type="text" :placeholder="$t('home.hint')" v-model="searchText" @click="showHotSearch" @keyup.13.exact="search"> </div> </div> </div> <hot-search-listv-show="hotSearchVisible" ref="hotSearch"></hot-search-list> </div></template><script> import { storeHomeMixin } from '../../utils/mixin' import HotSearchList from './HotSearchList' export default { components: { HotSearchList }, mixins: [storeHomeMixin], data() { return { searchText: '', titleVisible: true, shadowVisible: false, hotSearchVisible: false } }, watch: { offsetY(offsetY) { if (offsetY > 0) { this.hideTitle() this.showShadow() } else { this.showTitle() this.hideShadow() } }, hotSearchOffsetY(offsetY) { if (offsetY > 0) { this.showShadow() } else { this.hideShadow() } } }, methods: { search() { this.$router.push({ path: '/store/list', query: { keyword: this.searchText } }) }, showFlapCard() { this.setFlapCardVisible(true) }, back() { if (this.offsetY > 0) { this.showShadow() } else { this.hideShadow() } if (this.hotSearchVisible) { this.hideHotSearch() } else { this.$router.push('/store/shelf') } }, showHotSearch() { this.hideTitle() this.hideShadow() this.hotSearchVisible = true this.$nextTick(() => { this.$refs.hotSearch.reset() }) }, hideHotSearch() { this.hotSearchVisible = false if (this.offsetY > 0) { this.hideTitle() this.showShadow() } else { this.showTitle() this.hideShadow() } }, hideTitle() { this.titleVisible = false }, showTitle() { this.titleVisible = true }, hideShadow() { this.shadowVisible = false }, showShadow() { this.shadowVisible = true } } }</script>
| | --- |
在前后端分离开发的实现中,向后端发送请求和响应后端的请求并解析后端返回数据是重中之中。在实现向后端发送请求时,使用HTTP get/post 请求;解析返回数据时,使用vue.js 或javascript函数。
发送请求代码示例:
// 向后端发送请求的js函数
exportfunction flatList() {
return axios({
method: 'get',
url: `${process.env.VUE\_APP\_BOOK\_URL}/book/flat-list`
})
}
exportfunction shelf() {
return axios({
method: 'get',
url: `${process.env.VUE\_APP\_BASE\_URL}/book/shelf`
})
}
exportfunction list() {
return axios({
method: 'get',
url: `${process.env.VUE\_APP\_BASE\_URL}/book/list`
})
}
exportfunction download(book, onSucess, onError, onProgress) {
if (onProgress == null) {
onProgress = onError
onError = null
}
return axios.create({
baseURL: process.env.VUE\_APP\_EPUB\_URL,
method: 'get',
responseType: 'blob',
timeout: 180 \* 1000,
onDownloadProgress: progressEvent =\> {
if (onProgress) onProgress(progressEvent)
}
}).get(${book.categoryText}/${book.fileName}.epub
)
.then(res =\> {
const blob = new Blob([res.data])
setLocalForage(book.fileName, blob,
() =\> onSucess(book),
err =\> onError(err))
}).catch(err =\> {
if (onError) onError(err)
})
}
处理返回数据的代码示例:
// 通过API找到当前电子书的详情数据
findBookFromList(fileName) {
flatList().then(response =\> {
if (response.status === 200) {
const bookList = response.data.data.filter(item =\> item.fileName === fileName)
if (bookList && bookList.length \> 0) {
this.bookItem = bookList[0]
this.init()
}
}
})
},
// 初始化参数信息
init() {
const fileName = this.$route.query.fileName
if (!this.bookItem) {
this.bookItem = findBook(fileName)
}
//为减少篇幅,省略部分代码
// 解析电子书
parseBook(blob) {
// 解析电子书
this.book = new Epub(blob)
// 获取电子书的metadata
this.book.loaded.metadata.then(metadata =\> {
this.metadata = metadata
})
// 获取电子书的目录信息
this.book.loaded.navigation.then(nav =\> {
this.navigation = nav
})
// 渲染电子书
this.display()
},
书城首页
分类页面
按学科分类展示书籍
书籍详情页
听书页
搜索页
搜索结果页
书架页
编辑书架页面
随机推荐页面
精选页面