🎈 Author: ZENDU

🎈 DESCRIBE:A Ebook City Base On Vue3

🎈 Origin: https://coding.imooc.com/learn/list/285.html

🎈 Guide: https://www.bilibili.com/video/BV1Yt4y167c8

🎈 Github Repository: https://github.com/ArchKS/eBook

本项目有三个核心的页面,分别为:书架页面,书城首页,阅读器页面。

在书城首页,用户可以寻找感兴趣的图书,点击书籍封面,可以进入书籍详情页,详情页中可以阅读书籍、缓存书籍和将书籍加入到书架,详情页也是各组件路由之间的桥梁。

点击详情页中的阅读书籍可以进入到阅读器界面,阅读器是本项目最重要的功能,它实现了对ePub格式电子书的解析和渲染。同时在阅读器界面还支持查看当前阅读进度、切换上下章节,切换电子书的字体字号、改变电子书的主题颜色、生成电子书目录、下拉添加书签、以及电子书的全文检索和高亮显示。

详情页中,缓存书籍就是将书籍下载并存储到indexDB数据库中;加入书架就是将书籍加入到书架列表,书架中的书籍支持拖拽分组等功能。

经验体会

刚开始写前端的时候,只会写一些小页面,对于多个页面在一套代码里怎么布局,脑子里一点思路都没有。学习了这个项目和看过一些文章之后,大抵对之前的茫然有了一丝醒悟。单页面多组件开发的页面切换采用路由来实现,通过路由,将对应的组件渲染到<router-view>的位置,来实现html的置换,在本项目中,router-view存在于App.vue组件中,Vue通过构造Vue实例,再将App.vue渲染到 public/index.html#app 的位置,这样就可以看到一个完整的前端了。

再者是对菜单栏切换的茫然(标题栏菜单栏样式见 **代码逻辑-标题栏菜单栏功能 ** ),为什么点击目录图标可以呼出目录,为什么点击进度条可以呼出进度条,想象中当是菜单栏通过某种技术将子菜单藏在自己身体里,根据用户事件,对自身状态做相应改变。后来才发现,菜单栏组件、进度条组件、字体字号设置组件等都是完全独立的组件,它们和Menu毫无关系,它们的显示和隐藏只是单纯的通过v-if="ifSettingVisible===3"来实现,这些小组件和菜单栏之间的耦合仅仅只有一个变量,顿时觉得落差好大。

还有一个就是蒙版,比如目录的蒙版、推荐阅读蒙版、分组蒙版等,其实就是一个全屏的div,没有什么新奇的东西。

对于之后的开发建议的标准:

1、CSS方面

D:\TEMPDIR\GITHUB-EBOOK\SRC\ASSETS\STYLES
│ color.scss
│ global.scss
│ icon.css
│ mixin.scss
│ reset.scss

└─fonts

基础目录还应该是这样,所有css(less,scss,css)代码集成到global中,

color.scss 负责网站整体颜色

zindex.scss 负责网站整体层级,开发时应该化出网站组件的层级并生成堆叠图

mixin.scss 负责精简代码,比如flex的居中代码会在很多地方用到,可以精简到mixin中

2、Vue方面

关乎项目整体的变量,应该抽象到VueX中

关乎项目整体的代码,应该抽象到mixin中

关乎项目整体的组件,应该抽象到components/common中,以便形成组件的复用(比如scroll/toast等组件)

项目开始前应当明确各页面之间的跳转关系,并以此构建路由和子路由表

明确 .env.production & .env.development 变量使用

如果想要采用响应式,可以在App.vue中初始化Html.fontSize

整体设计

页面逻辑

总共分为五个页面,分别是用户的书架列表和分类页面、系统的书城首页、书籍的详情页、和最后的阅读页面。如图显示了页面的跳转信息,这里就不一一赘述了。

