如何开发高质量的Web阅读产品

做者:Samjavascript

前言

随着智能手机的普及,移动阅读成为愈来愈多人得到知识的选择,据统计移动阅读月活跃用户已突破3.2亿,2017年市场规模达到166亿,持续保持2位数增加,相比之下,2012年移动阅读市场仅为32.7亿,6年增加幅度超过5倍,让人惊叹。 html

2012-2017年中国移动阅读市场规模及增加率

正因如此,移动阅读市场巨头云集,各种阅读产品层出不穷。大部分优质阅读产品均为App版本,Web阅读产品虽多,但受限于以前的前端技术,体验与App相差甚远。近几年随着前端MVVM框架快速发展,Web阅读产品已经具有全面升级迭代的基础。前端

什么是高质量的Web阅读产品

点击这里体验高性能Web阅读器java

高质量的Web阅读产品应该至少具有书城、书架和阅读器三大模块,用户访问阅读器站点后,经过搜索+推荐+分类的方式找到本身感兴趣的电子书,查看详情后,将电子书加入书架,在书架中打开阅读器,读取电子书的内容进行阅读,功能结构以下图: git

阅读器功能结构
除此以外,高质量的阅读产品应提供流畅的阅读体验(切换流畅,不卡顿)和良好的兼容性(兼容PC端和移动端),可以接近原生App。

快速入门Web阅读器开发

2个月前我在慕课网发布了免费课《快速入门Web阅读器开发》(课程地址:点击这里,源码地址:点击这里,体验地址:点击这里),尝试用Vue.js+Webpack开发一个高质量的阅读器,事实证实,Vue.js能够为移动阅读带来突破性的提高。下面为你们介绍阅读器的实现原理和开发过程,本文重点介绍ePub电子书的实现过程,阅读器的原理以下图: es6

阅读器工做原理

电子书解析

ePub电子书的解析过程很是复杂,幸亏FuturePress(官网点击这里)帮咱们解决了这个问题,FuturePress是加州大学伯克利分校的一个跨学科项目,他们推出的epubjs库专门用于解决电子书的解析和渲染等复杂问题,安装过程很是简单github

npm install epubjs --save
复制代码

借助epubjs能够极大地下降阅读器的开发难度,使得我的开发者完成一个复杂的阅读产品成为可能。电子书的解析过程以下:算法

// 引入epubjs库
import Epub from 'epubjs'
// 设置全局的ePub对象
global.ePub = Epub
// 电子书的下载地址,这里提供一个测试电子书的地址,你们能够替换为本身感兴趣的电子书
const url = 'http://www.youbaobao.xyz/epub/History/2018_Book_TheCostOfInsanityInNineteenth-.epub'

// 解析电子书
this.book = new Epub(url)
复制代码

这里的book变量就是解析后的电子书对象npm

电子书渲染

电子书的渲染过程很是简单,经过Book.renderTo()方法便可实现数组

this.rendition = this.book.renderTo('reader', {
  width: window.innerWidth,
  height: window.innerHeight
})
复制代码

renderTo的第一个参数是div的id,阅读器会自动生成dom并挂载到指定div下,咱们须要经过id来匹配div,第二个参数能够指定阅读器的宽高,执行完毕后会生成Rendition对象,阅读器的渲染须要使用Rendition对象,渲染过程是调用Rendition的display()方法,他会返回一个Promise对象,以便咱们对渲染后的过程进行操做

this.rendition.display().then(() => {
  // 定义渲染完成后的操做
  ...
})
复制代码

翻页操做也很是简单,只须要调用Rendition.prev()Rendition.next()方法便可,这两个方法一样会返回Promise对象

// 上一页
this.rendition.prev()
// 下一页
this.rendition.next()
复制代码

字号设置

字号设置是阅读器必不可少的功能,用户但愿可以自主改变阅读器的字号大小,这个功能须要经过epubjs的Themes对象实现,Themes对象提供了fontSize()方法,传入实际字号便可快速修改字号大小

// 获取Themes对象
this.themes = this.rendition.themes
// 设置字号大小
this.themes.fontSize(16)
复制代码

主题设置

主题设置功能可让咱们改变阅读器的字体和背景色,进而修改阅读器的样式,经过Themes.register()方法注册主题,Themes.select()方法切换主题,主题容许实时切换

// 注册主题,body表示修改body标签的样式
this.themes.register('night', {
  body: { 'color': '#fff', 'background': '#000' },
  img: { 'width': '100%' }
})
// 切换主题
this.themes.select('night')
复制代码

进度设置

