1、epubjs核心工作原理
1.1 epubjs的核心工作原理解析
epub电子书,会通过epubjs去实例化一个Book对象,Book对象会对电子书进行解析。Book对象可以通过renderTo方法去生成一个Rendition对象,Rendition主要负责电子书的渲染,通过Rendition我们可以得到Theme对象,Theme负责电子书的样式和主题,比如设置字号设置主题这些功能都需要通过Theme对象实现;Book对象还可以生成Location对象,负责电子书的定位,用来实现拖动进度条时快速定位的功能;Book对象还可以生成Navigation对象,用来展示电子书目录并提供目录所在的路径。
2、电子书解析渲染、翻页
怎么实现电子书的渲染,就将epub电子书解析成Book对象,再通过Book对象生成Rendition对象的过程。
代码实现:在src文件下创建一个Ebook.vue,这个Ebook.vue即阅读器页面
然后我们到router文件夹下配置路由,使得我们能够通过url能够访问到这个组件。path为/的是根路径,我们这里是先将根路径redirect即重定向到ebook下
一般怎么配置路由呢:
首先把该组件导入进来即import Ebook from '@/Ebook'
然后在routes下面再建一个对象,标明path,即访问什么路径可以访问到这个页面,标明component即组件名。
接下来我们实现电子书的解析和渲染
电子书需要通过阅读器引擎epubjs才能生成Book对象嘛,所以要要导入阅读器引擎epubjs;
那电子书从哪里来,我们就定义一个变量DOWLOAD_URL指向电子书的下载路径,通过这个路径可以找到电子书;
然后methods中建一个showEpub方法用来进行电子书的解析和渲染,在vue页面绘制完成的时候电子书就应该解析和渲染完成了嘛,所以我们应该在mounted的钩子函数中去调用这个showEpub方法。
showEpub方法中就是:电子书的解析和渲染主要就是生成Book对象,Book对象再生成Rendition对象嘛,Rendition对象再通过display方法去渲染电子书。
如下,生成Book对象,打印出的book,可以看到电子书已经得到解析
如下,可以看到电子书就被渲染出来啦
接下来我们实现翻页功能
第一步:首先加个浮层,布置左中右三个部分。
我们id为read的dom是填充整个屏幕的,所以我们可以在这个id为read的div上层使用绝对定位做一个浮层,我们实现点击这个浮层左侧就返回上一页,点击浮层右侧就返回下一页。
我们在id为read的div下添加一个同级的mask的div即浮层,mask下有左中右三个div盒子,这里注意一下小细节:mask要用绝对定位在read上面,并且mask用绝对定位了,它的父元素就要用相对定位哈。
left、right 都给flex:0 0 px2rem(100) 表示左右盒子不放大 不缩小 框100rem;center盒子就根据屏幕大小出去左右两边盒子后剩余空间自动伸缩
最终如下,屏幕宽度改变,左右盒子的宽度不变,中间center盒子自动根据屏幕宽度自适应填满
然后第二步,我们给左右分别绑定点击事件,点击左即翻上一页,点击右即翻下一页,怎么做?
首先在左右盒子标签中@click绑定事件,然后在methods中定义这两个方法(电子书上一页下一页是通过Rendition的prev()方法和next()方法实现的)。你现在一点击左盒子,即可因为点击触发点击事件,从而上一页。
3、标题栏和菜单栏的实现
我们想要的效果:点击屏幕中央标题栏和菜单栏出现,再次点击标题栏和菜单栏会隐藏,标题栏和菜单栏出现和离开时都会带有一个过渡动画。
它们布局是都会覆盖阅读器的内容,所以应该采用绝对定位,让它们浮在阅读器上方;标题栏下方和菜单栏上方都有阴影用box-shadow: 0 px2rem(8) px2rem(8) rgba(0, 0, 0, 0.15);(水平方向阴影长度,垂直方向阴影长度,模糊程度,颜色/透明度)。
title-wrapper就是标题栏,read-wrapper是阅读器和浮层,
先处理图标,统一样式
这些图标样式各不相同,我们给它一样的样式,就都给它例名为icon的样式,这个icon样式写在global.css即公共样式中,这样其他的vue组件也可以用到
标题栏布局
是我菜,我是想左边给宽度,右边也固定个宽度,然后右边里面就flex布局,就两端对齐space-between就好。是错的,有问题,左右都给定宽度,这是自适应布局呀,用户屏幕宽度随时会变的,我们是想要它自适应的,所以应该title-wrapper下用display:flex布局后,它的儿子left用flex:0 0 rem2px(60),right就flex:1即右边自适应填满,即如下。注意区别弹性布局是同一级下display: flex; justify-content: center; align-items: center;,而这个是父级上display:flex;子级中flex:1;flex:0 0 20px;我容易搞混记一下
右边的图标怎么在最右边然后横向排列呢,刚开始是下图第一张,
右边里面再嵌套一个flex布局,加上这个flex布局后就水平方向排列了,因为默认主轴是水平方向嘛
再给主轴排列方式是flex-end即居右排列
再调整一下左边图标居中,居中很常用,所以我们就在公共样式中定义了一个居中的样式,哪里要用就直接加上@include center;即可
最终
菜单栏布局
也是浮在阅读器上面,所以也是采用绝对定位,不同的是阴影是在上面。这里面是四个图标自适应,所以给父display:flex;自己都flex:1即可
接下来我们实现标题栏菜单栏的事件
即点击的时候就出现,再次点击就隐藏。怎么实现呢,我们给mask浮层中的center添加点击事件,通过vue的v-show来控制标题栏和菜单栏的显示和隐藏。最后我们再通过vue的transition动画来实现显示和隐藏时的过渡动画。接下来代码实现
v-show一般是据一个变量来判断是否显示的,所以我们在data中定义一个变量ifTitleAndMenuShow,默认为false,我们想要v-show根据它为false时就隐藏标题栏菜单栏,为true时就显示标题栏菜单栏。(data(){return {} })
然后点击center的时候,我们应该去切换这个变量,使得这个变量若是false点击后就变为true,反之为false。所以我们应该在center标签中绑定一个点击事件,事件中切换这个变量即可
然后添加过渡动画
这里补充过渡动画的原理:
·使用v-show动态显示或隐藏元素时,会触发过渡动画
·我们可以使用transition标签(需要指定name,设为x)把包含v-show的div包裹起来
·vue会为transition包裹的div动态添加class,有6种样式,我们只需要为这些class指定样式即可实现过渡动画,6种样式是:
显示之前:x-enter 、显示之后:x-enter-to 、显示过程中:x-enter-active
隐藏之前:x-leave 、隐藏之后:x-leave-to 、隐藏过程中:x-leave-active
还有注意,transition的class会添加到标题栏的同级位置即和标题栏同级,而我们下面这个transition的class是写在了标题栏的class下面,所以我们加个&就可以表示和标题栏的class同级
我们以标题栏进入的过渡动画为例:
标题栏隐藏的过渡动画也是同理
菜单栏的显示隐藏的过渡动画也是同理,就不再展示出来了。
这里我们发现过渡动画当中有很多重复的代码,我们将它抽出来写到global.css当中
我们整理一下:由于我们整理到了global当中,所以以后哪里再叫slide-down就可以直接在global找到对应的样式代码,就不用再再写
4、字号设置功能实现
效果:点击菜单栏的A按钮,调整字号大小的窗口弹出也包含过渡动画,而且菜单栏和这个窗口有一个合并效果,阴影移到了最上方,点击屏幕中间,菜单栏和窗口会一同隐藏。字号选择条背景是一条横线,每一个可选字号是一条竖线,当前选择字号是一个圆圈,中间还有一个黑色的实心圆。
将标题栏和菜单栏抽离出去,作为Ebook的子组件
菜单栏中还有很多要做的东西,所以我们把菜单栏和标题栏单独做个组件,在components中分别建MenuBar.vue和TitleBar.vue,把标题栏菜单栏的html、css分别从Ebook中剪切到标题栏菜单栏中。怎么在Ebook中添加这两个组件?
首先在Ebook中引入标题栏菜单栏组件
然后在components下注册这两个组件
最后在布局文件当中就可以使用这两个组件啦
此时会出现报错如下:
这是因为你把标题栏菜单栏抽出去了,标题栏菜单栏中还用这个ifTitleAndMenuShow,但是标题栏菜单栏中没有定义这个,这个是在Ebook组件中定义的,所以要把这个从Ebook父组件中传过去给子组件们,怎么传:就父组件中在该子组件们标签中通过动态:ifTitleAndMenuShow="ifTitleAndMenuShow"传,子组件们用props接,如下
接下来我们实现字号窗口的布局
字号窗口也是浮在阅读器上方,所以也是用绝对定位,它的上方也有一个阴影,也有过渡动画。
接下来我们实现字号窗口的显示隐藏
注意这个的展示隐藏就不是和标题栏菜单栏的显示隐藏一样了哈,它的显示是点击菜单栏的A字母时才显示,点击屏幕时再和菜单栏一起隐藏。
所以我们要单独给它在data中定义一个变量叫ifSettingShow:false来判定它显示还是隐藏。
首先我们先搞它的显示,点击A时就触发事件去改ifSettingShow为false,所以我们给A绑定点击事件,然后去methosd中定义这个事件即showSetting事件,事件中即ifSettingShow=true
然后再看隐藏,我们想要的是我们点击屏幕中间时,字号窗口和标题栏菜单栏一起隐藏。那怎么实现呢?
我的想法:再给center绑定一个点击事件,如果点击center的时候字号窗口是显示状态,那么就在该事件中修改字号窗口状态为false。但是这个有点不好好像,center原先就有点击事件了,就直接在原先点击事件中加即可,按我的想法的话在父组件中看子组件的状态,就还要把状态传过来,麻烦,转换一下思路,菜单栏要是隐藏,字号窗口也跟着隐藏就好了嘛,这样的话就看父组件中菜单栏标题栏的状态变量就好了,这样看自己的就不用传值啦,然后也不用在父组件中去修改子组件的值呀,应该在子组件中写一个方法去修改自己的值,然后父组件去调用子组件这个方法,就可以实现父组件修改子组件中的值啦。
所以,子组件中定义隐藏事件hideSetting,让它把ifSettingShow改为false。然后在父组件Ebook中的center点击事件中看菜单栏标题栏的状态变量是否为false,即说明要隐藏了,那么就调用子组件MenuBar组件中的隐藏事件hideSetting,从而修改字号窗口的状态变量为false,这样字号窗口也就跟着一同隐藏起来了。
那父组件中调用子组件中的函数怎么调呢?父组件的子组件menu-bar组件中加入ref="menuBar",然后父组件下面要调用子组件该的地方就直接通过this.$refs.menuBar.子组件方法()即可
我们现在是下面这样,我们想要当字号窗口出现的时候,菜单栏的阴影box-shadow隐藏
怎么实现呢?我们通过vue的class绑定来实现,如下我们给它绑定一个class,这个class名字叫做hide-box-shadow,这个属性什么时候生效呢,到我们字号窗口出现的时候即ifSettingShow为true的时候。 :class="{ 'hide-box-shadow': ifSettingShow }"即表示当ifSettingShow为true时就显示hide-box-shadow这个样式。然后在样式中设置hide-box-shadow的样式,如下:
但是发现点击屏幕中间,字号窗口和菜单栏一起隐藏的过程中菜单栏的阴影还是会出现。这个很简单就ifSettingShow为false即菜单栏应该要隐藏的时候也走这个hide-box-shadow样式即可
接下来我们实现如何实现字号选择条的布局
左右是不同大小的字母A,最小字母设置为12,最大为24,每一级字号递增2。这里应该用flex布局,即左右两侧进行宽度固定,中间字号选择条进行自适应伸缩。中间字号选择条根据传入字号数量不同会自动进行伸缩,每一个选择条都是左右横线加上中间竖线,最左侧左横线和最右侧右横线要隐藏,这里是借助css伪类来实现选择器然后进行隐藏。当前选中字号需要用一个圆来标识,圆形里面有一个黑色实心圆,这里我们可以将css的圆角属性为50%,加上阴影边框即可,最后再加上点击事件即可。下面代码实现:
首先我们在Ebook中定义一个字号数组即fontSizeList,这个字号数组会传入到子组件菜单栏中
如下图是两个A,怎么设置它们的字体大小为字号数组中最小那个的大小和最大那个的大小呢?
我们可以通过绑定style来实现,怎么实现呢?如下
然后字号选择条,字号选择条就是左边一条横线中间一个圆右边又一条横线
字号选择条也应该用flex布局,让左右横线自适应伸缩即给flex:1,而那个圆应该给flex:0 0 0; 即没有宽度。怎么表示出横线呢?给它height为0,上边框为1px的边框即可
上面这个只是一个选择条,我们是需要有7个选择条来切换字号,所以我们在select-wrapper上加v-for字号数组,让它自动复制多个dom,如下
我们采用伪元素:first-child{} ,即挑字号数组循环出来的7个选择条的第一个选择条进行我们需要做的处理。而这里注意我们需要用一个独立的div把这7个选择条包裹起来,因为:first-child{} 表示该父亲下的第一个儿子,且父亲一定要用display:flex; flex:1; 然后在儿子即选择条中用&:first-child{}
我们找到7个选择条中的第一个选择条后,我们继续找这第一个选择条的第一个line。
右边同样处理即可
接下来我们实现选中字体的小球
小球显示在哪里?
小球是当前字号值为这7个字号中某一个时,小球才会显示并且在该字号中间。我们到时候肯定需要一个属性来保存当前字号值的,现在还没有我们可以先给一个默认值,即在Ebook组件中定义一个默认字号defaultFontSize:16;并且把它传过去给子组件菜单栏的。
那怎么知道小球应该在这7个字号的哪一个的中间显示呢?通过在循环中,判断循环中的7个哪一个等于defaultFontSize就在哪一个中间显示,即给point标签中添加如下
小球布局:
点击修改字体大小
即我们点击不同字号的选择条它能捕捉我们的点击事件且去修改defalutFontSize去修改。默认字号修改了小球也就跟着修改了嘛,那切换字号就实现了嘛。
因为修改字体大小需要用到book对象中的theme对象来完成这个功能,所以修改默认字号这个方法只能在Ebook组件中定义,可是我们点击这个选择条是在menubar组件中,所以我们是在子组件menubar中获取点击的是哪个字号,因为点击的那一刻应该触发修改默认字号方法,获取点击的是哪个字号的同时还要在子组件中去调用父组件的这个方法。即点击后在子组件中去调用父组件的修改默认字号方法并且把字号传过去给父组件Ebook。
所以给子组件选择条标签中绑定一个点击事件,该事件要把当前被点击出的字号数因为是循环所以是item.fontSize传进去,然后在子组件的methods中定义这个点击事件,该事件中去调用父组件的修改默认字号方法并且往里面传入获取过来的值,通过this.$emit('父组件的方法',要传的参数)。父组件那边定义好这个修改默认字号的方法,然后在该子组件标签中@方法名=“方法名”即监听子组件什么时候调的
修改字体就用theme对象的fontSize(传入参数)方法即可
现在你去点击不同字号时发现小球跟着动了,但是电子书字体大小没跟着变,这是因为fontSize是数值,这里你需要加一下px,如下
5、主题设置功能实现
我们需要通过theme对象来实现主题切换,这里theme对象为我们提供了两个非常关键的方法:第一个是register方法即将主题注册到theme对象中,它包含两个参数,第一个是主题名称,第二个是主题包含的样式;第二个方法是select方法,就一个参数name,可以通过传入的参数name即主题名称来切换主题。
我们先创建主题数组
因为主题中包含名称name包含样式,有四个主题,所以应该用数组。
然后去循环这个数组,因为这四个主题首先你都要在theme对象中注册才行。
即定义一个方法去循环这个主题数组,循环每一个都去注册,然后在showEpub中调用这个方法,即mounted的时候这四个主题都注册了
然后我们通过theme对象的select方法即可选择显示哪个主题。
我们可以先试一试,如下可知主题切换功能是可以的
我们把主题切换即select()方法封装成一个方法
同理我们在data中定义一个属性叫defaultTheme:0,0就是主题数组的下标,这样的好处是我们可以将用户选择某一个主题之后我们可以把它保存在defaultTheme中。我们肯定会切换主题的,也就是select中的name不是固定的哪个主题,因为主题是一个数组,所以只要传过来用户点击的哪个主题,这个主题在主题数组中是什么下标即可,即传过来一个下标index即可
同理用户是在菜单栏中点击切换的主题的,所以和字号那个一样,就是在子组件菜单栏中,在用户点击主题时调用父组件中这个setTheme方法并传用户点击的主题下标,从而实现切换。
所以Ebook中的菜单栏标签监听setTheme方法并且因为菜单栏需要用到主题数组所以主题数组也要传过去以及默认主题,并且在菜单栏中用props接收
菜单栏上面那个栏显示字号窗口还是主题窗口
主题窗口和字号窗口都是在那个高为60的白框即setting-wrap中展示的,但是字号窗口展示的时候主题窗口不应该展示,即是互斥关系,一个展示的时候另一个不能展示。
我们定义一个变量showTag默认为0,定如果showTag为0则显示字号窗口,如果为1则显示主题窗口,即
那咋切换0,1呢?你点击字号图标我就在该图标上添加点击事件,点击的同时把0传进去;点击主题图标就在点击事件中把1传过去,点击事件中。
这个点击事件之前就是前面字号窗口做的点击字号图标它就改变状态变量为true,那高60的框就显示出来了。之前是因为框中只有字号窗口,所以这个高60的框一出来字号窗口也就跟着出来。现在是想要我点击哪个图标这个框中就显示哪个窗口,所以在点击图标时就把对应数传过去,这个点击事件中就会根据传过来的是0还是1从而去修改data中的showTag默认值,从而展示哪个窗口
下面来做主题选择的布局
preview中的背景颜色就是主题数组中当前item的背景颜色,text中的文字就是主题数组中当前item的主题名称,怎么表示出来,如下
这里我们想要白色主题的时候给它个边框,涉及到状态,所以我们在preview当中做class绑定,使得是白色主题的时候就有这个边框这个class,如下
我们想要没选择的主题主题名称是浅色,选择的主题主题名称是深黑色,同理绑定一个class
最后添加点击事件即可
6、进度条功能实现
效果:通过拖动进度条,我们就可以快速定位到指定位置。这里我们需要通过epubjs的location对象来解决。初次打开设置时,若进度条没有加载完成会显示在加载中...同时进度条进行锁定并不能进行操作。
在showEpub中去生成locations对象
在showEpub中打印this.book.locations可以发现locations默认是不会生成的,因为locations对象生成比较消耗性能,所以默认时不会生成locations对象,我们需要通过epubjs的钩子函数实现,即this.book.ready这样一个钩子函数,它表示当我电子书解析完成之后就会进行回调的一个方法,会返回一个promise对象,我们用then接,用异步的方法来实现电子书的定位即电子书解析完成之后再去生成locations对象,这个epubcfi就是帮我们做定位的,即电子书中某一页某一个字,通过epubcfi它都能够定位的到
在mounted钩子函数中也就是页面绘制完成前,电子书应该渲染出用户当前阅读的页的内容嘛,所以到methods中定义一个方法,该方法就是用来获取当前用户在读哪一页然后渲染出这一页。
该方法中接收一个进度条数值的参数(进度条数值用户是可以改变的,而且是根据这个进度条数值来进行确定去渲染那一页的,所以它应该作为一个参数传入),方法中要把进度条数值0-100转化为百分比;然后去获取当前百分比对应在哪一页,通过this.locations.cfiFromPercentage(百分比)可以获取到这个百分比是处于哪一页;最后知道是哪一页了,就用book的rendition的display方法去渲染这一页内容。
我们看看这个方法有没有生效,如下在showEpub中调用这个方法,可以看到成功了给我们展示的是最后一页
这两个方法生效以后,我们就可以去写布局的代码
因为电子书没有解析完成的时候我们进度条是不能点击的嘛,所以我们定义一个变量bookAvailable去保存图书是否处于可用状态默认为false,等电子书解析完成即locations对象生成后,图书就处于可用状态了即改为true,即这时候可以去拖动进度条了。
因为是菜单栏中需要用到这个bookaVailable变量和onProgressChange方法,会据这个变量判断进度条可不可以用,据这个方法去改变进度条数值时改变阅读器当前渲染的页数嘛,所以要把这几个东西传给子组件菜单栏,同理菜单栏中需要在props中接收这个变量
同理这个盛放进度条、主题、字号条的都是发在那个高60的框中的,所以展示这个框的函数showSetting函数中传入2(0则显示字号条,1则显示主题,2则显示进度条),所以也给进度条图标绑定一个点击事件,点击事件就是展示那个高60的框,同时传入2。
然后去做这个进度条,这里我们可以用<input>标签的 type="range" 属性
当input的type设置为"range" 时,浏览器会显示一个滑动条,这可以模拟进度条的外观,<input type="range" min="0" max="100" value="0"> ,如下:max min即最小0最大100,step表示每移动一次增加的幅度是多少我们这里给它增加1,@change表示它松手后调用的一个事件,@input表示的是我们在拖动的时候进度条下面百分比数值的变化,它的值我们建一个变量叫progress默认0,disabled状态即什么时候不可用即当bookAvailabe为false时不可用
进度条样式如下
这个进度条是前面的页数的进度条部分就深一点的背景颜色,后面的就浅的颜色,这个是用background设置两个背景颜色,然后通过background-size的两个数是表示前面的颜色占据多少,后面颜色占据多少的。而这两个颜色长短的变化是根据用户拖动的进度条即progress来决定的,所以我们会定一个方法来去改变这两个背景颜色的比例的
这里我们有两个方法,拖动进度条时,松开进度条时
onProgressInput方法是拖动进度条时触发的函数,拖动进度条时百分比即progress会变所以要改变progress并且保存下来,然后拖动进度条进度条的那两个背景颜色也应该改变,怎么改变,去获取这个进度条的DOM,然后去改变这个dom的样式,即改变这个背景颜色的百分比即可;另一个方法是松开进度条后触发的事件,松开进度条后你在这个子组件中就获取到了此时的progress就传回给父组件,调用父组件那个方法去重新渲染当前新的页数的内容。
7、目录功能实现
效果:点击目录按钮时,会弹出目录窗口,背景为透明的灰色,这里的 目录我们可以通过epubjs的navigations对象来实现,作为一个列表进行展示,点击一个目录的时候会跳转到该目录所在位置,跳转过程我们依然可以通过renditions对象的display方法来实现渲染,点击背景的时候目录会进行隐藏,显示和隐藏都有一个过渡动画,初次加入的时候会显示加载中...,等电子书解析完毕后才能加载
要实现目录功能,首先要获取navigation对象
打印出navigation可以看到如下,那个href就是我们目录的链接,我们只需要把href放到我们rendision的display方法里面就可以显示出目录了
然后定义一个navigation变量,定义一个方法根据传进来的href去进行跳转,传出去给子组件,props接navigation是Object
布局(蒙版部分、目录部分)
蒙版部分,这个content-mask就是点击目录后那个蒙版即点击之后背后阴影的部分,html、css部分如下,然后我们定义一个变量即ifShowContent来判断是否显示这个蒙版,如果点击这个蒙版则隐藏目录,然后它的过渡动画是fade
目录部分
目录抽离成了菜单栏的一个子组件叫Content.vue。在菜单栏中引入,compontents中注册,加上子组件标签,如下
什么时候出现这个目录呢,点击目录按钮图标的时候。即如下,点击目录图标传入3,调整showSetting方法,如果是3则先把这个高60的框收起来,然后再把这个目录状态变量变为true即展示出目录栏
然后我们来看目录组件即content组件内容
做的过程中发现目录组件就是怎么也显示不出来,搞了好久,原先是用的<content></content> import Content from "@/components/Content" components:{ Content }就不行,改成ContentView下面这种才行