{"id":13536485,"url":"https://github.com/GitaiQAQ/HyaReader","last_synced_at":"2025-04-02T03:30:48.977Z","repository":{"id":72925893,"uuid":"80115150","full_name":"GitaiQAQ/HyaReader","owner":"GitaiQAQ","description":"A mobile-friendly reader powered by Vue 2.","archived":false,"fork":false,"pushed_at":"2017-02-10T11:03:13.000Z","size":1018,"stargazers_count":72,"open_issues_count":0,"forks_count":14,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-11-03T01:33:15.502Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://gitaiqaq.github.io/HyaReader/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GitaiQAQ.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-01-26T13:06:03.000Z","updated_at":"2024-01-05T09:00:35.000Z","dependencies_parsed_at":"2023-03-15T11:45:31.605Z","dependency_job_id":null,"html_url":"https://github.com/GitaiQAQ/HyaReader","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GitaiQAQ%2FHyaReader","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GitaiQAQ%2FHyaReader/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GitaiQAQ%2FHyaReader/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GitaiQAQ%2FHyaReader/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GitaiQAQ","download_url":"https://codeload.github.com/GitaiQAQ/HyaReader/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246750874,"owners_count":20827799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T09:00:40.332Z","updated_at":"2025-04-02T03:30:48.625Z","avatar_url":"https://github.com/GitaiQAQ.png","language":"JavaScript","funding_links":[],"categories":["Demo示例"],"sub_categories":[],"readme":"# Hyacinth\n\n\u003e A mobile-friendly reader powered by Vue 2.\n\n---\n\n![](http://wx1.sinaimg.cn/mw690/690c6f7cgy1fci57vzkmkg20dc07iu0y.gif)\n\n## TODO\n\n- [x] 分页器 (参照豆瓣阅读实现)\n- [x] 响应式\n- [x] 阅读模式 (豆瓣)\n- [x] 动态加载\n    - [x] 水平：豆瓣\n    - [x] 垂直：微博/Twitter\n- [x] 源文件格式\n    - [x] Plain Text\n    - [x] Html\n    - [x] Markdown\n- [ ] 章节目录\n- [ ] 全文检索\n- [ ] 字体样式\n- [ ] HTML5 History Mode\n- [ ] Progressive Web Apps\n- [ ] 漢字標準格式\n\n## Demo\n\n* [Brave-New-World.txt](https://gitaiqaq.github.io/HyaReader/?url=https://raw.githubusercontent.com/GitaiQAQ/HyaReader/master/static/Brave-New-World.txt)\n* [楚辞.txt](https://gitaiqaq.github.io/HyaReader/?url=https://raw.githubusercontent.com/GitaiQAQ/HyaReader/master/static/楚辞.txt)\n* [枕草子.txt](https://gitaiqaq.github.io/HyaReader/?url=https://raw.githubusercontent.com/GitaiQAQ/HyaReader/master/static/枕草子.txt)\n\n## Build Setup\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at localhost:8080\nnpm run dev\n\n# build for production with minification\nnpm run build\n\n# run unit tests\nnpm run unit\n\n# run e2e tests\nnpm run e2e\n\n# run all tests\nnpm test\n```\n\n## Core code\n\n\u003e 整体代码非常垃圾，远不如参照，官方的 [Shopping Cart](https://github.com/vuejs/vuex/tree/86095364c3ae8d7b76c5f11ac87014c87210616c/examples/shopping-cart) 之类的例子，遂继续重构。\n\u003e 在此把可能有用的代码记录下来\n\n目标：\n\n* 减少\n    * DOM 操作\n    * 外部库引入\n    * 代码量\n* 封装\n    * 低耦合\n    * 可复用\n\n### 实现流程\n\n1. 单页阅读器\n    1. 解析 Markdown\n    2. 实现分页\n2. 水平切页\n    1. 动态插入\n    2. 替换 Vue transition 方法，减少自定义 Dom 操作\n3. 垂直模式\n    1. 去除 Header/Footer 连接 Body 部分\n    2. 动态插入\n\n#### 初始化布局参数\n\n* FontSize(px): 16\n* LineHeight(em): 1.5\n* Full(em):\n    * Height: `document.documentElement.clientHeight`\n    * Width: `document.documentElement.clientWidth`\n* Layout(em):\n    * Height: `$Height / $FontSize`\n    * Width: `$Width / $FontSize`\n\n#### 计算区块\n##### 插入 Dom\n\n浏览器自行适配，拆分段落，或者采用 JS 实现解析器（如：Flipboard/react-canvas）\n\n```js\nthis.page.map(v =\u003e v.html).join('\\n');\n```\n\n### 枚举节点＆拆分页面\n\n* Page:\n    * Title\n    * Paragraphs\n        * Offset\n        * Html\n\n```js\nconst lineHeight = this.fontSize * this.lineHeight;\nconst contentHeight = this.contentHeight / this.lineHeight;\nlet pagination = 0;\nlet pageHeight = 0;\nconst $this = this;\n\nfunction isPlaceholder(oElTreeel) {\n    if (typeof oElTreeel === 'string') return 0;\n    // let numOfImg = 0;\n    const elTree = JSON.parse(JSON.stringify(oElTreeel));\n    const tag = elTree.shift();\n    if (tag === 'img') return 2;// numOfImg += 1;\n    else if (tag === 'h1') {\n        $this.toc.push({\n        pagination,\n        title: elTree.shift(),\n        });\n        return 1;\n    }\n    if (elTree.length \u0026\u0026 typeof elTree[0] === 'object' \u0026\u0026 !(elTree[0] instanceof Array)) elTree.shift();\n    while (elTree.length) {\n        // numOfImg += isPlaceholder(elTree.shift());\n        const rt = isPlaceholder(elTree.shift());\n        if (rt) return rt;\n    }\n    // return numOfImg;\n    return false;\n}\n\nfunction nextPage(height) {\n    pageHeight = height;\n    pagination += 1;\n    if (!$this.pages[pagination]) $this.pages[pagination] = [];\n}\n\n[].forEach.call($this.$el.children, (v, i) =\u003e {\n    const paragraph = {\n        height: Math.ceil(v.offsetHeight / lineHeight),\n        offset: (lineHeight - (v.offsetHeight % lineHeight)) % lineHeight,\n        html: $this.book.tree[i].html,\n        tree: $this.book.tree[i].el,\n    };\n    // Pagination\n    switch (isPlaceholder(paragraph.tree)) {\n        case 1: {\n        nextPage(paragraph.height);\n        $this.toc.push({\n            title: paragraph.tree[1],\n            pagination,\n        });\n        $this.pages[pagination].push(paragraph);\n        return;\n        }\n        case 2: {\n        nextPage(0);\n        $this.pages[pagination].push(paragraph);\n        nextPage(0);\n        return;\n        }\n        default:\n        break;\n    }\n    pageHeight += paragraph.height;\n    $this.pages[pagination].push(paragraph);\n    while (pageHeight \u003e= contentHeight) {\n        nextPage(pageHeight - contentHeight);\n        const oParagraph = JSON.parse(JSON.stringify(paragraph));\n        oParagraph.offset = -(paragraph.height - pageHeight);\n        $this.pages[pagination].push(oParagraph);\n    }\n});\nthis.$store.dispatch('UPDATE_BOOK_TOC', { toc: this.toc.reverse() });\nthis.$store.dispatch('UPDATE_BOOK_PAGES', { pages: this.pages });\n```\n\n计算 `el[i].offsetHeight` 之和，构造页数组，以 `$Layout.Height` 为参数分离\n\n\u003e `el[i].offsetHeight` 向上取 `$FontSize * $LineHeight` 倍数\n\n#### 特殊占位符（Placeholder）\n\n* H1(章节分离)\n* Img(惰性加载，难以计算，独立成页)\n\n### 切换节点\n\n父元素监听 `Page` 对象变化，删除渲染节点，并写入需要显示的页面数据\n\n```js\nisLoading: state =\u003e !state.book.pages.length,\n```\n\n```jade\npage.page-reader(v-if=\"!$store.getters.isLoading \u0026\u0026 vertical\", v-for=\"(page, key) in slicedPage\", :key=\"sliced[0] + key\")\n    PageReader(slot=\"bd\", :index=\"sliced[0] + key\")\ntransition(v-if=\"!$store.getters.isLoading \u0026\u0026 !vertical\", :name=\"transition\")\n    page.page-reader(v-if=\"currPos\", :key=\"currPos\")\n        h3(slot=\"hd\") {{ title }}\n        PageReader(slot=\"bd\", :index=\"currPos - 1\")\n        span(slot=\"ft\") {{ currPos }}\n            span / {{ pages.length }} \n```\n\n### 滚动加载\n\n```jade\npage.page-reader(v-if=\"!$store.getters.isLoading \u0026\u0026 vertical\", v-for=\"(page, key) in slicedPage\", :key=\"sliced[0] + key\")\n    PageReader(slot=\"bd\", :index=\"sliced[0] + key\")\ntransition(v-if=\"!$store.getters.isLoading \u0026\u0026 !vertical\", :name=\"transition\")\n    page.page-reader(v-if=\"currPos\", :key=\"currPos\")\n        h3(slot=\"hd\") {{ title }}\n        PageReader(slot=\"bd\", :index=\"currPos - 1\")\n        span(slot=\"ft\") {{ currPos }}\n            span / {{ pages.length }} \n```\n\n在 `mounted` 挂载滚动监听\n\n```js\nmounted() {\n    global.window.addEventListener('scroll', this.handleScroll);\n},\n```\n\n`turn` 改变动画参数和游标\n\n```js\nhandleScroll() {\n    const scrollY = Math.floor((global.window.scrollY / this.fontSize) / this.contentHeight);\n    this.turn(scrollY - this.currPos);\n},\nturn(num) {\n    this.transition = (num \u003e 0) ? 'slide-left' : 'slide-right';\n    if ((this.currPos + num) \u003e= 0 \u0026\u0026 (this.currPos + num) \u003c this.pages.length) {\n        this.currPos += num;\n    }\n},\n```\n\n游标变化后，`sliced` 重新计算，`slicedPage` 随之计算，写入 DOM\n\n```js\nsliced() {\n    const start = this.currPos \u003e= 3 ? (this.currPos - 3) : 0;\n    let end = this.pages.length - 1;\n    end = (this.currPos + 3) \u003c end ? (this.currPos + 3) : end;\n    return [\n    start, end, end - start,\n    ];\n},\nslicedPage() {\n    return this.pages.slice(this.sliced[0], this.sliced[1] + 1);\n},\n```\n\n父元素改变 `padding-top` \u0026 `paddingbottom` 来填充滚动条\n\n```js\ninnerStyle() {\n    if (!this.vertical) {\n    return {\n        width: `${this.pageWidth}em`,\n    };\n    }\n    return {\n        width: `${this.pageWidth}em`,\n        paddingTop: `${this.contentHeight * this.sliced[0]}em`,\n        paddingBottom: `${this.contentHeight * ((this.pages.length - 1) - (this.sliced[0] + this.sliced[2]))}em`,\n    };\n},\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FGitaiQAQ%2FHyaReader","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FGitaiQAQ%2FHyaReader","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FGitaiQAQ%2FHyaReader/lists"}