阅读过程当中,咱们会但愿快速切换到本身想要浏览的位置,一般会采用两种方法:拖动进度条快速定位和经过目录切换,本节咱们将介绍拖动进度条的切换方式,进度条能够借助HTML5新增的input range控件实现

<input type="range" :value="progress" max="100" min="0" step="1" @change="onProgressChange($event.target.value)" @input="onProgressInput($event.target.value)">
复制代码
  • 将input标签的type属性指定为range设置一个滑块控件
  • range绑定值为progress,max指定progress最大值为100,min指定progress最小值为0,step指定按照1的幅度进行增加,好比移动滑块1格,progress就会增加1
  • @change绑定了input的change事件,即修改完成后触发的事件,$event.target.value能够获取到最新的progress值
  • @input绑定了修改过程事件,拖动滑块即会触发

阅读进度修改的原理就是根据progress的值动态改变阅读器的位置,要改变阅读位置,首先须要进行分页,能够经过epubjs的Locations对象实现

// Book对象的钩子函数ready
this.book.ready.then(() => {
  // 执行分页
  return this.book.locations.generate()
}).then(result => {
  // 获取Locations对象
  this.locations = this.book.locations
})
复制代码

完成分页后,咱们能够经过Locations.cfiFromPercentage()方法获取百分比对应的EpubCFI,EpubCFI用于解决电子书的定位问题,它能够定位到电子书中任意一个字符,这一点在后续文章中会详细讲解,将EpubCFI直接传入Rendition.display()方法,便可跳转到百分比所对应的电子书位置

// 将百分比转化为小数形式
const percentage = progress / 100
// 获取百分比对应的EpubCFI
const location = percentage > 0 ? this.locations.cfiFromPercentage(percentage) : 0
// 跳转到百分比对应的电子书位置
this.rendition.display(location)
复制代码

翻页时,咱们还能够反过来,获取当前所在位置的百分比,首先经过Rendition.currentLocation()获取当前的位置信息,经过currentLocation.start.cfi提取本页开始位置的EpubCFI,将这个值传入Locations.percentageFromCfi()方法,获取当前页的百分比

this.rendition.next().then(() => {
  const currentLocation = this.rendition.currentLocation()
  const progress = this.locations.percentageFromCfi(currentLocation.start.cfi)
})
复制代码

电子书目录

epubjs为咱们提供了Navigation对象管理电子书目录,获取方法以下:

this.book.loaded.navigation.then(nav => {
  this.navigation = nav
})
复制代码

Navigation的数据结构以下:

Navigation的数据结构
Navigation.toc表示电子书的目录结构,toc中的每个元素对应一个目录,toc.href表示目录的路径,将这个值传入 Rendition.display()便可完成目录的渲染

// 跳转到第一章
this.rendition.display(this.navigation.toc[0].href)
// 跳转到第二章
this.rendition.display(this.navigation.toc[1].href)
复制代码

接下来要解决目录的展现问题,Navigation.toc是一个嵌套的数组结构,他的数据结构以下:

目录的数据结构
toc包含一个subitems属性,subitems也是一个数组,结构与toc相同,举一个例子:

const toc = [
  {
    id: '1',
    subitems: [
      {
        id: '2',
        subitems: [
	      {
	        id: '3',
	        subitems: []
	      }
        ]
      },
      {
        id: '4',
        subitems: []
      }
    ]
  },
  {
    id: '5',
    subitems: []
  }  
]
复制代码

该目录表示的含义为:最外层是id为1和5的目录,id为1的目录下包含id为2和4的目录,id为2的目录下包含id为3的目录,而最终呈现的效果应该为:

目录1
  目录2
    目录3
  目录4
目录5
复制代码

一级目录不缩进,二级目录缩进两格,三级目录缩进四格,以此类推。若是咱们直接采用toc的原始结构进行解析,不只实现过程复杂(须要实现多层嵌套循环),并且执行的性能也会降低(多层循环下降性能),同时代码不利于阅读也不利于后期维护

<div v-for="toc in navigation">
  <div v-for="subitems in toc">
    <div v-for="subitems2 in subitems">
    </div>
  </div>
</div>
复制代码

咱们能够转换一下思路,若是提供一个一维数组,里面包含一个层级属性,那么实现的难度将大大下降,转化后的一维目录的数据结构应该以下:

toc = [
  { id: '1', level: '0' },
  { id: '2', level: '1' },
  { id: '3', level: '2' },
  { id: '4', level: '1' },
  { id: '5', level: '0' }  
]
复制代码

这样问题就转变为如何将嵌套的数组结构转变为一维数组,es6提供了扩展运算符...,能够很是有效地解决这个问题,先实现一个最简单的场景,定义以下数组:

