A mobile-friendly reader powered by Vue 2.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
build deploy script Feb 10, 2017
config Auto-commit Feb 8, 2017
src add TOC Feb 10, 2017
static New demo Feb 10, 2017
test first commit Jan 26, 2017
.babelrc first commit Jan 26, 2017
.editorconfig first commit Jan 26, 2017
.eslintignore first commit Jan 26, 2017
.eslintrc.js first commit Jan 26, 2017
.gitignore first commit Jan 26, 2017
LICENSE Refactoring Feb 7, 2017
README.md add logs Feb 10, 2017
index.html Refactoring Feb 7, 2017
package.json add TOC Feb 10, 2017
test.js first commit Jan 26, 2017

README.md

Hyacinth

A mobile-friendly reader powered by Vue 2.


TODO

  • 分页器 (参照豆瓣阅读实现)
  • 响应式
  • 阅读模式 (豆瓣)
  • 动态加载
    • 水平:豆瓣
    • 垂直:微博/Twitter
  • 源文件格式
    • Plain Text
    • Html
    • Markdown
  • 章节目录
  • 全文检索
  • 字体样式
  • HTML5 History Mode
  • Progressive Web Apps
  • 漢字標準格式

Demo

Build Setup

# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# run unit tests
npm run unit

# run e2e tests
npm run e2e

# run all tests
npm test

Core code

整体代码非常垃圾,远不如参照,官方的 Shopping Cart 之类的例子,遂继续重构。 在此把可能有用的代码记录下来

目标:

  • 减少
    • DOM 操作
    • 外部库引入
    • 代码量
  • 封装
    • 低耦合
    • 可复用

实现流程

  1. 单页阅读器
    1. 解析 Markdown
    2. 实现分页
  2. 水平切页
    1. 动态插入
    2. 替换 Vue transition 方法,减少自定义 Dom 操作
  3. 垂直模式
    1. 去除 Header/Footer 连接 Body 部分
    2. 动态插入

初始化布局参数

  • FontSize(px): 16
  • LineHeight(em): 1.5
  • Full(em):
    • Height: document.documentElement.clientHeight
    • Width: document.documentElement.clientWidth
  • Layout(em):
    • Height: $Height / $FontSize
    • Width: $Width / $FontSize

计算区块

插入 Dom

浏览器自行适配,拆分段落,或者采用 JS 实现解析器(如:Flipboard/react-canvas)

this.page.map(v => v.html).join('\n');

枚举节点&拆分页面

  • Page:
    • Title
    • Paragraphs
      • Offset
      • Html
const lineHeight = this.fontSize * this.lineHeight;
const contentHeight = this.contentHeight / this.lineHeight;
let pagination = 0;
let pageHeight = 0;
const $this = this;

function isPlaceholder(oElTreeel) {
    if (typeof oElTreeel === 'string') return 0;
    // let numOfImg = 0;
    const elTree = JSON.parse(JSON.stringify(oElTreeel));
    const tag = elTree.shift();
    if (tag === 'img') return 2;// numOfImg += 1;
    else if (tag === 'h1') {
        $this.toc.push({
        pagination,
        title: elTree.shift(),
        });
        return 1;
    }
    if (elTree.length && typeof elTree[0] === 'object' && !(elTree[0] instanceof Array)) elTree.shift();
    while (elTree.length) {
        // numOfImg += isPlaceholder(elTree.shift());
        const rt = isPlaceholder(elTree.shift());
        if (rt) return rt;
    }
    // return numOfImg;
    return false;
}

function nextPage(height) {
    pageHeight = height;
    pagination += 1;
    if (!$this.pages[pagination]) $this.pages[pagination] = [];
}