流程安排

  1. 了解阅读器原理
    1. ePub解析原理
    2. ePub标准
    3. ePubApi使用
  2. 搭建开发环境
    1. Vue3.0
    2. Vue-Router
    3. VueX
    4. Epubjs
    5. Node -> Static File Service
  3. 阅读器的开发
    1. ePub电子书渲染到页面
    2. 标题和菜单栏
    3. 翻页和标题菜单栏显示隐藏
    4. 主题 / 字体 / 字号 切换
    5. 阅读进度 / 切换上下章节 / 进度同步
    6. 阅读器页眉页脚开发,页眉显示章节名,页脚显示进度百分比
    7. 阅读时间同步
    8. 整体结合Vuex和localStorage,抽象出refreshLocation方法
    9. 目录书签组件开发
      1. 动态组件绑定
      2. 搜索框
      3. 书籍信息
      4. 滚动条
      5. 目录信息和跳转 ,抽象出display方法
      6. 书签组件开发
      7. 全文查找
      8. 高亮显示
      9. 文章中高亮显示
    10. 动态绑定主题,即动态创建link元素并append到head后面
  4. 书城首页的开发
    1. 首页标题、搜索框、返回按钮之间的交互动画
    2. shake推荐图书动画和推荐图书组件
    3. 热门搜索组件
    4. banner图片
    5. 猜你喜欢组件开发
    6. 精选 / 推荐 / 分类图书 / 分类列表 组件开发
  5. 书籍详情页的开发
    1. 书籍信息展示部分
    2. 系统功能开发
      1. 阅读 结合Vue-Router传递书籍信息
      2. 缓存书籍 + IndexDB
      3. 加入书架
  6. 书架列表的开发
    1. 书架标题 (清除缓存 + 编辑 + Title),选中图书会显示副标题
    2. 搜索栏
    3. 书籍列表
      1. 简单书籍组件开发 ,点击跳转到详情页
      2. 书籍分组组件开发 ,点击跳转到书籍分组中
      3. Add-ICON 组件开发,点击跳转到书城首页
    4. 路由维护和跳转,定义路由跳转方法
  7. 分类列表的开发
    1. 列表标题显示和重命名
    2. 列表背景
    3. 列表项目罗列
    4. 拖拽分组开发
  8. 项目构建、调试、上线
    1. 打包测试 & Fix BUGs
    2. Nginx跨域
    3. Vue-Router更换为History模式
    4. 上线发布

技术栈

开发日志

https://github.com/ArchKS/eBook

SaSS

这里分为两部分总结,首先是项目整体的sass代码布局,然后是具体sass代码的使用。对于本项目,sass代码分散在assets和不同的vue组件内部,assets目录中的代码是公共代码,统一集成到global.scss中,形成了如下的目录风格,color.scss用于管理整体颜色,icon.scss管理字体图标库,reset.scss重置浏览器默认样式,mixin.scss集成复用代码,global.scss集成所有scss样式,然后采用@import "@/assets/styles/global.scss";的方式,统一导入到每个组件

1
2
3
4
5
6
7
8
D:\TEMPDIR\GITHUB-EBOOK\SRC\ASSETS\STYLES
│ color.scss
│ global.scss
│ icon.css
│ mixin.scss
│ reset.scss

└─fonts

1、定义和使用变量

1
2
$primary-color: #333;
color: $primary-color;

2、层级嵌套

1
2
3
4
5
6
7
8
9
10
nav { 
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline-block;
}
}

3、混入 mixin

1
2
3
4
5
6
7
8
@mixin transform($property) {
-webkit-transform: $property;
-ms-transform: $property;
transform: $property;
}
.box {
@include transform(rotate(30deg));
}

4、扩展extend

extend和mixin的区别:https://www.w3cplus.com/preprocessor/sass-mixin-or-extend.html

1
2
3
4
5
6
7
.button { 
    background: green;
 } 
 
.button-1 { 
    @extend .button;
}

5、数学运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
.container {
width: 100%;
}

article[role="main"] {
float: left;
width: 600px / 960px * 100%;
}

aside[role="complementary"] {
float: right;
width: 300px / 960px * 100%;
}

HTML5

localStorage

项目的相关状态信息,比如书架中的书籍、电子书的阅读进度时间、读者选择的主题字号字体等都需要保存起来(Vuex是不能保存的,Vuex相当于一个对象变量,浏览器刷新后,就会重置),所以需要保存到浏览器的本地缓存,下面是localStorage的几个API,本次项目中采用 web-storage-cache 库,可以直接存取JSON格式。

