# epub阅读器的开发
入口:最终效果
# 阅读器基础功能:
- 屏幕适配(大屏两栏显示)
- 字号选择
- 背景色选择
- 选择目录
- 拖动进度条
- 翻页
# 涉及知识点
阅读器
- 阅读器工作原理:通过阅读器引擎(epub.js)将各种格式的电子书进行解析,渲染到页面上,再添加一系列功能
epub.js
- 通过epub.js解析电子书,创建Book对象
- 通过renderTo方法实例化Rendition对象(负责电子书的渲染)
- 可以得到Theme对象(负责样式和主题)
themes.register(name,styles) //注册主题样式 themes.select(name) //切换主题
1
2 - Location对象(负责电子书的定位,拖动进度条跳转)
- Navigation对象(展示电子书目录,提供目录所在路径)
vue.js
- transition过渡
- 组件化
- class与style绑定
- 父子组件通信
- nextTick()方法
- DOM操作
css
- 伪类和伪元素
- 定位
- 过渡动画
- flex布局
scss
- @import
- @function
- @mixin :一个可以在整个样式表中重复使用的样式
# 起步
node -v # v14.17.3
npm -v # 6.14.13
vue -v # @vue/cli 4.5.13
创建项目
- 因为Vue-cli 3.x和vue-cli 2.x使用了相同的 vue 命令,所以 vue-cli 2.x 被覆盖了。如果你仍然需要使用旧版本的 vue init 功能,你可以全局安装一个桥接工具
npm install -g @vue/cli-init
1vue init webpack ireader cd ireader npm run dev
1
2
3下载依赖包
npm install node-sass sass-loader --save-dev
1下载epubjs
npm install epubjs --save
1引入font字体图标
viewport 配置
# index.html <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalabel=no">
1
2
3rem配置
# App.vue // DOMContentLoaded事件动态设置根元素字体大小 documnet.addEventListener('DOMContentLoaded', () => { const html = document.querySelector('html') html.style.fontSize = window.innerWidth / 10 + 'px' })
1
2
3
4
5
6reset.scss:消除不同浏览器默认样式的不一致
global.scss:规定整个站点的公共样式、方法和参数
@import 'reset'; // 1rem = fontSize px // 2px = (2 / fontSize) rem $fontSize:37.5px; @function px2rem($px) { @return ($px/$fontSize)+rem; } @mixin center { display : flex; justify-content: center; align-items : center; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 电子书解析和渲染
import ePub from 'epubjs'
const DOWNLOAD_URL = '../static/2018_Book_AgileProcessesInSoftwareEngine.epub'
showEpub () {
// 解析为Book对象
this.book = ePub(DOWNLOAD_URL)
// 通过renderTo实例化Rendition对象
this.rendition = this.book.renderTo('read', {
width: window.innerWidth,
height: innerHeight
})
this.rendition.display()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 翻页功能
- 添加浮层(包含左中右区域), 通过绑定this.rendition.prev及next方法实现左右区域翻页
# 标题栏和菜单栏的显示与隐藏
- 在点击中间区域时触发事件,控制显示与隐藏
- 添加过渡动画:用transition标签 (指定name) 包裹标题栏以及菜单栏,vue会为包裹的div动态添加6种class(enter,enter-to,enter-active,leave,leave-to,leave-active)
<transition name="slide-down"> <div>.......</div> </transition>
1
2
3.slide-down-enter, .slide-down-leave-to { transform: translate3d(0, -100%, 0); } .slide-down-enter-to, .slide-down-leave, .slide-up-enter-to, .slide-up-leave { transform: translate3d(0, 0, 0); } .slide-down-enter-active, .slide-down-leave-active, .slide-up-enter-active, .slide-up-leave-active { transition: all 0.3s linear; } .slide-up-enter, .slide-up-leave-to { transform: translate3d(0, 100%, 0); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Note:标题栏和菜单栏会添加很多功能,所以利用组件化的思想,对代码进行拆分
- 父子组件通信
- 将父组件中isTitleAndMenuShow数据传到子组件
<!--父--> <title-bar :isTitleAndMenuShow='isTitleAndMenuShow'></title-bar> <!--子--> props: { isTitleAndMenuShow: { type: Boolean, default: false } }
1
2
3
4
5
6
7
8
9
10- 父组件调用子组件中的方法
// 父 <menu-bar ref="menuBar"></menu-bar> this.$refs.menuBar.hideSetting() // 子 hideSetting () { this.isSettingShow = false }
1
2
3
4
5
6
7
8
# 字号设置
- 字号设置窗口出现的时候会被菜单栏的阴影覆盖
- 解决方法:当字号设置窗口出现时||标题栏和菜单栏消失时,将菜单栏的阴影取消
<div class="menu-wrapper"
:class="{'hide-box-shadow':isSettingShow || !isTitleAndMenuShow}">
1
2
2
.menu-wrapper {
&.hide-box-shadow {
box-shadow: none;
}
}
1
2
3
4
5
2
3
4
5
- 字号设置窗口和菜单栏一并隐藏时过渡动画不够流畅
- 原因:位移不同步(设置过渡动画时,字号设置窗口向上移动60px,菜单栏向上移动48px)
- 解决方法:将向上移动距离设置为108px
.slide-up-enter,
.slide-up-leave-to {
transform: translate3d(0, 100%, 0); // ==> translate3d(0, px2rem(108), 0)
}
1
2
3
4
2
3
4
- 字号选择条的布局
- 采用flex布局,左右两侧设置字号A图标,中间设置伸缩条(左侧横线+右侧竖线实现,字号范围12-24,递增2), 根据fontSizeList字号列表用v-for复制
# 主题背景色设置
提供四种主题背景色,设置栏提供了背景色的预览,选择一个会改变阅读器背景色,选中的label颜色会加深
背景主题切换使用epubjs的theme实现
this.themes.register(name,style) // 注册主题 this.themes.select(name) // 切换主题
1
2
- 定义主题数组,遍历该数组注册主题
- 定义默认主题defaultTheme,根据索引设置该变量,来切换主题
themeList: [ { name: 'default', style: { body: { 'color': '#000', 'background': '#fff' } } }, {name: 'eye',...} ]
1
2
3
4
5
6
7
8
9
10
11
12this.themeList.forEach((theme) => { this.themes.register(theme.name, theme.style) })
1
2
3this.themes.select(this.themeList[index].name) this.defaultTheme = index
1
2
# 阅读进度功能
拖动进度条快速到达指定位置,初次打开如果没有加载完电子书,则提示在加载中;
进度条左侧(已读部分)颜色加深,手柄拖动时下方显示相应的百分比
使用epubjs的location实现
- 通过epubjs钩子函数获取location对象
- 定义bookAvailable变量保存图书是否加载完成
this.book.ready.then(()=>{ return this.book.locations.generate() }).then((result)=>{ this.locations=this.book.locations this.bookAvailable=true })
1
2
3
4
5
6 - 如果图书加载完成(bookAvailable===true),则可以拖动进度条;否则显示‘加载中’
- 传递到子组件menuBar
<!-- 父组件 Ebook --> <menu-bar :bookAvailable='bookAvailable'></menu-bar>
1
2// 子组件接收 export default{ props:{ bookAvailable:Boolean } }
1
2
3
4
5
6 - 子组件menuBar进度条中使用disabled属性来控制是否可拖动
<!-- 子组件menuBar --> <input type='range' :disabled='!bookAvailable'>
1
2 - 数值百分比显示信息
<!-- 子组件menuBar --> <span>{{bookAvailable? xxx+'%': 'Loading...'}}</span>
1
2
- 传递到子组件menuBar
- 当拖动进度条时,分别绑定input方法(实时获取进度条当前长度)和change方法(进度条松开后,跳转到指定电子书页面)
<!-- 子组件menuBar --> <input type='range' @change='onProgressChange($event.target.value)' @input='onProgressInput($event.target.value)'>
1
2- 定义progress变量(初始为0),拖动进度条时给progress赋值,并根据progress值改变进度条样式(进度条手柄左侧为#999,右侧为#ccc)
// <!-- 子组件menuBar --> onProgressInput(value){ this.progress=value this.$refs.progress.style.backgroundSize=`${this.progress}% 100%` }
1
2
3
4
5<!-- 子组件menuBar --> <input type='range' ref='progress' class='progress'>
1
2<!-- 子组件menuBar --> .progress{ appearance: none; background: linear-gradient(#999, #999) no-repeat, #ccc; background-size: 0 100%; }
1
2
3
4
5
6- 松开进度条触发事件,并将当前进度条长度progress传递给父组件Ebook中的setProgress方法
// <!-- 子组件menuBar --> onProgressChange(value){ this.$emit('onProgressChange',value) }
1
2
3
4<!-- 父组件Ebook中需要向子组件传递函数 --> <menu-bar @setProgress='onProgressChange'></menu-bar>
1
2// <!-- 父组件Ebook --> onProgressChange(value){ const percentage=value/100 // 根据百分比值重新渲染电子书页面所在位置 const location=percentage>0?this.locations.cliFromPercentage(percentage):0 this.rendition.display(location) }
1
2
3
4
5
6
7 - 点击菜单栏的图标,显示相应的设置栏
<!-- 子组件menuBar --> <span class="icon-a icon" @click="showSetting(0)">A</span>
1
2
3// 子组件menuBar // 如果传入的index参数是3,则显示设置栏,否则显示目录栏 // showTag控制显示哪个图标对应的设置栏 showSetting (index) { if (index === 3) { // 如果是目录 this.ifShowContent = true } else { this.ifSettingShow = true } this.showTag = index }
1
2
3
4
5
6
7
8
9
10
11
12<!-- 子组件menuBar --> <div class="setting-wrapper" v-show="isSettingShow"> <div class="setting-font-size" v-if="showTag===0"></div> <div class="setting-theme" v-else-if="showTag===1"></div> <div class="setting-progress" v-else-if="showTag===2"></div> </div>
1
2
3
4
5
6
# 目录功能
点击目录图标弹出目录窗口,点击一个目录项跳转至电子书相应位置;
点击目录背景时,目录窗口会隐藏
初次进入时都会有一个加载过程
目录的显示隐藏都有过渡动画
使用epubjs的Navigation实现
- 通过epubjs钩子函数获取navigation对象
this.book.ready.then(()=>{ this.navigation=this.book.navigation })
1
2
3 - 定义ifShowContent变量,点击目录图标弹出目录窗口子子组件
<!-- 子组件menuBar --> <!-- 目录显示区域 --> <content-view v-show='ifShowContent'></content-view> <!-- 目录蒙版区域 --> <div class='content-mask' v-show='ifShowContent'></div>
1
2
3
4
5<!-- 子子组件ContentView --> <div class="content"> <div class="content-wrapper"> </div> </div>
1
2
3
4
5 - 当图书加载完毕后显示目录,否则显示加载中. 需要从父组件中传入navigation对象和bookAvailable属性
<!-- 子组件menuBar --> <!-- 目录显示区域 --> <content-view v-show='ifShowContent' :navigation='navigation' :bookAvailable='bookAvailable'></content-view>
1
2
3
4<!-- 子子组件ContentView --> <div class="content"> <div class="content-wrapper" v-if="bookAvailable"> <div class="content-item" v-for="(item,index) in navigation.toc" :key="index"> <span class="text">{{item.label}}</span> </div> </div> <div class="loading" v-else>Loading...</div> </div>
1
2
3
4
5
6
7
8
9
10
11 - 点击目录项触发跳转页面事件, 调用父组件的方法 (根据链接跳转到指定位置,并隐藏标题栏、菜单栏、设置栏、目录)
<!-- 子子组件ContentView --> <div class="content-item" @click="jumpTo(item.href)> </div> jumpTo (href) { this.$emit('jumpTo', href) }
1
2
3
4
5
6
7<!-- 子组件MenuBar --> <content-view @jumpTo='jumpTo'></content-view> jumpTo (href) { this.$emit('jumpTo', href) }
1
2
3
4
5
6<!-- 父组件Ebook --> <menu-bar @jumpTo='jumpTo'></menu-bar> jumpTo (href) { this.rendition.display(href) this.hideTitleAndMenu() }, hideTitleAndMenu () { // 隐藏标题栏和菜单栏 this.isTitleAndMenuShow = false // 隐藏菜单栏的设置栏 this.$refs.menuBar.hideSetting() // 隐藏目录 this.$refs.menuBar.hideContent() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// <!-- 子组件MenuBar --> hideSetting () { this.ifSettingShow = false }, hideContent () { this.ifShowContent = false }
1
2
3
4
5
6
7