1、阅读器解析和渲染
1.1 动态路由的设置
观察成品可知,在地址栏会出现书名、分类的信息,而且可以在地址栏更改信息得到不同的书,这就利用了动态路由的技术。
1.1.1 现在先实现,在地址栏输入的值直接显示到页面的效果。
- 在路由index.js文件中的ebook路由下,添加children属性来存放动态路由,children是一个数组,每个元素是对象,里面包含了一个动态路由:
{
path: '/ebook',
component: () => import('../views/ebook/index.vue'),
children: [
{
//这里的冒号代表后面是参数,在EbookReader组件内部可以通过 $route.params.fileName来访问到
path: ':fileName',
component: () => import('../components/ebook/EbookReader')
}
]
}
1.1.2 构造书本地址
-
简单功能实现完毕之后,来写:在地址栏输入 地址+分类+|+书名可以访问到电子书,其实也就是在刚刚基础上将EbookReader组件里的打印改为字符串拼接;
-
字符串输入样例:
http://localhost:8080/#/ebook/History|2013_Book_FungalDiseaseInBritainAndTheUn
mounted () {
const fileName = this.$route.params.fileName.split('|').join('/')
const baseUrl = '192.168.1.112:8082/epub/'
console.log(`${baseUrl}${fileName}.epub`)
}
- 这样就从nginx上获取到了想要的资源,有了书本的地址就可以开始构造Book对象
- 将书本地址放到vuex中存储,这时由于后来会有很多地方都要用到这个值,同样使用getter和mapGetters来实现简便取值;
1.2 书本构造开始
1.2.1 将书本渲染到dom上
-
this.book = new Epub(url)
创建新book对象 -
获取rendition对象,主要用于电子书的渲染
-
this.rendition = this.book.renderTo('read', { window.innerWidth, height: window.innerHeight //这里课程里老师说要加,但是加上之后发现渲染出来的元素宽度为0,去掉就好了也不知道为什么 // method: 'default' })
-
-
this.rendition.display()
展示出电子书来
1.2.2 实现左滑右滑翻页效果
- 这里左滑右滑均不支持长按,且距离不能太短以与点击事件区别出来
this.rendition.on('事件名', '事件处理函数')
用来给book对象绑定事件
this.rendition.on('touchstart', event => {
this.touchStartX = event.changedTouches[0].clientX
this.touchStartTime = event.timeStamp
})
this.rendition.on('touchend', event => {
const offsetX = event.changedTouches[0].clientX - this.touchStartX
const time = event.timeStamp - this.touchStartTime
if (time < 500 && offsetX < -40) {
this.nextPage()
} else if (time < 500 && offsetX > 40) {
this.prePage()
} else {
this.toggleTitleAndMenu()
}
event.passive = false
event.stopPropagation() //阻止事件传播
})
- 上面这段代码是获取到滑动的起始和结束的位置与时间,通过简单的运算来判断滑动的方向以及时间
- 这里用的是
event.passive = false
,而不是老师的event.preventDefault()
是因为,滑动的时候会报错,设置passive为false也可以阻止默认行为
1.2.3 标题栏和菜单栏部分
- 布局和样式都是用之前的代码,稍微改动即可,改完之后大概样子以及过渡动画也完成了
- 这里需要滑动页面的时候标题栏菜单栏都要隐藏,要增加一个menuVisible的state来决定是否显示这两样东西,依然是用mapGatters来简化
1.2.4 用mixin技术来简化代码
1.2.4.1 代码复用
- 发现在每个组件里都用到了以下代码,代码重复度太大:
import { mapGetters, mapActions } from 'vuex'
export const ebookMixin = {
computed: {
...mapGetters([
'menuVisible',
'fileName'
])
}
}
- 所以将这段代码封装到utils/mixin.js文件中,再在要使用的组件里的script标签里用
import { ebookMixin } from '../../utils/mixin'
,原理和使用mapGetters一样差不多 - 再加上一个mixin的属性:
mixins: [ebookMixin]
1.2.4.2 vuex中methods的调用方法简化
- 之前利用了mapGetters和getter计数来让组件里引用state的数据简便了不少,其实vuex中同样可以使用类似的及时让外面的组件调用方法比较简单,本来是
this.$store.dispatch('方法名','参数')
,现在可以变为this.方法名('参数')
- 和getter差不多,在vuex的index.js文件中加上actions属性,同时在modules/actions文件里将方法都封装进去并且export出来,也就是将方法都放在一起方便管理
- 注意这里和上面的getters有点不一样,是直接把book里的actions全拿出来放到actions.js文件中,接着把book里原来的actions删掉
- 引用同样和mapGatters差不多,在组件里
import { mapActions } from 'vuex'
,但这是方法,所以...mapActions([''])
要写在methods里哦(同样这一段也可以写在mixin里方便管理)
1.2.5 字号设置
1.2.5.1 布局部分
-
字体的菜单栏部分会多出上面那一截,主要的实现思路是:
-
字体进度条左边是 最小的字体,右边是最大的字体
-
将菜单栏横着主要分成七个部分(这个数量是由总共有多少可以字号设置来决定的),每个部分分为左右两个部分 显示一个边框(视觉上就是一条直线)
-
几个部分由中间的小点隔开,其实每个小点的地方都是一个大点,但只有当选中某一个部分(字体)时,那个地方的大点才会显示出来表示选中
-
-
将fontSizeList这种静态数据都放在统一的文件下管理,这里放在utils文件夹下的book.js中
- 用
import { FONT_SIZE_LIST } from '../../utils/book'
引入进来 - 再在数据里定义好自己的数据 fontSizeList值为FONT_SIZE_LIST
- 用
export const FONT_SIZE_LIST = [
{ fontSize: 12 },
{ fontSize: 14 },
{ fontSize: 16 },
{ fontSize: 18 },
{ fontSize: 20 },
{ fontSize: 22 },
{ fontSize: 24 }
]
- 每个小圆点有个字体,全局有defaultFontSize为当前的字体,每次触发点击事件后通过setFontSize来修改默认字体
- 注意点:
- 当字体设置面板出来之后,设置面板的阴影要隐藏
- 设置面板隐藏,下一次再出来的时候字体设置面板应该是不出现的
1.2.6 字体部分
1.2.6.1 布局
- 字体和字号在同一个面板上,上面是字号条 占比2/3,下面是字号 1/3,其中字号设置点开有弹窗效果
- 占比就用flex布局,垂直分布,一个flex为2一个为1
1.2.6.2 字号设置弹窗
-
弹窗分为上下两个部分,上面是标题(后面支持中英切换),下面是字体列表
-
先对大致布局进行设置,再写css,就不再多说,但是都用弹性布局(非常好!!!)
-
下面的字体列表用v-for循环显示完之后,选中的那个要单独变颜色,用
:class="{'selected': isSelected(item)}"
来控制加不加上那个selected的类
1.2.6.3 字体设置(比字号设置复杂)
- 有一步的设置方法和字号设置差不多,
this.currentBook.rendition.themes.font(font)
,但这一步之后字体并没有发生变化 - epubjs的渲染是通过iframe来实现的,iframe中才是真实的阅读器的dom,要设置字体必须在这个dom里面设置,所以直接写是没用的(里面是一个独立的dom)
- 利用epubjs的钩子函数:
// content代表iframe里的dom已经加载完毕可以访问
// contents是管理资源文件
// addStylesheet是手动添加style样式的函数
this.rendition.hooks.content.register(contents => {
Promise.all(
[contents.addStylesheet('http://10.69.198.212:8082/fonts/daysOne.css'),
contents.addStylesheet('http://10.69.198.212:8082/fonts/cabin.css'),
contents.addStylesheet('http://10.69.198.212:8082/fonts/montserrat.css'),
contents.addStylesheet('http://10.69.198.212:8082/fonts/tangerine.css').then(() => {})
])
})
- 观察epubjs中contents的addStylesheet函数源码,发现它将接收的参数拼成一个url,利用link标签引入css,所以要接收url的css,因此就将字体文件放入nginx服务器上,传入的就是nginx服务器下的那个url
- 上面的register函数返回是一个promise对象,所以可以利用Promise.all这个函数来对那些promise对象进行处理,就可以在全部注册完之后干点什么了
环境变量
-
http://10.69.198.212:8082
这个网址很可能在生产环境和开发环境是不一样的,所以在这里不能直接写死网址,而是要添加环境变量 -
在项目根目录下添加文件 .env.development,在里面写上要替换的网址 只有以
VUE_APP_
开头的变量会被webpack.DefinePlugin
静态嵌入到客户端侧的包中
// .env.development
VUE_APP_RES_URL=http://10.69.198.212:8082
// 替换之后
contents.addStylesheet(`${process.env.VUE_APP_RES_URL}/fonts/cabin.css`),
- 环境变量是在服务器启动的时候加入,所以这里要重启服务器
1.2.7 字体和字号设置离线缓存
- 一般会有要求 在客户下一次进入页面的时候,设置仍然保持上一次退出的样子,所以这里用到了localStorage来实现
cookie localStorage sessionStorage三者异同
-
先安装
cnpm i --save web-storage-cache
-
在utils文件夹下创建localStorage.js文件用来封装localStorage的操作
// 基本操作
import Storage from 'web-storage-cache'
const localStorage = new Storage()
export function setLocalStorage (key, value) {
return localStorage.set(key, value)
}
export function getLocalStorage (key) {
return localStorage.get(key)
}
export function removeLocalStorage (key) {
return localStorage.delete(key)
}
export function clearLocalStorage () {
localStorage.clear()
}
- 下面的具体操作代码,每本书都在localStorage里有自己的单独设置
- localStorage里键是${fileName}-info,值是一个book对象,以后每次要加新的设置就直接在book对象中加就可以了
// 具体操作代码
export function setBookObject (fileName, key, value) {
let book = getLocalStorage(`${fileName}-info`)
if (!book) {
book = {}
}
book[key] = value
setLocalStorage(`${fileName}-info`, book)
}
export function getBookObject (fileName, key) {
const book = getLocalStorage(`${fileName}-info`)
if (book) {
return book[key]
} else {
return null
}
}
export function getFontFamily (fileName) {
return getBookObject(fileName, 'fontFamily')
}
export function saveFontFamily (fileName, font) {
setBookObject(fileName, 'fontFamily', font)
}
- 加完这些之后要设置,每次换完字体就要saveFontFamily,以及一开始渲染的时候也要设置,开始的时候利用rendition.display返回的是一个promise对象来进行操作
this.rendition.display().then(() => {
const font = getFontFamily(this.fileName)
if (font) {
this.currentBook.rendition.themes.font(font)
this.setDefaultFontFamily(font)
} else {
saveFontFamily(this.fileName, 'Default')
}
})
- 其余的设置也是差不多的操作
1.2.8 语言国际化
- 首先准备好了两个不同语言的文件, 里面准备好了各个地方需要的文字
- 利用 VueI18N插件
- 安装
cnpm i --save vue-i18n
- 在src目录下新建lang文件夹来管理语言
- lang文件夹下的index.js文件里使用插件, 同时注意要在main.js中引入这个插件文件并注册, 不然无法使用
- 同样利用localStorage来存储当前的语言, 以备下一次打开时需要
- 安装
1.2.9 主题设置
-
这里主题依旧是静态资源文件, 所以在utils下的book.js中仍然要加上theme相关信息
-
主题的名字也用到了国际化语言, 所以book.js中也要使用插件vuei18n, 这里就有问题了
- 发现i18n.t()函数使用的时候报错
i18n is undefied
, 明明i18n已经在main.js的根组件中注册好了, 为什么还是使用不了? - 这是由于book.js中代码执行的时候, i18n插件还没被注册完毕. 也没什么好的解决方法, 就在开头引入i18n插件吧
import i18n from '../lang'
- 可以看看这篇文章 如何让一个vue项目支持多语言(vue-i18n)
- 发现i18n.t()函数使用的时候报错
-
设置主题要先在eBook对象中注册主题 分别需要名字以及对应的样式, 这里由于在EBook Reader组件以及EbookThemeSetting组件中都用到了themeList, 所以也将theme List的那个混入过程加入到mixin中, 这样只要引用了mixin就都能取到themeList
-
其余就和字体 字号一样, initTheme里判断默认主题什么什么的, 还有localStorage的存储~
-
小bug, 发现每个主题只能切换一次就不能再换回来了, 所以这里把epubjs的版本换成0.3.71 就行了
1.2.10 全局主题设置
- 主要的思想是动态添加css文件, 我们知道, css文件的添加是通过link标签实现的, 所以这里也就是动态添加link标签来实现 ( 这个函数写在book.js文件中并且export出去, 要使用的地方就引用 )
export function addClass (url) {
const link = document.createElement('link')
link.setAttribute('rel', 'stylesheet')
link.setAttribute('href', url)
link.setAttribute('type', 'text/css')
document.querySelector('head').appendChild(link)
}
-
这里的link标签需要url,我们将主题文件放到nginx服务器下, (注意这里的地址还是要使用环境变量, ~包括之前的book的地址也要). 每次切换主题的时候就调用addClass这个函数
-
每次选择切换一次主题, 不仅电子书的主题要切换 全局主题也要切换, 写另外一个函数来根据不同的电子书主题同步切换到全局主题
// 这个函数在EbookSettingTheme和EbookReader中都要用到,所以写在mixin中供全局调用
initGlobalStyle () {
switch (this.defaultTheme) {
case 'Default': addClass(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
break
case 'Gold': addClass(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`)
break
case 'Eye': addClass(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
break
case 'Night': addClass(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`)
break
default: addClass(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
}
}
- 但是这也下去会有一个问题, 每切换一次主题就多添加一个link标签, 加重了渲染的负担, 所以同时还要想办法移除之前添加的link标签