Skip to content

GitaiQAQ/HyaReader

master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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`,
    };
},

About

A mobile-friendly reader powered by Vue 2.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published