3.8 字体设置弹窗功能实现
实现点击屏幕,字体设置面板消失:
toggleTitleAndMenu () {
if (this.menuVisible) {
// 隐藏字号设置面板
this.setSettingVisible(-1)
this.setFontFamilyVisible(false) // 实现点击屏幕,字体设置面板消失
}
// 显示标题和菜单栏
this.setMenuVisible(!this.menuVisible)
},
hideTitleAndMenu () {
this.setMenuVisible(false)
this.setSettingVisible(-1)
this.setFontFamilyVisible(false)// 实现点击屏幕,字体设置面板消失
},
实现字体被选中
给item绑定点击事件
<div class="ebook-popup-item" v-for="(item, index) in fontFamilyList" :key="index"
@click="setFontFamily(item.font)">
定义setFontFamily函数
setFontFamily (font) {
this.setDefaultFontFamily(font)
}
接下来实现,字体的切换的生效
与字号类似,在currentBook下面调用rendition
setFontFamily (font) {
this.setDefaultFontFamily(font)
this.currentBook.rendition.themes.font(font)
}
但这里没有生效,因为scss文件没有引入iframe
回到EbookReader.vue中,在initEpub函数中,使用rendition的钩子函数
this.rendition.hooks.content.register(contents => {
contents.addStylesheet(`...`)
})
根据contents.js源码,省略号这里必须传入一个url,那么我们利用nginx来实现。
我们把fonts文件放入resource文件夹,生成Http链接可以引用到钩子函数中
这是default字体未生效,通过改动setFontFamily方法来解决
setFontFamily (font) {
this.setDefaultFontFamily(font)
if (font === 'Default') {
// 设置自己喜欢的字体
this.currentBook.rendition.themes.font('Times New Roman')
} else {
this.currentBook.rendition.themes.font(font)
}
}
到这里,基本功能实现,我们做一些改动。
在Vue-CLI3.0中环境变量和模式设置,为以后上线做准备
在根目录下创建一个文件.env.development,把我们本地的url抽象成一个变量下入该文件。
注意一点,只有以 VUE_APP_
开头的变量会被 webpack.DefinePlugin
静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:
console.log(process.env.VUE_APP_SECRET)
所以我们的变量如下:
VUE_APP_RES_URL=http://192.168.0.102:8081
下面我们就可以使用了
this.rendition.hooks.content.register(contents => {
Promise.all(
[
contents.addStylesheet(`${process.env.VUE_APP_RES_URL}/fonts/daysOne.css`), contents.addStylesheet(`${process.env.VUE_APP_RES_URL}/fonts/cabin.css`),
contents.addStylesheet(`${process.env.VUE_APP_RES_URL}/fonts/montserrat.css`, contents.addStylesheet(`${process.env.VUE_APP_RES_URL}/fonts/tangerine.css`)
]
)
})
!环境变量和配置变量的重新载入都需要重新启动服务器才能生效。
3.9实现字号和字体的离线存储
我们这里利用Local Storage进行离线缓存,首先,安装库npm i --save web-storage-cache
在utils下面引入这个库
创建一个localStorage.js文件,将安装的库引入。这个库可以将我们传入的字符串或对象转换成json存储,读取时又能转换成字符串或对象使用。随后创建一个实例化对象,写入基本方法。
import Storage from 'web-storage-cache'
const localStorage = new Storage()
export function getLocalStorage (key) {
return localStorage.get(key)
}
export function setLocalStorage (key, value) {
return localStorage.set(key, value)
}
export function removeLocalStorage (key) {
return localStorage.delete(key)
}
export function clearLocalStorage () {
return localStorage.clear()
}
export function getHome () {
return getLocalStorage('home')
}
export function saveHome (home) {
return setLocalStorage('home', home, 1800)
}
export function getLocale () {
return getLocalStorage('locale')
}
export function saveLocale (locale) {
return setLocalStorage('locale', locale)
}
export function getLocation (fileName) {
return getBookObject(fileName, 'location')
}
export function saveLocation (fileName, location) {
setBookObject(fileName, 'location', location)
}
export function getBookmark (fileName) {
return getBookObject(fileName, 'bookmark')
}
export function saveBookmark (fileName, bookmark) {
setBookObject(fileName, 'bookmark', bookmark)
}
export function getReadTime (fileName) {
return getBookObject(fileName, 'time')
}
export function saveReadTime (fileName, theme) {
setBookObject(fileName, 'time', theme)
}
export function getProgress (fileName) {
return getBookObject(fileName, 'progress')
}
export function saveProgress (fileName, progress) {
setBookObject(fileName, 'progress', progress)
}
export function getNavigation (fileName) {
return getBookObject(fileName, 'navigation')
}
export function saveNavigation (fileName, navigation) {
setBookObject(fileName, 'navigation', navigation)
}
export function getMetadata (fileName) {
return getBookObject(fileName, 'metadata')
}
export function saveMetadata (fileName, metadata) {
setBookObject(fileName, 'metadata', metadata)
}
export function getCover (fileName) {
return getBookObject(fileName, 'cover')
}
export function saveCover (fileName, cover) {
setBookObject(fileName, 'cover', cover)
}
export function getFontFamily (fileName) {
return getBookObject(fileName, 'fontFamily')
}
export function saveFontFamily (fileName, fontFamily) {
setBookObject(fileName, 'fontFamily', fontFamily)
}
export function getTheme (fileName) {
return getBookObject(fileName, 'theme')
}
export function saveTheme (fileName, theme) {
setBookObject(fileName, 'theme', theme)
}
export function getFontSize (fileName) {
return getBookObject(fileName, 'fontSize')
}
export function saveFontSize (fileName, fontSize) {
setBookObject(fileName, 'fontSize', fontSize)
}
export function getBookObject (fileName, key) {
if (getLocalStorage(`${fileName}-info`)) {
return getLocalStorage(`${fileName}-info`)[key]
} else {
return null
}
}
export function setBookObject (fileName, key, value) {
let book = {}
if (getLocalStorage(`${fileName}-info`)) {
book = getLocalStorage(`${fileName}-info`)
}
book[key] = value
setLocalStorage(`${fileName}-info`, book)
}
接下来带入业务场景进行使用,我们分别实现字体和字号的存储。
字体存储:
首先我们要在setFontFamily函数中对字体进行保存
setFontSize (fontSize) {
this.setDefaultFontSize(fontSize)
// 字号的离线存储
saveFontSize(this.fileName, fontSize)
this.currentBook.rendition.themes.fontSize(fontSize)
}
再进行字体初始化操作,这里我们将其抽象为一个函数
initFontFamily () {
let font = getFontFamily(this.fileName)
if (!font) {
saveFontFamily(this.fileName, this.defaultFontFamily)
} else {
this.rendition.themes.font(font)
this.setDefaultFontFamily(font)
}
}
在this.rendition.display()
返回的promise对象进行调用
this.rendition.display().then(() => {
this.initFontFamily()
})
接下来字号设置,与字体类似:
先在setFontSize函数中对字号进行保存
setFontSize (fontSize) {
this.setDefaultFontSize(fontSize)
// 字号的离线存储
saveFontSize(this.fileName, fontSize)
this.currentBook.rendition.themes.fontSize(fontSize)
}
=>EbookReader.vue
initFontSize () {
let fontSize = getFontSize(this.fileName)
if (!fontSize) {
saveFontSize(this.fileName, this.defaultFontSize)
} else {
this.rendition.themes.font(fontSize)
this.setDefaultFontFamily(fontSize)
}
}
...
this.rendition.display().then(() => {
this.initFontSize()
})
3.10标题国际化
这里我们需要使用Vue-i18n这个插件
首先我们准备好资源文件,在src目录下建立文件夹lang来管理语言,在lang中放入我们的en.js和cn.js。
接下来安装插件:cnpm i -save vue-i18n
在lang文件夹下创建Js文件index.js,在其中引入库和资源文件,
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import en from './en'
import cn from './cn'
import { getLocale, saveLocale } from '../utils/localStorage'
Vue.use(VueI18n)
const messages = {
en, cn
}
// 语言的获取和存储
let locale = getLocale()
if (!locale) {
// 默认语言为中文
locale = 'cn'
saveLocale(locale)
}
const i18n = new VueI18n({
locale,
messages
})
export default i18n
在项目中使用
<span class="ebook-popup-title-text">{{$t('book.selectFont')}}</span>
这是我们选择中文时,显示选择字体,选择英文时,显示Select Font
3.11 阅读器主题设置功能实现
首先建立相应的组件EbookSettingTheme.vue,在组件中完成结构和样式:
<transition name="slide-up">
<div class="setting-wrapper" v-show="menuVisible && settingVisible===1">
<div class="setting-theme">
<div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
<div class="preview" :class="{'selected': item.name === defaultTheme}"
:style="{background: item.style.body.background}"></div>
<div class="text" :class="{'selected': item.name === defaultTheme}">{{item.alias}}</div>
</div>
</div>
</div>
</transition>
...
<style lang="scss" rel="stylesheet/scss" scoped>
@import "../../assets/styles/global.scss";
.setting-wrapper {
position: absolute;
bottom: px2rem(48);
left: 0;
z-index: 101;
width: 100%;
height: px2rem(90);
background: white;
box-shadow: 0 px2rem(-8) px2rem(8) rgba(0, 0, 0, .15);
.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;
&.selected {
box-shadow: 0 px2rem(4) px2rem(6) 0 rgba(0, 0, 0, .1);
}
}
.text {
flex: 0 0 px2rem(20);
font-size: px2rem(14);
color: #ccc;
@include center;
&.selected {
color: #333;
}
}
}
}
}
</style>
这里我们需要一个themeList的数据对象,老规矩,在book.js中创建
export function themeList (vue) {
return [
{
alias: vue.$t('book.themeDefault'),
name: 'Default',
style: {
body: {
'color': '#4c5059',
'background': '#cecece'
},
img: {
'width': '100%'
},
'.epubjs-hl': {
'fill': 'red', 'fill-opacity': '0.3', 'mix-blend-mode': 'multiply'
}
}
},
{
alias: vue.$t('book.themeGold'),
name: 'Gold',
style: {
body: {
'color': '#5c5b56',
'background': '#c6c2b6'
},
img: {
'width': '100%'
},
'.epubjs-hl': {
'fill': 'red', 'fill-opacity': '0.3', 'mix-blend-mode': 'multiply'
}
}
},
{
alias: vue.$t('book.themeEye'),
name: 'Eye',
style: {
body: {
'color': '#404c42',
'background': '#a9c1a9'
},
img: {
'width': '100%'
},
'.epubjs-hl': {
'fill': 'red', 'fill-opacity': '0.3', 'mix-blend-mode': 'multiply'
}
}
},
{
alias: vue.$t('book.themeNight'),
name: 'Night',
style: {
body: {
'color': '#cecece',
'background': '#000000'
},
img: {
'width': '100%'
},
'.epubjs-hl': {
'fill': 'red', 'fill-opacity': '0.3', 'mix-blend-mode': 'multiply'
}
}
}
]
}
我们需要把组件放入menu中,所以在menu组件引入、注册加放坑
<template>
...
<ebook-setting-theme></ebook-setting-theme>
...
</template>
<script>
...
import EbookSettingTheme from './EbookSettingTheme'
...
components: {
...
EbookSettingTheme
},
...
</script>
接下来我们给主题绑定点击事件@click="setTheme(index)"
setTheme (index) {
this.setDefaultTheme(theme.name)
}
为了方便管理,我们把themeList混入到mixin.js中
import { themeList } from './book'
...
export const ebookMixin = {
computed: {
...
themeList () {
return themeList(this)
}
},
...
}
实现主题切换和缓存功能
// 前面需要引入getTheme、saveTheme,
initTheme () {
let defaultTheme = getTheme(this.fileName)
if (!defaultTheme) {
defaultTheme = this.themeList[0].name
// 设置到vuex中
this.setDefaultTheme(defaultTheme)
saveTheme(this.fileName, defaultTheme)
}
this.themeList.forEach(theme => {
this.rendition.themes.register(theme.name, theme.style)
})
// 设置默认样式
this.rendition.themes.select(this.defaultTheme)
}
// 后面将initTheme()在initEpub的display中异步调用
=>setTheme组件
setTheme (index) {
const theme = this.themeList[index]
// 异步调用
this.setDefaultTheme(theme.name).then(() => {
this.currentBook.rendition.themes.select(this.defaultTheme)
})
saveTheme(this.fileName, theme.name)
}
3.12 全局主体设置功能实现
动态全局主题设置的原理是为DOM动态添加删除css文件,
—找到head标签,添加我们自定义的head标签即可。
在Book.js中添加方法addCss实现动态添加css
export function addCss (href) {
// 创建link标签
const link = document.createElement('link')
// 设置link标签属性
link.setAttribute('rel', 'stylesheet')
link.setAttribute('type', 'text/css')
link.setAttribute('href', href)
// 追加到head中
document.getElementsByTagName('head')[0].appendChild(link)
}
将theme文件放入resource生成url链接。
在EbookReader.vue中添加一个方法,initGlobalStyle()初始化全局样式
import { addCss } from '../../utils/book'
...
initGlobalStyle () {
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
}
...
this.rendition.display().then(() => {
this.initTheme()
this.initFontSize()
this.initFontFamily()
this.initGlobalStyle()
})
实现主题的动态切换,通过switch函数,根据当前主题加载响应的css文件
initGlobalStyle () {
switch (this.defaultTheme) {
case 'Default':
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
break
case 'Eye':
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
break
case 'Gold':
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`)
break
case 'Night':
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`)
break
default:
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
break
}
}
这时切换主题,存在两个问题
问题1:只有阅读器实现主题切换,而全局主题没有切换
initTheme () {
let defaultTheme = getTheme(this.fileName)
if (!defaultTheme) {
// this.setDefaultTheme(defaultTheme)把这句放在括号外面?
defaultTheme = this.themeList[0].name
saveTheme(this.fileName, defaultTheme)
}
// 设置到vuex中
this.setDefaultTheme(defaultTheme)
this.themeList.forEach(theme => {
this.rendition.themes.register(theme.name, theme.style)
})
// 设置默认样式
this.rendition.themes.select(this.defaultTheme)
},
问题2:刷新页面,全局主题没有得到保存
首先,我们将initGlobalStyle方法放入到mixin中实现复用。
在EbookReader和setTheme组件中分别调用:
methods: {
setTheme (index) {
const theme = this.themeList[index]
this.setDefaultTheme(theme.name).then(() => {
this.currentBook.rendition.themes.select(this.defaultTheme)
this.initGlobalStyle() // EbookSettingTheme.vue中调用
})
saveTheme(this.fileName, theme.name)
}
}
这时可以实现模式切换了,但是用户每点击一次切换主题在head标签中就多一个css文件,接下来我们要对这些样式做一下清除。
首先,我们在book.js中添加removeCss方法和removeAllCss方法
export function removeCss (href) {
const links = document.getElementsByTagName('link')
for (let i = links.length; i >= 0; i--) {
const link = links[i]
if (link && link.getAttribute('href') && link.getAttribute('href') === href) {
link.parentNode.removeChild(link)
}
}
},
export function removeAllCss () {
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`)
}
在mixin.js中initGlobalStyle方法的最前面调用removeAllCss进行初始化。
四、阅读器–阅读进度、目录、全文搜索功能开发
4.1阅读进度功能实现(进度面板和分页逻辑)
首先我们创建阅读进度组件,EbookSettingProgress.vue,写入结构和样式,混入ebookMixin
<template>
<transition name="slide-up">
<div class="setting-wrapper" v-show="menuVisible && settingVisible ===2">
<div class="setting-progress">
<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"
ref="progress">
</div>
<div class="text-wrapper">
<span>{{bookAvailable ? progress + '%' : '加载中...'}}</span>
</div>
</div>
</div>
</transition>
</template>
<script>
import { ebookMixin } from '../../utils/mixin'
export default {
mixins: [ebookMixin]
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
@import "../../assets/styles/global.scss";
.setting-wrapper {
position: absolute;
bottom: px2rem(48);
left: 0;
z-index: 101;
width: 100%;
height: px2rem(60);
background: white;
box-shadow: 0 px2rem(-8) px2rem(8) rgba(0, 0, 0, .15);
.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% !important;
&: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;
}
}
}
</style>
将新建的组件在EbookMenu中导入、注册、引用
<ebook-setting-progress></ebook-setting-progress>
...
import EbookSettingProgress from './EbookSettingProgress'
...
export default {
mixins: [ebookMixin],
components: {
EbookSettingFont,
EbookSettingFontPopup,
EbookSettingTheme,
EbookSettingProgress
}
...
}
完善布局,在左右加上图标,表示上一章节和下一章节,并加入样式
<div class="progress-wrapper">
<div class="progress-icon-wrapper">
<span class="icon-back" @click="prevSection()"></span>
</div>
<input ...>
<div class="progress-icon-wrapper">
<span class="icon-forward" @click="nextSection()"></span>
</div>
</div>
...
.progress-wrapper {
...
.progress-icon-wrapper{
font-size: px2rem(20);
}
}
在进度条上方布局–显示阅读时间
<div class="read-time-wrapper">
<span class="read-time-text"></span>
<span class="icon-forward"></span>
</div>
...
.read-time-wrapper{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: px2rem(40);
font-size: px2rem(12);
@include center;
}
实现进度条的正常使用—设置分页逻辑
在实现分页逻辑之前,我们进行了代码重构,建立了initRendition()函数和初始化手势函数initGesture()函数,并在initEpub中进行调用
下面,编写简单的分页算法,实现分页,在initEpub中
this.book.ready.then(() => {
return this.book.locations.generate(750 * (window.innerWidth / 375) * (getFontSize(this.fileName / 16)))
}).then(locations => {
this.setBookAvailable(true)
})
4.2阅读进度功能实现(进度条拖动功能)
在之前的结构中,我们给进度条绑定了两个事件
<input
//@change移除焦点触发事件
@change="onProgressChange($event.target.value)" @input="onProgressInput($event.target.value)">
// 进度条拖动松手以后调用的方法
onProgressChange (progress) {
this.setProgress(progress).then(() => {
this.displayProgress()
this.updateProgressBg()
})
},
// 进度条拖动过程中调用的方法
onProgressInput (progress) {
this.setProgress(progress).then(() => {
this.updateProgressBg()
})
},
displayProgress () {
const cfi = this.currentBook.locations.cfiFromPercentage(this.progress / 100)
this.currentBook.rendition.display(cfi)
},
updateProgressBg () {
// 拖动进度条,读过的部分进度条颜色加深
this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
}
},
updated () {
// 调用updated钩子函数实现进度条初始化
this.updateProgressBg()
}
4.3阅读进度功能实现(上下章切换)
先来实现上一章功能
我们先前在vuex(book.js)中定义了一个变量section,用来指定当前章节的位置,0表示第一章…
prevSection () {
// 章节不为负且书籍解析完成
if (this.section > 0 && this.bookAvailable) {
this.setSection(this.section - 1).then(() => {
const sectionInfo = this.currentBook.section(this.section)
if (sectionInfo && sectionInfo.href) {
this.currentBook.rendition.display(sectionInfo.href)
}
})
}
}
我们把display出来,因为下一章跳转也要用
displaySection () {
const sectionInfo = this.currentBook.section(this.section)
if (sectionInfo && sectionInfo.href) {
this.currentBook.rendition.display(sectionInfo.href)
}
}
那么,上下翻页的函数变为:
prevSection () {
// 章节不为负且书籍解析完成
if (this.section > 0 && this.bookAvailable) {
this.setSection(this.section - 1).then(() => {
this.displaySection()
})
}
},
nextSection () {
// this.currentBook.spine.length表示章节数
if (this.section < this.currentBook.spine.length - 1 && this.bookAvailable) {
this.setSection(this.section + 1).then(() => {
this.displaySection()
})
}
},
...
4.4阅读进度功能实现(章节切换和进步同步)
实现章节变化时,进度百分比同步变化
定义函数refreshLocation刷新当前位置,并在displaySection()中调用即可
refreshLocation () {
// 刷新当前位置
const currentLocation = this.currentBook.rendition.currentLocation()
// 获取当前进度百分比
const progress = this.currentBook.locations.percentageFromCfi(currentLocation.start.cfi)
this.setProgress(Math.floor(progress * 100))
}
下面来实现获取相应的章节名称,显示在百分比的左边,首先完成布局
<div class="text-wrapper">
<span class="progress-section-text">{{getSectionName}}</span>
<span>({{bookAvailable ? progress + '%' : '加载中...'}})</span>
</div>
这里我们用getSectionName函数来获取当前章节名称
computed: {
getSectionName () {
if (this.section) {
const sectionInfo = this.currentBook.section(this.section)
if (sectionInfo && sectionInfo.href) {
return this.currentBook.navigation.get(sectionInfo.href).label
}
}
}
},
这时可以实现功能,但样式需要继续优化,章节名称太长时实现一行显示,为了以后代码复用,我们把它写在mixin.scss中
@mixin ellipsis{
//省略显示
text-overflow:ellipsis;
overflow: hidden;
white-space: nowrap;
}
然后在章节显示的样式中调用
.progress-section-text {
/*设置章节一行显示*/
@include ellipsis;
}