[].forEach.call($this.$el.children, (v, i) => {
    const paragraph = {
        height: Math.ceil(v.offsetHeight / lineHeight),
        offset: (lineHeight - (v.offsetHeight % lineHeight)) % lineHeight,
        html: $this.book.tree[i].html,
        tree: $this.book.tree[i].el,
    };
    // Pagination
    switch (isPlaceholder(paragraph.tree)) {
        case 1: {
        nextPage(paragraph.height);
        $this.toc.push({
            title: paragraph.tree[1],
            pagination,
        });
        $this.pages[pagination].push(paragraph);
        return;
        }
        case 2: {
        nextPage(0);
        $this.pages[pagination].push(paragraph);
        nextPage(0);
        return;
        }
        default:
        break;
    }
    pageHeight += paragraph.height;
    $this.pages[pagination].push(paragraph);
    while (pageHeight >= contentHeight) {
        nextPage(pageHeight - contentHeight);
        const oParagraph = JSON.parse(JSON.stringify(paragraph));
        oParagraph.offset = -(paragraph.height - pageHeight);
        $this.pages[pagination].push(oParagraph);
    }
});
this.$store.dispatch('UPDATE_BOOK_TOC', { toc: this.toc.reverse() });
this.$store.dispatch('UPDATE_BOOK_PAGES', { pages: this.pages });

计算 el[i].offsetHeight 之和,构造页数组,以 $Layout.Height 为参数分离

el[i].offsetHeight 向上取 $FontSize * $LineHeight 倍数

特殊占位符(Placeholder)

  • H1(章节分离)
  • Img(惰性加载,难以计算,独立成页)

切换节点

父元素监听 Page 对象变化,删除渲染节点,并写入需要显示的页面数据

isLoading: state => !state.book.pages.length,
page.page-reader(v-if="!$store.getters.isLoading && vertical", v-for="(page, key) in slicedPage", :key="sliced[0] + key")
    PageReader(slot="bd", :index="sliced[0] + key")
transition(v-if="!$store.getters.isLoading && !vertical", :name="transition")
    page.page-reader(v-if="currPos", :key="currPos")
        h3(slot="hd") {{ title }}
        PageReader(slot="bd", :index="currPos - 1")
        span(slot="ft") {{ currPos }}
            span / {{ pages.length }} 

滚动加载

page.page-reader(v-if="!$store.getters.isLoading && vertical", v-for="(page, key) in slicedPage", :key="sliced[0] + key")
    PageReader(slot="bd", :index="sliced[0] + key")
transition(v-if="!$store.getters.isLoading && !vertical", :name="transition")
    page.page-reader(v-if="currPos", :key="currPos")
        h3(slot="hd") {{ title }}
        PageReader(slot="bd", :index="currPos - 1")
        span(slot="ft") {{ currPos }}
            span / {{ pages.length }} 

mounted 挂载滚动监听

mounted() {
    global.window.addEventListener('scroll', this.handleScroll);
},

turn 改变动画参数和游标

handleScroll() {
    const scrollY = Math.floor((global.window.scrollY / this.fontSize) / this.contentHeight);
    this.turn(scrollY - this.currPos);
},
turn(num) {
    this.transition = (num > 0) ? 'slide-left' : 'slide-right';
    if ((this.currPos + num) >= 0 && (this.currPos + num) < this.pages.length) {
        this.currPos += num;
    }
},

游标变化后,sliced 重新计算,slicedPage 随之计算,写入 DOM

sliced() {
    const start = this.currPos >= 3 ? (this.currPos - 3) : 0;
    let end = this.pages.length - 1;
    end = (this.currPos + 3) < end ? (this.currPos + 3) : end;
    return [
    start, end, end - start,
    ];
},
slicedPage() {
    return this.pages.slice(this.sliced[0], this.sliced[1] + 1);
},

父元素改变 padding-top & paddingbottom 来填充滚动条

innerStyle() {
    if (!this.vertical) {
    return {
        width: `${this.pageWidth}em`,
    };
    }
    return {
        width: `${this.pageWidth}em`,
        paddingTop: `${this.contentHeight * this.sliced[0]}em`,
        paddingBottom: `${this.contentHeight * ((this.pages.length - 1) - (this.sliced[0] + this.sliced[2]))}em`,
    };
},