1
2
3
4
localStorage.setItem('myCat', 'Tom');       // 增
const cat = localStorage.getItem('myCat'); // 查
localStorage.removeItem('myCat'); // 删
localStorage.clear();

https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

https://www.npmjs.com/package/web-storage-cache

IndexDB

因为localStorage有容量限制,大约是5M,对于体积较大的电子书无法缓存。indexDB作为前端数据库可以很好的实现这样点,它有点类似于NOSQL,采用键值对的形式异步存取数据。本次项目中,采用基于indexDB的封装库 localForage 实现电子书缓存。

https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import localForage from 'localforage'

localForage.setItem(key, data)
.then(value => {
// 成功的回调
})
.catch(err => {
// 失败的回调
})


localForage.removeItem(key)
.then(()=> {
// 成功的回调
}).catch( err => {
// 失败的回调
})

ePub解析原理

epubjs解析电子书

  1. 首先要把电子书解压缩,比如2015_Book_GlobalBusinessStrategy.epub 这本电子书解压后会形成三部分内容:META-INF、OEBPS、mimetype

  2. 其次是在META-INF目录下找到container.xml文件,该文件记载了content.opf的路径

image-20210121112251422
  1. 第三步就是解析content.opf

    • metadata记载了图书的基本信息
    • manifest记载了点击书所有的资源文件路径
    • spine对应电子书资源文件的排列顺序,spine会对应一个toc=ncx文件,这个就是电子书的目录,打开./toc.ncx之后可以看到电子书的目录信息,如图所示
    • Guide是指南信息
    image-20210121112410334

ePub标准

epub标准主要解决电子书的分发,管理,加密等问题,ePub的核心是:ZIP + XML + 资源文件 (HTML+CSS+音频+视频等)。

epub就是利用ZIP技术,将不同类型的文件打包成一个文件,用XML技术进行资源配置管理,用HTML、CSS、图片等技术进行电子书的展示

Epub阅读器引擎

Epubjs是基于JavaScript的ePub引擎,解决了ePub电子书的解析、渲染、定位等技术难题。

核心类 介绍
Book 阅读器的解析
Rendition 阅读器的渲染
locations 负责阅读器的定位
Navigation 存储了目录信息
View Manager 渲染视图完整性
EpubCFI 利用了EpubCFI标准进行子节定位,可以定位到电子书中的任意一个字符
Theme 负责管理场景切换
Spine 负责管理阅读顺序和章节
Section 指向一个具体的章节
Contents 负责管理一个章节中的资源内容
Hook 定义了钩子函数,用于管理某个类的生命周期
Annotations 负责管理标签

EpubApi

下面罗列以下本项目中主要使用到的EpubJS的APi,代码中的ebook 是电子书实例,rendition 是电子书渲染实例。

Ebook实例 Rendition实例

渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
import ePub from "epubjs";
// 初始化Book
// url是电子书的路径 比如: http://localhost:3000/Ebooks/EarthSciences/2018_Book_HandbookOfMathematicalGeoscien.epub
var ebook = ePub(url);

// 将Book渲染到页面上
// area是页面元素的ID
let rendtion = ebook.renderTo("area", {
width: window.innerWidth,
     height: window.innerHeight,
     method: "default"// 支持微信
     // flow: 'scrolled' // 滚动模式
});

翻页

1
2
3
4
5
// 上一页
rendtion.prev();

// 下一页 
rendtion.next();

跳转到指定CFI位置

1
2
// 传入href就可以跳转到ebook的指定位置
ebook.rendition.display(href);  // href: "A333616_1_En_1_Chapter.html"

切换章节

1
2
3
4
5
6
7
8
9
10
11
12
// 获取第2个章节的信息
// 其中 sectionNum是章节标号,是一个数字类型
let sectionInfo = ebook.section(2);
let sectionName = sectionInfo.label; // 获取章节名
let href = sectionInfo.href ;      // 获取第二个章节的href位置
ebook.rendition.display(href);   // 跳转到href的位置


/* 章节信息 和 拆分跳转 */
const  sections = ebook.locations.spine.items;  // 获取所有目录信息,如items中所示
const oneHref = sections[3].href;               // 获取第三个目录信息
ebook.rendition.display(oneHref)                // 跳转到当前目录
items章节信息

