如何開發高質量的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端和移動端哦。

相關文章
相關標籤/搜索