Author: Sam

preface

With the popularity of smart phones, mobile reading become the choice of more and more people to get knowledge, according to the statistics of mobile reading monthly active users has exceeded 320 million, the size of the market in 2017 to 16.6 billion, continue to maintain double-digit growth, by contrast, the mobile reading market was only 3.27 billion, 2012, six years growth more than 5 times, amazing.

Because of this, mobile reading market giants gather, all kinds of reading products emerge in endlessly. Most of the high-quality reading products are App versions. Although there are many Web reading products, the experience is far from App due to the limitations of the previous front-end technology. With the rapid development of the front-end MVVM framework in recent years, the Web reading products have been equipped with the basis for comprehensive upgrades and iterations.

What is a quality Web reading product

Click here to experience a high-performance Web reader

High quality Web reading product should have at least three modules, bookshelf and reader, reader user access to the site, through the classification search + + recommended the way to find yourself interested in ebooks, after check the details, adding e-books shelf, open the reader in the bookshelf, read e-books to read the content, function structure is as follows:

Quick start Web reader development

Two months ago, I published the free course “Quick Start Web Reader Development” on MOOCs (course address: click here, source address: click here, experience address: click here), trying to develop a high quality reader with vue.js +Webpack. The fact proves that vue.js can bring breakthrough improvement to mobile reading. The following is to introduce the realization principle and development process of the reader. This paper focuses on the realization process of ePub e-books. The principle of the reader is shown as follows:

Ebook analysis

The ePub ebook parsing process is complex, but FuturePress, an interdisciplinary project from the University of California, Berkeley, has helped us solve this problem. FuturePress is an ePUBJS library that addresses complex issues such as ebook parsing and rendering. The installation process is simple

npm install epubjs --save
Copy the code

With the help of EpubJS, reader development is greatly reduced, making it possible for individual developers to complete a complex reading product. The process of ebook parsing is as follows:

// Introduce the epubjs library
import Epub from 'epubjs'
// Set the global ePub object
global.ePub = Epub
// Ebooks download address, here provides a test ebook address, you can replace their interested ebook
const url = 'http://www.youbaobao.xyz/epub/History/2018_Book_TheCostOfInsanityInNineteenth-.epub'

// Parse ebooks
this.book = new Epub(url)
Copy the code

The book variable here is the resolved e-book object

Ebook rendering

The ebook rendering process is very simple, with the book.renderto () method

this.rendition = this.book.renderTo('reader', {
  width: window.innerWidth,
  height: window.innerHeight
})
Copy the code

RenderTo: the first argument is the DIV id. The reader automatically generates a DOM and mounts it to the specified div. We need to match the DIV with the ID. The second argument specifies the width and height of the reader. The rendering process calls Rendition’s display() method, which returns a Promise object so we can manipulate the rendered process