跳转到指定百分比位置

1
2
3
4
5
6
// 通过百分比获取那个位置的CFI,然后用display跳转
const cfi = ebook.locations.cfiFromPercentage( 30 / 100);
ebook.rendition.display(cfi);

// 通过设置background-size 来达到两边背景色差
progress.style.backgroundSize = `${progress}% 100%`;

获取图书元信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 获取图书元信息 */
ebook.loaded.cover.then(res=>{
    // 封面
    console.log('封面: ',res); // http://localhost:6866/ebook/OEBPS/A978-4-431-55468-4_CoverFigure.jpg
})

ebook.loaded.manifest.then(res=>{
    console.log('manifest: ',res); // 资源文件信息
})

ebook.loaded.navigation.then(res=>{
    console.log('navigation: ',res.toc); // 目录信息
}) 

ebook.loaded.resources.then(res=>{
    console.log('resources: ',res); // 资源文件信息
})

ebook.loaded.spine.then(res=>{
    console.log('spine: ',res); // 每一个章节信息
})

主题切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var MyTheme = {
    alias: '护眼',
    name: 'Eye',
    style: {
        body: {
            'color''#cecece',
            'background''#000000',
        }
    }
}
/* 注册主题 */
ebook.rendition.themes.register(MyTheme.name, MyTheme.style);

/* 切换主题 */
ebook.rendition.themes.select('Eye');

字体字号切换

1
2
3
4
5
6
7
8
9
10
11
/* 注册字体 */
ebook.rendition.hooks.content.register(contents=>{
    contents.addStylesheet('http://ebook.ymlog.cn/Style/fonts/montserrat.css');
})

/* 切换字体 */
ebook.rendition.themes.font("Lucida Console");


/* 切换字号 */
ebook.rendition.themes.fontSize('20px'); 

添加书签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let bookmark = [];

// 获取当前章节信息
let currentLocation = ebook.rendition.currentLocation();

let cfiBase = currentLocation.start.cfi.replace(/!.*/g, "");
let cfiStart = currentLocation.start.cfi.replace(/.*!/g, "").replace(/\)$/, "");
let cfiEnd = currentLocation.end.cfi.replace(/.*!/g, "").replace(/\)$/, "");

// 拼接章节开始和结束
const cfiRange = `${cfiBase}!,${cfiStart},${cfiEnd})`;
console.log(cfiRange);

// 获取当前章节范围内的内容
ebook.getRange(cfiRange).then(res => {
let text = res.toString().replace(/\s\s/g, ""); // 去除多余的空格
bookmark.push({
cfi: currentLocation.start.cfi,
text: text,
})
});

定位当前章节的currentLocation是什么:?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
atStart: true
end:
cfi: "epubcfi(/6/2[ACoverHTML]!/4/4/1:1473)"
displayed: {page: 1, total: 3}
href: "ACoverHTML.html"
index: 0
location: -1
percentage: 0

start:
cfi: "epubcfi(/6/2[ACoverHTML]!/4/4/1:0)"
displayed: {page: 1, total: 3}
href: "ACoverHTML.html"
index: 0
location: -1
percentage: 0

如何获取某页的内容?

1
2
3
4
5
6
7
8
拼接如下格式的字符串: A,B,C
epubcfi(/6/2[ACoverHTML]!,/4/4/1:0,/4/4/1:1473);
A="epubcfi(/6/2[ACoverHTML]!" 当前电子书
B="/4/4/1:0" 页面起使位置
C="/4/4/1:1473" 页面结束位置

通过ebook.getRange(cfiRange).then(res=>res)方法,就可以获取指定CFI的内容了

全文检索

1
2
3
4
5
6
7
8
9
10
/* q是搜索的关键词,返回一个搜索结果列表 */
function doSearch(q) {
return Promise.all(
ebook.spine.spineItems.map(item => item.load(ebook.load.bind(ebook)).then(item.find.bind(item, q)).finally(item.unload.bind(item)))
).then(results => Promise.resolve([].concat.apply([], results)));
};

