Home

Awesome

Hyacinth

A mobile-friendly reader powered by Vue 2.


TODO

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 之类的例子,遂继续重构。 在此把可能有用的代码记录下来

目标:

实现流程

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

初始化布局参数

计算区块

插入 Dom

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

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

枚举节点&拆分页面

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)

切换节点

父元素监听 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`,
    };
},