this.rendition.display().then((a)= > {
  // Define what to do after rendering is complete. })Copy the code

Page-turning is also very simple, just calling the rendition.prev () and rendition.next () methods, which also return a Promise object

/ / back
this.rendition.prev()
/ / the next page
this.rendition.next()
Copy the code

The font size setting

The size setting is an essential function of the reader. Users want to be able to change the size of the reader independently. This function needs to be realized through epubJS Themes object, which provides the fontSize() method

// Get the Themes object
this.themes = this.rendition.themes
// Set the size
this.themes.fontSize(16)
Copy the code

Theme Settings

The theme setting function allows us to change the font and background color of the reader, and then modify the style of the reader. The theme.register () method is used to register the theme, and the theme.select () method is used to switch the theme

// Register the theme, body to change the style of the body tag
this.themes.register('night', {
  body: { 'color': '#fff'.'background': '# 000' },
  img: { 'width': '100%'}})// Switch themes
this.themes.select('night')
Copy the code

Schedule set

In the process of reading, we want to quickly switch to the location we want to browse, usually using two methods: drag the progress bar to quickly locate and switch through the directory. In this section, we will introduce the way of dragging the progress bar to switch, and the progress bar can be realized by using the input range control newly added in HTML5

<input type="range"
       :value="progress"
       max="100"
       min="0"
       step="1"
       @change="onProgressChange($event.target.value)"
       @input="onProgressInput($event.target.value)">
Copy the code
  • Sets a slider control that specifies the Type property of the input tag as range
  • The binding value of range is progress, the maximum value of progress is 100, the minimum value of progress is 0, and the increment of step is 1. For example, if you move the slider 1, the increment of progress will be 1
  • $event. Target. Value gets the latest progress value
  • @INPUT binds the modification process event, which is triggered by dragging the slider

The principle of changing the reading progress is to dynamically change the location of the reader based on the value of progress. To change the reading position, you first need to do pagination, which can be achieved through epubJS Locations object

// The Book object's hook function is ready
this.book.ready.then((a)= > {
  // Perform paging
  return this.book.locations.generate()
}).then(result= > {
  // Get the Locations object
  this.locations = this.book.locations
})
Copy the code

Paging finished, we can through Locations. CfiFromPercentage percentage () method to obtain the corresponding EpubCFI, EpubCFI used to solve the location problem of the e-book, it can locate the ebook in any one character at a time, it will explain in detail in subsequent articles, Pass EpubCFI directly into the rendition.display () method to jump to the e-book location corresponding to the percentage

// Convert percentages to decimals
const percentage = progress / 100
// Get the EpubCFI corresponding to the percentage
const location = percentage > 0 ? this.locations.cfiFromPercentage(percentage) : 0
// Jump to the ebook position corresponding to the percentage
this.rendition.display(location)
Copy the code

Page, we can, in turn, to obtain the percentage of the current location, first by Rendition. CurrentLocation () to obtain the current location information, through currentLocation. Start. Cfi extract EpubCFI starting position on this page, This value into the Locations. PercentageFromCfi () method, obtain the percentage of the current page

this.rendition.next().then((a)= > {
  const currentLocation = this.rendition.currentLocation()
  const progress = this.locations.percentageFromCfi(currentLocation.start.cfi)
})
Copy the code

Ebook catalogue

Epubjs provides us with a Navigation object management ebook directory, which can be obtained as follows:

this.book.loaded.navigation.then(nav= > {
  this.navigation = nav
})
Copy the code

The data structure of Navigation is as follows:

Navigation.toc
Rendition.display()

// Skip to chapter 1
this.rendition.display(this.navigation.toc[0].href)
// Jump to chapter 2
this.rendition.display(this.navigation.toc[1].href)
Copy the code

Navigation. Toc is a nested array structure. Its data structure is as follows:

const toc = [
  {
    id: '1'.subitems: [{id: '2'.subitems: [{id: '3'.subitems: []}]}, {id: '4'.subitems: []}]}, {id: '5'.subitems: []}]Copy the code

The outermost layer is directories 1 and 5. Directory 1 contains directories 2 and 4, and directory 2 contains directory 3. The final display should be:

Directory 1 Directory 2 Directory 3 Directory 4 Directory 5Copy the code

Level-1 directories are not indent, level-2 directories are indent by two Spaces, level3 directories are indent by four Spaces, and so on. If we went straight to the raw structure of toc, not only would the implementation be complex (requiring multiple nested loops), but the performance of the execution would be degraded (multiple loops degrade performance), and the code would be difficult to read and maintain

<div v-for="toc in navigation">
  <div v-for="subitems in toc">
    <div v-for="subitems2 in subitems">
    </div>
  </div>
</div>
Copy the code

If we provide a one-dimensional array containing a hierarchical attribute, the difficulty of implementation will be greatly reduced. The converted one-dimensional directory data structure should be as follows:

toc = [
  { id: '1'.level: '0' },
  { id: '2'.level: '1' },
  { id: '3'.level: '2' },
  { id: '4'.level: '1' },
  { id: '5'.level: '0'}]Copy the code

The problem then becomes how to convert a nested array structure into a one-dimensional array, and ES6 provides extension operators… , we can solve this problem very effectively by implementing the simplest scenario, which defines the following array:

const a = [
  { id:1.subitems: [{id:2.subitems:[] },
      { id:3.subitems:[]}]}]// Generates a one-dimensional array of new arrays
// [{id:1}, {id:2}, {id:3}]
console.log([a[0], ...a[0].subitems])
Copy the code

We can convert a tree object into a one-dimensional array, and then iterate through the TOC array in the example above

toc.map(item= > [item, ...item.subitems])
Copy the code

The result is an array of two-dimensional arrays:

[[{id: '1' }, { id: '2' }, { id: '4'}], [{id: '5'}]]Copy the code

You can start with the extension operator… Expand the array, and then an empty array joins them together

[].concat(... toc.map(item= > [item, ...item.subitems]))
Copy the code

And now you have a one-dimensional array

[{id: '1' }, { id: '2' }, { id: '4' }, { id: '5'}]Copy the code

This is fine for the secondary directory structure, but you will find that the tertiary directory is not expanded.

[].concat(... toc.map(item= > 
  [
    item, 
    ...[].concat(...item.subitems.map(sub= > 
      [sub, ...sub.subitems]
    ))
  ]
))
Copy the code

The output is:

[{id: '1' }, { id: '2' }, { id: '3'}, {id: '4' }, { id: '5'}]Copy the code

Iterative calls are obviously made here, so an iterative algorithm can be used for optimization

function flatten(arr) {
  return[].concat(... arr.map(v= > [v, ...flatten(v.subitems)]))
}
Copy the code

Toc provides the parent field to determine the parent id. If the parent field is null, it is the top level directory. Example data after adding parent:

const toc = [
  { id: '1'.'parent': null }, 
  { id: '2'.'parent': '1' }, 
  { id: '3'.'parent': '2' },
  { id: '4'.'parent': '1' }, 
  { id: '5'.'parent': null}]Copy the code

If the upper level directory is NULL, the iteration is terminated; if it is not null, the iteration is traced all the time, and the changes of the hierarchy are recorded during the tracing process. Each time the judgment is made, the hierarchy is increased by 1. The specific algorithm is implemented as follows:

// Find the hierarchy of a directory
function find(item, v = 0) {
  const parent = toc.filter(it= > it.id === item.parent)[0]
  return! item.parent ? v : (parent ? find(parent, ++v) : v) }/ / call
toc.forEach(item= > {
  item.level = find(item)
})
Copy the code

After the calculation, the TOC results as follows:

[{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}]Copy the code

With these two steps, multi-level directory layout is easy to implement

<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>
Copy the code

extension

Through the above content, we applied vue. js+epubjs to quickly realize a simple reader, which can meet the most basic reading needs, but the user needs are constantly changing and growing, it requires Web reader to support more powerful functions, such as:

  • Store the reading Settings offline without having to reset them every time you visit the reader
  • Different configuration schemes are provided for different ebooks. The configuration of each ebook can be different
  • Provides font Settings to support the acquisition of novel Web fonts from the Internet
  • Offline storage of e-books is supported, which can be used even in non-network environment to achieve data free reading
  • Keep track of your total reading time
  • Supports quick switching between previous and next chapters
  • Supports reading bookmarks
  • Supports full-text search
  • Realize more powerful theme switching function, realize the switch of the whole reader scene, can support fast custom scene development
  • Realize gesture page turning operation

The above functions are common in App readers, but not common in Web readers. To realize these functions, you need to have a deep understanding and application of vue. js and epubjs. I have explained the implementation methods of these knowledge points in detail in the practical course launched by MOOCs recently. Based on wechat reading, the course highly restored the functions and interaction level of App. It not only introduced the realization of reader, but also explained in detail the functions of book city and bookshelf that must be included in a mature reading product. If you want to experience the product directly, you can click here. Both PC and mobile devices are supported.