const a = [
  { id:1,
    subitems: [
      { id:2, subitems:[] },
      { id:3, subitems:[] }
    ]
  }
]
// 生成新数组的一维数组
// [{id:1}, {id:2}, {id:3}]
console.log([a[0], ...a[0].subitems])
复制代码

经过以上方法咱们能够实现将一个树状的对象转变为一个一维数组,接下来咱们要对上面示例中的toc数组进行遍历

toc.map(item => [item, ...item.subitems])
复制代码

此时获得的结果为一个二维数组的数组:

[
  [ { id: '1' }, { id: '2' }, { id: '4' } ],
  [ { id: '5' } ]
]
复制代码

能够先用扩展运算符...把数组展开,而后一个空数组把他们链接起来

[].concat(...toc.map(item => [item, ...item.subitems]))
复制代码

此时就能够获得一个一维数组了

[
  { id: '1' }, { id: '2' }, { id: '4' }, { id: '5' }
]
复制代码

这样的作法针对二级目录的结构是没问题的,可是会发现三级目录没有展开,针对三级目录须要这样实现:

[].concat(...toc.map(item => 
  [
    item, 
    ...[].concat(...item.subitems.map(sub => 
      [sub, ...sub.subitems]
    ))
  ]
))
复制代码

输出结果为:

[
  { id: '1' }, { id: '2' }, { id: '3' },{ id: '4' }, { id: '5' }
]
复制代码

这里明显地进行了迭代调用,因此能够采用迭代算法进行优化

function flatten(arr) {
  return [].concat(...arr.map(v => [v, ...flatten(v.subitems)]))
}
复制代码

接下来须要判断目录的层次,Navigation.toc中提供了parent字段用于判断父级的id,若是parent字段为null,则为顶级目录,加入parent后的示例数据以下:

const toc = [
  { id: '1', 'parent': null }, 
  { id: '2', 'parent': '1' }, 
  { id: '3', 'parent': '2' },
  { id: '4', 'parent': '1' }, 
  { id: '5', 'parent': null }
]
复制代码

判断层级的算法比较简单,咱们须要应用迭代算法,判断上层目录是否为null,若是上层目录为null,则迭代终止,若是不为null,则一直追溯,在追溯的过程当中记录层级的变化,每判断一次,层级加1,具体算法实现以下:

// 查找某一个目录的层级
function find(item, v = 0) {
  const parent = toc.filter(it => it.id === item.parent)[0]
  return !item.parent ? v : (parent ? find(parent, ++v) : v)
}
// 调用
toc.forEach(item => {
  item.level = find(item)
})
复制代码

运算后,toc的结果以下:

[
  { id: '1', 'parent': null, level: 0 }, 
  { id: '2', 'parent': '1', level: 1 }, 
  { id: '3', 'parent': '2', level: 2 },
  { id: '4', 'parent': '1', level: 1 }, 
  { id: '5', 'parent': null, level: 0 }
]
复制代码

经过以上两步,实现多级目录布局就很是容易实现了

<div v-for="(item, index) in flatten(navigation)" :key="index" :style="{marginLeft: (item.level * 10) + 'px'}" @click="rendition.display(item.href)">
   <span>{{item.label}}</span>
 </div>
复制代码

扩展

经过以上内容咱们应用Vue.js+epubjs快速实现了一个简单的阅读器,它能够知足最基本的阅读需求,可是用户需求在不断变化和增加,它要求Web阅读器可以支持更强大的功能,如:

  • 将阅读设置进行离线存储,没必要每次访问阅读器再从新设置一遍
  • 针对不一样电子书提供不一样的配置方案,每一个电子书的配置能够不一样
  • 提供字体设置功能,可以支持从互联网上获取新奇的Web字体
  • 支持电子书的离线存储,在无网络环境下也可使用,实现免流量阅读
  • 记录阅读的总时间
  • 支持上一章和下一章的快速切换
  • 支持阅读书签功能
  • 支持全文搜索功能
  • 实现更强大的主题切换功能,实现整个阅读器场景的切换,可以支持快速自定义场景开发
  • 实现手势翻页操做

以上功能在App阅读器中比较广泛,可是在Web阅读器中却并很少见,要实现这些功能须要对Vue.js和epubjs有深刻地理解和应用,近期我在慕课网推出的实战课程中详细讲解了这些知识点的实现方法,感兴趣的同窗能够点击这里进行了解。课程以微信读书做为蓝本,高度还原了App的功能和交互水准,不只介绍了阅读器的实现,还详细讲解了一个成熟阅读产品必须包含的:书城和书架功能。想直接体验产品的同窗能够点击这里,同时支持PC端和移动端哦。

相关文章
相关标签/搜索