epub实例化一个Book对象
Book对象通过renderTo方法生成一个rendition
对象(负责电子书的渲染)
Book对象的location对象:负责电子书的定位
Book对象的Navigation对象:负责电子书目录,并提供目录所在的路径
通过epub生成Book对象,通过renderTo方法生成rendition对象的过程
this.rendition = this.book.renderTo('read', {
width: window.innerWidth,
height: window.innerHeight
})
为了方便开发,在vscode中配置vue的代码段:preferences-user snippets
新建一个vue的片段
{
"Print to console": {
"prefix": "vue",
"body": [
""
" "
""
""
""
""
""
],
"description": "Log output to console"
}
}
在src下创建Ebook组件
在router下配置路由,配置成功后可以通过url访问页面
routes: [
{
path: '/',
redirect: '/ebook'
},
{
path: '/ebook',
component: Ebook
}
]
在Ebook.vue文件下编写代码
this.book = new Epub('/static/电子书名称');
console.log(this.book);
生成Book对象
生成rendition,通过book.renderTo
renderTo方法(两个参数
)
参一:DOM的id,生成的电子书会以DOM的形式挂载到这个id上
参二:是一个配置对象,定义渲染电子书的宽高
import Epub from 'epubjs'
const DOWNLOAD_URL = '/static/2018_Book_AgileProcessesInSoftwareEngine.epub'
global.ePub = Epub
export default {
methods: {
// 电子书的解析和渲染
showEpub() {
// 生成bok
this.book = new Epub(DOWNLOAD_URL)
// 生成rendition 通过book.renderTo
this.rendition = this.book.renderTo('read', {
width: window.innerWidth,
height: window.innerHeight
})
// 通过rendition.display渲染电子书
this.rendition.display()
}
},
mounted() {
this.showEpub()
}
}
可以取消eslint检查 - 函数后的空格
在eslint的配置文件中配置:'space-before-function-paren': 'off'
挂载在read这个id上的电子书,epub实际上是使用了iframe实现的,内部有一个完整的document对象
思路:
电子书是在id为read的div,填充了整个屏幕
可以在div的上层使用绝对定位做一个浮层,点击浮层的左侧,进入上一页,点击浮层的右侧,进入下一页
<div id="read">div>
<div class="mask">
<div class="left" @click="prevPage">div>
<div class="center" @click="toggleTitleAndMenu">div>
<div class="right" @click="nextPage">div>
div>
.read-wrapper {
.mask {
position: absolute;
top: 0;
left: 0;
z-index: 100;
display: flex;
width: 100%;
height: 100%;
.left {
flex: 0 0 px2rem(100);
}
.center {
flex: 1;
}
.right {
flex: 0 0 px2rem(100);
}
}
}
flex布局的知识点
翻页功能实际上时利用rendition.prev和rendition.next方法
prevPage() {
// Rendition.prev
this.rendition && this.rendition.prev().then(() => this.showProgress())
},
// 下一页
nextPage() {
// Rendition.prev
this.rendition && this.rendition.next().then(() => this.showProgress())
},
采用绝对定位
标题栏
<div class="title-wrapper" v-show="ifTitleAndMenuShow">
<div class="left">
<span class="icon-back icon">span>
div>
<div class="right">
<div class="icon-wrapper">
<span class="icon-cart icon">span>
div>
<div class="icon-wrapper">
<span class="icon-person icon">span>
div>
<div class="icon-wrapper">
<span class="icon-more icon">span>
div>
div>
div>
为图标设置默认样式,在global.scss中
.icon {
color: #333;
font-size: px2rem(22);
}
设置阴影样式:设置box-shadow属性:box-shadow: 0 px2rem(8) px2rem(8) rgba(0, 0, 0, .15);
设置左右区域的样式
.left {
flex: 0 0 px2rem(60);
@include center;
}
.right {
flex: 1;
display: flex;
justify-content: flex-end;
.icon-wrapper {
flex: 0 0 px2rem(40);
@include center;
.icon-cart {
font-size: px2rem(22);
}
}
}
菜单栏
布局
<div class="menu-wrapper">
<div class="icon-wrapper">
<span class="icon-menu">span>
div>
<div class="icon-wrapper">
<span class="icon-progress icon">span>
div>
<div class="icon-wrapper">
<span class="icon-bright icon">span>
div>
<div class="icon-wrapper">
<span class="icon-a icon">Aspan>
div>
div>
样式
.menu-wrapper {
position: absolute;
bottom: 0;
left: 0;
z-index: 102;
display: flex;
width: 100%;
height: px2rem(48);
background: white;
box-shadow: 0 px2rem(-8) px2rem(8) rgba(0, 0, 0, .15);
&.hide-box-shadow {
box-shadow: none;
}
.icon-wrapper {
flex: 1;
@include center;
.icon-progress {
font-size: px2rem(28);
}
.icon-bright {
font-size: px2rem(24);
}
}
}
点击中间,标题栏和菜单栏会显示,再次点击会隐藏
<div class="mask">
<div class="left" @click="prevPage">div>
<div class="center" @click="toggleTitleAndMenu">div>
<div class="right" @click="nextPage">div>
div>
给center绑定方法
data() {
return {
ifTitleAndMenuShow: false
}
},
methods: {
toggleTitleAndMenu() {
this.ifTitleAndMenuShow = !this.ifTitleAndMenuShow
}
}
Transition动画原理
标题栏的过渡动画
<transition name="slide-down">
<div class="title-wrapper" v-show="ifTitleAndMenuShow">div>
transition>
菜单栏的过渡动画
<transition name="slide-up">
<div class="menu-wrapper" v-show="ifTitleAndMenuShow">
transition>
样式
.slide-down-enter,
.slide-down-leave-to {
transform: translate3d(0, px2rem(-108), 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 .3s linear;
}
.slide-up-enter,
.slide-up-leave-to {
transform: translate3d(0, px2rem(108), 0);
}
由于两个过渡动画重复代码非常多,合并重复代码,放在global.scss中,这样以后的transition组件的name属性是slide-down
和slide-up
的就会直接去global.scss
中去找了
在Ebook文件中配置
data() {
return {
ifTitleAndMenuShow: false,
fontSizeList: [
{ fontSize: 12 },
{ fontSize: 14 },
{ fontSize: 16 },
{ fontSize: 18 },
{ fontSize: 20 },
{ fontSize: 22 },
{ fontSize: 24 },
]
}
}
在MenuBar中接收fontSize属性
props: {
ifTitleAndMenuShow: {
type: Boolean,
default: false
},
fontSizeList: Array
}
动态绑定style属性
<div class="preview" :style="{fontSize: fontSizeList[0].fontSize + 'px'}">Adiv>
字号选择条的布局与样式
<div class="setting-font-size" v-if="showTag === 0">
<div class="preview" :style="{fontSize: fontSizeList[0].fontSize + 'px'}">Adiv>
<div class="select">
<div class="select-wrapper" v-for="(item, index) in fontSizeList" :key="index" @click="setFontSize(item.fontSize)">
<div class="line">div>
<div class="point-wrapper">
<div class="point" v-show="defaultFontSize === item.fontSize">
<div class="small-point">div>
div>
div>
<div class="line">div>
div>
div>
<div class="preview" :style="{fontSize: fontSizeList[fontSizeList.length - 1].fontSize + 'px'}">Adiv>
div>
.setting-font-size {
display: flex;
height: 100%;
.preview {
flex: 0 0 px2rem(40);
@include center;
}
.select {
display: flex;
flex: 1;
.select-wrapper {
flex: 1;
display: flex;
align-items: center;
.line {
flex: 1;
height: 0;
border-top: px2rem(1) solid #ccc;
}
.point-wrapper {
position: relative;
flex: 0 0 0;
width: 0;
height: px2rem(7);
border-left: px2rem(1) solid #ccc;
}
}
}
}
}
<div class="select-wrapper" v-for="(item, index) in fontSizeList" :key="index">div>
为了完成左侧和右侧的线消失掉,需要在select-wrapper外面再套一层,用first-child类实现
&:first-child {
.line {
&:first-child {
border-top: none;
}
}
}
&:last-child {
.line {
&:last-child {
border-top: none;
}
}
}
设置栏的font-size
<div class="select-wrapper"
v-for="(item, index) in fontSizeList"
:key="indxe"
@click="setFontSize(item.fontSize)"
>div>
不是在MenuBar中操作,而是要传到父类中去操作,因为它需要父类中的book对象中的一些数据去处理要在这个方法中调用父组件的方法,将fontSize
传进去,让父组件来完成这个操作
this.themes = this.rendition.themes
setFontSize(fontSize) {
this.defaultFontSize = fontSize
if (this.themes) {
this.themes.fontSize(fontSize + 'px')
}
}
.point {
position: absolute;
top: px2rem(-8);
left: px2rem(-10);
width: px2rem(20);
height: px2rem(20);
border-radius: 50%;
background: white;
border: px2rem(1) solid #ccc;
box-shadow: 0 px2rem(4) px2rem(4) rgba(0, 0, 0, .15);
@include center;
.small-point {
width: px2rem(5);
height: px2rem(5);
background: black;
border-radius: 50%;
}
}
注意:当点击设置栏后,点击设置栏的任意一个位置,就会触发之前设置的蒙版的点击事件
原因:蒙版的z-index值大于设置栏的index值
解决:给设置栏设置一个z-index值
通过theme对象实现主题切换
设置主题数据
themeList: [
{
name: 'default',
style: {
body: {
'color': '#000',
'background': '#fff'
}
}
},
{
name: 'eye',
style: {
body: {
'color': '#000',
'background': '#fff'
}
}
}
]
定义注册主题的方法
registerTheme() {
this.themeList.forEach(theme => {
this.themes.register(theme.name, theme.style)
})
}
调用registerTheme
方法测试下
this.registerTheme()
this.themes.select('eye')
颜色改变,即为成功
定义设置主题的方法
setTheme(index) {
this.themes.select(this.themeList[index].name)
}
定义一个defaultTheme的属性
优点:将defaultTheme保存下载,当用户选择了一个主题后,可以保存在cookie、localStorage中,当用户下一次打开的时候,仍然能够默认使用上一次使用的主题
使用setTheme方法
this.registerTheme()
this.setTheme(this.defaultTheme)
这样在Ebook组件上的方法和属性就配置好了
将themeList属性、defaultTheme属性、setTheme方法传到MenuBar组件,在MenuBar组件中接收这些属性和方法,实现主题栏的布局
接收数据
props: {
ifTitleAndMenuShow: {
type: Boolean,
default: false
},
fontSizeList: Array,
defaultFontSize: Number,
themeList: Array,
defaltTheme: Number
}
在MenuBar中为showSetting方法添加参数,来实现点击不同的图标出现不同的菜单
<div class="setting-font-size" v-if="showTag === 0">···div>
<div class="setting-theme" v-else-if="showTag === 1">div>
<div class="icon-wrapper">
<span class="icon-bright icon" @click="showSetting(1)">span>
div>
<div class="icon-wrapper">
<span class="icon-a icon" @click="showSetting(0)">Aspan>
div>
showSetting(tag) {
this.ifSettingShow = true
this.showTag = tag
}
data() {
return {
ifSettingShow: false,
showTag: 0
}
}
结构
<div class="setting-theme" v-else-if="showTag === 1">
<div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
<div class="preview" :style="{background: item.style.body.background}" :class="{'no-border': item.style.body.background !== '#fff'}">div>
<div class="text" :class="{'selected': index === defaultTheme}">{{item.name}}div>
div>
div>
样式
.setting-theme {
height: 100%;
display: flex;
.setting-theme-item {
flex: 1;
display: flex;
flex-direction: column;
padding: px2rem(5);
box-sizing: border-box;
.preview {
flex: 1;
border: px2rem(1) solid #ccc;
box-sizing: border-box;
&.no-border {
border: none;
}
}
.text {
flex: 0 0 px2rem(20);
font-size: px2rem(14);
color: #ccc;
@include center;
&.selected {
color: #333;
}
}
}
}
设置点击事件
<div class="setting-theme" v-else-if="showTag === 1">
<div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
div>
div>
setTheme(index) {
this.$emit('setTheme', index)
}
通过epubjs的locations对象实现
打印locations对象
发现_locations是空的,epubcfi也是空的,说明locations对象默认是不会生成的,因为locations对象生成比较消耗性能,所以默认情况是不会生成locations对象的
通过epubjs的钩子函数ready来实现,当电子书解析完毕,进行回调的方法,返回一个promise对象,通过异步的方法实现电子书的定位,当电子书解析完毕,就可以生成它的locations对象,通过locations的generate方法生成定位符,參數是每页的字数
this.book.ready.then(() => {
return this.book.locations.generate()
}).then(result => console.log(result))
epubcfi:EPUB Canoical Fragment Identifiers 1.1 定位符规范,用来定位的
通过epubcfi和百分比做一个集合,就可以找到指定的页数
新建数据变量:bookAvailable: false,表示图书是否处于可用状态,默认是false,当locations对象生成后,将其设置为true。传递给MenuBar组件
在MenuBar中设置点击事件,点击progress按钮的时候的点击事件
<div class="icon-wrapper">
<span class="icon-progress icon" @click="showSetting(2)">span>
div>
progress结构
<div class="setting-progress" v-else-if="showTag === 2">
<div class="progress-wrapper">
<input class="progress" type="range"
max="100"
min="0"
step="1"
@change="onProgressChange($event.target.value)" @input="onProgressInput($event.target.value)"
:value="progress"
:disabled="!bookAvailable"
:style="{backgroundSize: calcBackgroundSize}"
ref="progress">
div>
<div class="text-wrapper">
<span>{{bookAvailable ? progress + '%' : '加载中...'}}span>
div>
div>
type=”range” 将控件设置为滑块
step=1 每移动一次增加的幅度
@change表示松手的时候触发的事件
@input表示拖动的时候下面的百分比变化
.setting-progress {
position: relative;
width: 100%;
height: 100%;
.progress-wrapper {
width: 100%;
height: 100%;
@include center;
padding: 0 px2rem(30);
box-sizing: border-box;
.progress {
width: 100%;
-webkit-appearance: none;
height: px2rem(2);
background: -webkit-linear-gradient(#999, #999) no-repeat, #ddd;
background-size: 0 100%;
&:focus {
outline: none;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
height: px2rem(20);
width: px2rem(20);
border-radius: 50%;
background: white;
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, .15);
border: px2rem(1) solid #ddd;
}
}
}
.text-wrapper {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
color: #333;
font-size: px2rem(12);
text-align: center;
}
}
onProgressInput(progress) {
this.progress = progress
this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
},
// 进度条松开后触发事件 根据进度条数值跳转到指定位置
onProgressChange(progress) {
this.$emit('onProgressChange', progress)
}
首先获取navigation对象
this.book.ready.then(() => {
this.navigation = this.book.navigation
console.log(this.navigation)
return this.book.locations.generate()
}).then(result => {
this.locations = this.book.locations
// 标记电子书为解析完毕
this.bookAvailable = true
})
toc:目录的内容
href:目录的链接
将href放入this.rendition.display
中就可显示了