doSearch(' land ').then(res=>{
console.log(res);
})

全文搜索doSearch的返回结果,cfi标记位置,excerpt标记内容。这里需要注意一点,CFI标记的位置精度是字符,比如搜索结果的第一条记录中CFI末尾为/1102 /1108,1102就是指第一个 字符在电子书中的位置,而1107就是最后一个 在电子书中的位置。依次类推,land中的an在电子书中的位置就依次是1104和1105。

1
2
3
4
{
cfi: "epubcfi(/6/6[A333616_1_En_1_Chapter]!/4/8/2[Sec1]/10[Par5],/1:1102,/1:1108)"
excerpt: "...nfall. The companies here comply with these regulations by increasing their land area in relation to the size of their buildings and reusing wastewate..."
}

文字高亮

其中的/1:730,/1:741就是高亮的文字,共11个字符

1
2
3
4
5
6
const testCFI = 'epubcfi(/6/6[A333616_1_En_1_Chapter]!/4/8/6[Sec3]/8[Sec6]/10[Par20],/1:730,/1:741)'
ebook.rendition.display(testCFI)

ebook.rendition.annotations.highlight(testCFI,{},(e) => {
console.log("highlight clicked", e.target); // 点击高亮,触发事件
})

代码逻辑

讲解整个项目中难点、重点的业务实现

标题菜单栏功能

先介绍一下基本逻辑,点击页面会显示 上方标题 和 下方菜单栏,点击菜单栏下面的几个按钮,会显示对应的按钮功能。 具体原理就是页面最外层套了个透明的Mask,劫持了鼠标点击事件,如果点击了,就将ifTitleAndMenuVisible状态取反。而下方不同功能之间的切换则是通过设置ifSettingVisible的值来实现的,如下:

1
2
3
4
0: 目录
1: 进度
2: 主题
3: 字号

再结合组件中的v-if进行判断,这样就可以通过点击传递参数,设置对应功能组件的显示和隐藏了:

1
2
3
<span @click="menuSetting(1)" class="icon-progress"></span> // 设置进度条的显示或隐藏 EbookMenu.vue :7 line

<div class="setting-wrapper" v-show="ifTitleAndMenuVisiable && ifSettingVisible === 1"> // EbookProgressSetting.vue :2 line

书签下拉和手势操作

实现了电子书的翻页、下拉添加书签的功能,同时支持鼠标和手势,主要代码在EbookReader.vue: 712 line 和 EbookBookmark.vue:4675 line。

实现效果节选:

1
2
3
4
5
6
@click="onMaskClick"
@touchmove="move"
@touchend="moveEnd"
@mousedown.left="onMouseEnter"
@mousemove.left="onMouseMove"
@mouseup.left="onMouseUp"

手指滑动事件

  1. 当手指滑动时,记录下初始滑动的位置,滑动过程中,不断把当前滑动位置 - 初始滑动位置,从而得出滑动距离和方向。
  2. 在 EbookBookmark.vue 中监听offSetY的值
    1. 如果当前不存在书签
      1. y < Critical Value1 将 ‘text’ 改为 ‘下拉添加书签’ ;
      2. Critical Value1 < y < Critical Value2 ,将 ‘text’ 改为 ‘松手添加书签’;书签颜色变蓝 ;下拉箭头翻转180°指向上方
      3. y = 0 , 重置样式,并添加 or 删除书签

鼠标滑动事件

  1. 鼠标按下时(mousedown),将鼠标的状态置为1,记录当前时间戳
  2. 鼠标移动时(mousemove)
    1. 如果鼠标状态为1,则将鼠标状态置为2;
    2. 如果鼠标状态为2,则计算鼠标偏移量offSetY(类似于上方的手指偏移量)
    3. 如果鼠标状态 !=1 / 2,则无操作
  3. 鼠标抬起时(mouseup)
    1. 如果鼠标状态为2,则将偏移量置为0 ( 书签归位 ),同时将鼠标状态置为3
    2. 否则将鼠标状态置为4
    3. 判断总共滑动的时间,如果 < 100ms ,则将鼠标状态置为4

鼠标点击事件

  1. 如果当前鼠标状态 = 2 / 3(表示鼠标在滑动) ,则不做任何操作
  2. 如果点击在屏幕 0 < clickTarget < 0.3 * innerWidth ,则上一页
  3. 如果点击在屏幕 0.7 * innerWIdth < clickTarget < innerWidth ,则下一页

标题搜索框的交互动画

交互逻辑

  1. 点击搜索框时,显示hotSearch组件,隐藏标题,返回icon下滑到搜索框左侧
  2. 点击返回按钮时
    1. 如果当前有hotSearch组件,则退出hotSearch组件;
      1. 如果当前offsetY = 0,则同时显示标题栏,不显示阴影
      2. 如果当前offset != 0 ,则不显示标题栏,显示阴影
    2. 如果当前没有hotSearch组件,则进度书架页面
  3. 在页面上滑动时
    1. 滑动到顶部,出现标题,隐藏阴影,返回icon移动到标题栏
    2. 不在顶部时,隐藏标题,显示阴影,返回icon移动到搜索栏

html/css结构

1
2
3
4
5
6
7
8
9
.wrapper
title_Text
shake_icon

.wrapper
back_icon

.wrapper
input

整体采用绝对定位,返回按钮采用动态绑定class的方式调整位置

关于书城首页

image-20210121220558682

书城首页分为猜你喜欢 ( GuessYouLike.vue ),热门推荐 ( Recommand.vue ),精选 ( Feature.vue ),分类图书 ( CategoryList.vue ),分类 ( Categories.vue ) 和 分类标题 ( TtileView.vue ) 几个组件,大多数采用flex & grid布局,实现相对容易,在这里不做过多赘述。另外由于时间匆忙,’查看全部’ -> 呈现的全部信息没有实现,也因为比较简单且内容和书城首页冗余,所以不打算写了。

书架分组实现

当时看到那个9宫格的书架,以为是用什么iframe抽象出来的,结果知识简单的一个shelfItemCategory.vue组件,向这个组件中传一个list,list中有很多Book,然后在组件中遍历Book,放上cover,就行了,十分的朴实无华。

拖拽分组和移出分组

原版作者采用的是 弹出框 + INPUT 控件的方式让用户创建分组,通过点击事件的方式让用户移出分组。我采用了一个更加符合人类直觉的方式,拖拽形成分组,拖拽取消分组。

bookshelfAndCategoryList
  1. 拖拽书籍A到书籍B上面,可以形成新的分组
    1. 按照分组规范 [ 2 ] 分组对象描述格式 创建一个新的分组,命名为 ‘新建分组’
    2. 将sourceBook & targetBook 加入到新建分组的itemList
    3. 在书架列表中删除 sourceBook 和 targetBook
  2. 拖拽书籍A到分组C上面,就是将书籍A加入到C分组
    1. 将 sourceBook 加入到C分组
    2. 在书架列表中删除 sourceBook
  3. 代码实现
    1. source 拖拽端 shelfItemBook 16 line
      1. 对于拖拽,只能在图片上使用,需要设置draggable="true"
      2. 只在shelfItemBook.vue组件(其他两个item组件分别为 AddIcon 和 CateGory )中添加@dragstart事件
      3. @dragstart记录当前拖拽的Book在书架列表中的sourceIndex
    2. target 放置端
      1. 添加 @dragover 事件,阻止冒泡和浏览器默认事件触发
      2. 添加 @drop 事件,用于触发当鼠标松开(即放置)这一行为 shelfItem: 108 line
        1. 获取target在书架列表中的targetIndex
        2. 如果targetIndex === sourceIndex,则什么都不做
        3. 如果targetItem的类型是book,则创建新分组
        4. 如果targetItem的类型是category,则将该图书加入目标分组
        5. BUG:分组信息 => 书架列表信息 => 同步到Vuex的时候,itemList内容会被清空,暂时解决是写入到 localStorage中,之后使用缓存的信息
      3. 当分组中没有图书时,分组会自动删除
      4. 进入分组,点击分组的title可以修改分组名,点击外侧区域可以保存修改。这里就用到了 v-if 套了两个组件,第一次看到的是div,点击之后看到的是input。

RefreshLocation干了什么

refreshLocation是eBook组件组中非常重要的一个方法,其本身也被display方法调用,每一次页面变化,都会调用该方法。下面罗列以下该方法到底做了什么:

  1. 重置章节 [type: Number]
  2. 保存当前阅读位置,EbookReader中会调用getLocation加载阅读位置,避免阅读进度信息丢失
  3. 保存阅读进度百分比
  4. 判断当前页面是否存在书签,如果存在,则将ifBookMark置为true (具体操作是:判断书签列表中的item 是否等于 当前页面首字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 刷新 section章节 progress阅读进度、location位置、书签bookmark
refreshLocation() {
// 获取当前位置
const currentLocation = this.book.rendition.currentLocation()
if (currentLocation && currentLocation.start) {
// 设置章节
this.SET_SECTION(currentLocation.start.index);

// 获取阅读进度 并 保存
const startCfi = currentLocation.start.cfi; // 当前页面首字节的位置
const progress = this.book.locations.percentageFromCfi(startCfi);
saveLocation(this.fileName, startCfi);

saveReadProgress(progress);
this.SET_PROGRESS(Math.floor(progress * 100));


// 是否显示书签
const bokmks = getBookmark(this.fileName);
if (bokmks) {
let isbookmark = bokmks.some(item => item.cfi === startCfi);
isbookmark ? this.SET_IFBOOKMARK(true) : this.SET_IFBOOKMARK(false);
}
}

文字超出显示省略号

单行文字

1
2
3
4
5
@mixin ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

多行文字

1
2
3
4
5
6
7
8
9
10
11
font-size: 12px;
line-height: 12px;
@mixin ellipseWithLine($line) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $line; // 文字行数
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
word-break: keep-all; // break-all 强制换行 keep-all 依据字符完整性换行
}

存在的BUG

1、details/foot.vue ,即详情页的页脚信息部分

打算实现的逻辑是:

  1. 进入详情页时,判断当前书架中是否有该书籍,如果有则显示 ‘ 已加入书架 ‘ ;如果没有,则显示 ‘加入书架’;判断当前书籍是否缓存,如果缓存了,则显示’已缓存’,如果没有,则显示’缓存书籍’。 Finish:50%
  2. 点击加入书架,就将当前book对象push到书架中,book规范请见规范标准 [ 1 ] 电子书对象描述格式,再次点击则将当前book移出书架 Finish:100%
  3. 点击缓存书籍,如果当前书籍在书架内,则缓存书籍到indexDB;如果当前不在书架内,则显示 ‘不再书架不能缓存’ Finish:50%
  4. 点击阅读,如果当前书籍已经下载,则从indexDB中读取Blob对象渲染,如果没有下载,则在线下载。Finish:100%

2、书籍是否加入书架问题

分组内的书籍无法判断是否在书架中,书籍在书架中的判断标准是:item.title = book.title,然而分组中的书籍在itemList中,所以该等式不成立,需要遍历itemList中的每一项,判断其title是否和当前书籍相等,若相等,则在书架中。

规范标准

[ 1 ] 电子书对象描述格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
author: "Steven A. Frank"
bookId: "2018_Book_ControlTheoryTutorial"
cache: false
category: 5
categoryText: "Engineering"
cover: "http://47.99.166.157/book/res/https://ymlog.cn/file//Engineering/978-3-319-91707-8_CoverFigure.jpg"
fileName: "2018_Book_ControlTheoryTutorial"
id: 5
language: "en"
publisher: "Springer International Publishing"
rootFile: "OEBPS/package.opf"
selected: false
title: "Control Theory Tutorial"
type: 1
}

其中:

  • cache:是否缓存,缓存为true
  • selected:是否选中,选中为true
  • type:1表示图书、-1表示Add图标,2表示目录
  • 默认比较图书采用 title进行比较

[ 2 ] 分组对象描述格式

1
2
3
4
5
6
7
{
title: "新建分组",
itemList: [book1, book2],
id: 1,
selected: false,
type: 2,
}
  • book1 , book2 符合电子书对象描述格式[ 1 ]

Reference

ePubCfi是什么 http://idpf.org/epub/linking/cfi/

http://epubjs.org/documentation/0.3/#epubcfi