Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/weianke/vue-ebook
实战微信读书Vue-cli 3.2.1 + Vue2.5 + Vue全家桶 + epubjs
https://github.com/weianke/vue-ebook
axios epub vue-cli vue-router vuejs2 vuex webpack4
Last synced: 2 months ago
JSON representation
实战微信读书Vue-cli 3.2.1 + Vue2.5 + Vue全家桶 + epubjs
- Host: GitHub
- URL: https://github.com/weianke/vue-ebook
- Owner: weianke
- Created: 2019-03-08T04:39:15.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2022-12-09T15:04:32.000Z (about 2 years ago)
- Last Synced: 2023-03-03T18:23:45.106Z (almost 2 years ago)
- Topics: axios, epub, vue-cli, vue-router, vuejs2, vuex, webpack4
- Language: Vue
- Homepage:
- Size: 8.22 MB
- Stars: 27
- Watchers: 3
- Forks: 7
- Open Issues: 22
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
### Vue web 读书器
Vue web 读书器是一款以**Vue 全家桶**为主要的技术栈,媲美原生 App 的读书器。主要技术栈包括**Vue 全家桶、Vue-cli3.0、交互动画、LocalStorage+IndexedDB**等
### 1、初始化项目
#### 1.1、使用 Vue-cli3.0 搭建开发环境
已安装好 Vue-cli3.0 后直接创建项目
```python
vue create my-project
```#### 1.2、导入 Iconfont svg 图标
首先先去[Iconfont](https://www.iconfont.cn/)选好矢量图标并下载,再去[Icomoon](https://icomoon.io/app/#/select)将字体图标设置为 svg 格式并下载到本地。
具体步骤为: `New Empty Set` --> `Import to Set` -->`选择已下载的图标` -->`Generate Font` --> `Download`--> 解压后将`style.css`和`fonts` 复制至`assets/styles`最后在`main.js`中引入图标
```python
// 引入字体图标
import './assets/styles/icon.css'
```#### 1.3、引入字体
将提前准备好的字体引入`src/assets/fonts`
例如:```python
// daysOne.css@font-face {
font-family: 'Days One';
font-style: normal;
font-weight: 400;
src: url('daysOne.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
```引入字体的方式有几种:
- 在`public/index.html`中引入
```python
```
- 在`src/main.js`中引入
```python
import './assets/fonts/daysOne.css'
```
使用时则需注明`font-family````python
.text {
font-family: "Days One";
color: orange;
}
```这里的`font-family`需和引入字体的`font-family`一致
#### 1.4、设置响应式 rem
**`rem`** 是指根元素(root element,html)的字体大小,在移动端中需要根据屏幕大小自动适应根源素大小从而响应其他`fong-size`
在`src/App.vue`页面下进行`rem`设置```python
export default {};
// 设置rem
document.addEventListener("DOMContentLoaded", () => {
const html = document.querySelector("html");
let fontSize = window.innerWidth / 10;
fontSize = fontSize > 50 ? 50 : fontSize;
html.style.fontSize = fontSize + "px";
});
```#### 1.5、安装 epub
`ePub` 是一种电子书的标准格式,该项目主要使用 `Vue` 和 `epub.js` 制作电子书阅读器
安装`epubjs````python
npm i --save epubjs
```引入并使用`epubjs`
```python
import Epub from "epubjs";
global.Epub = Epub;
```#### 1.6、安装 Nginx 作为代理服务器
安装[Nginx](http://nginx.org/en/download.html)作为代理服务器,具体配置以及跨域问题询问度娘
### 2、电子书解析与渲染
#### 2.1、动态路由
在`router.js`下重定向到`/ebook`,并在`views/ebook`下创建`index.vue`作为根页面,在`components/ebook/` 创建`EbookReader.vue`二级路由
```python
path: '/ebook',
component: () => import('./views/ebook/index.vue'),
children: [
{
path: ':fileName', // 动态路由
component: () => import('./components/ebook/EbookReader.vue')
}
]
```在`EbookReader.vue`组件中利用 **`$route.params.fileName`** 来获取`fileName`,然后利用`split、join`对`fileName`进行处理
#### 2.2、vuex 模块化管理
**`vuex`** 是 **`vue`** 用来集中管理状态的容器,用来管理全局的状态的,实现不同组件之间相互的数据访问。如果一个项目非常大的话状态就会非常的多,如果不进行分类处理,所有的状态都维护在一个`state`里面的话,状态管理就会变得非常的混乱,这样非常不利于项目的后期维护。这里需要使用到 **`vuex模块化管理`**。
在`store/modules/book.js` 模块下
- **`states`** 保存应用的状态值
```python
state: {
fileName: ''
}
```
- **`mutations`** 定义对状态值的操作
更改`Vuex` 的 `store` 中的状态的唯一方法是`提交 mutation`。`Vuex` 中的 `mutation` 非常类似于事件:每个 `mutation` 都有一个字符串的 **`事件类型 (type)`** 和 一个 **`回调函数 (handler)`**。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 **`state`** 作为第一个参数。且这里要注意不要在 **`mutations`** 里面进行异步操作。
```python
mutations: {
'SET_FILENAME': (state, fileName) => {
state.fileName = fileName
}
}
```
- **`actions`** 将 **`mutations`** 中定义的方法进行了一次封装**`actions`** 定义的方法只是将 **`mutations`** 中定义的方法进行了一次**封装**,就是去触发 **`mutations`** 中的方法。如果传递的参数需要**异步获取**的话,可以在这里等待异步返回成功,然后在触发 **`mutations`** 中的方法。
**`actions`** 通过调用 **`store.commit`** 提交载荷(`fileName`这个对象)到名为`SET_FILENAME`的 `mutation` 中
```python
actions: {
setFileName: ({commit}, fileName) => {
return commit('SET_FILENAME', fileName)
// 需要return才能返回一个promise对象
}
}
```且在`components/ebook/EbookReader.vue`中,**`actions`** 通过调用 **`store.dispatch`** 方法提交载荷(`fileName`这个对象)触发 **`actions`** 中的`setFileName`
```python
this.$store.dispatch("setFileName", fileName)
```在`store/` 模块下
- **`getters`** 将 **`states`** 中定义的值暴露在 **`store.getters对象`** 中
```python
const book = {
fileName: state => state.book.fileName
}export default book
```这里将 **`getters`** 提取出来作为多个模块之间共享的方法,故提取出来。
在`components/ebook/EbookReader.vue`中, **`mapGetters 辅助函数`** 仅仅是将 store 中的 `getter`映射到局部计算属性,且使用对象展开运算符将 `getter` 混入`computed` 对象中```python
import { mapGetters } from "vuex";computed: {
...mapGetters(["fileName"])
}
```- **`index.js`** 入口文件
```python
import Vue from 'vue'
import Vuex from 'vuex'
import book from './modules/book'
import actions from './actions'
import getters from './getters'Vue.use(Vuex)
export default new Vuex.Store({
modules: {
book
},
getters,
actions
})
```最后在`main.js`里引入`store`: `import store from './store'`
#### 2.3、**`mixins`** 混入
混入 **`mixins`** 是一种分发 `Vue`组件中可复用功能的非常灵活的方式。**混入对象**可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
全局注册一个**混入**,影响注册之后所有创建的每个 `Vue` 实例
**`mapActions`**
组件中使用`this.$store.dispatch('xxx')` 分发`action`,或者使用 **`mapActions`** 辅助函数将组件的`methods` 映射为 `store.dispatch`
- 在`src/store/actions.js`中定义`actions`
```python
const actions = {
setFileName: ({ commit }, fileName) => {
return commit('SET_FILENAME', fileName)
// 需要return才能返回一个promise对象
},
setMenuVisible: ({ commit }, menuVisible) => {
return commit('SET_MENUVISIBLE', menuVisible)
}
}export default actions
```- 在`src/store/getters.js`中定义 `getters.js`
```python
const book = {
fileName: state => state.book.fileName,
menuVisible: state => state.book.menuVisible
}export default book
```- 在`src/utils/mixin.js`中创建`mixin.js`
```python
import { mapGetters, mapActions } from 'vuex'
export const ebookMixin = {
computed:{
...mapGetters([
"fileName",
"menuVisible",
// 这里是 getters 暴露 state中的对象
])
},
methods: {
...mapActions([
"setMenuVisible",
"setFileName",
// 这里是定义 actions.js中分发 mutations 中的方法
])
}
}
```最后在组件中引入`mixins`并使用`mixins`
```python
import { ebookMixin } from "../../utils/mixin";// 使用mixins
mixins: [ebookMixin]
```使用`mixins`中定义的方法时,直接`this.+函数名`,如`this.setFileName()`
### 3、阅读器开发
#### 3.1、标题栏和菜单栏实现
> transition
在`src/components/ebook`下分别新建`EbookTitle.vue`、`EbookMenu.vue`作为顶部标题栏,底部菜单栏。为了使其过渡流程,需使用 **`transition`**
`` 元素作为单个元素/组件的过渡效果。`` 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 `DOM` 元素,也不会出现在检测过的组件层级中。
`动态组件`
```python
````transition.css`
```python
.slide-down-enter, .slide-down-leave-to {
transform: translate3d(0, -100%, 0)
}
```> 切换标题栏和菜单栏
如同电子阅读类 App 一样,通过手势来实现上/下页滑动、切换标题栏和菜单栏等。`Epub`集成了手势操作类方法。
```python
// 手势操作
this.rendition.on("touchstart", event => {
this.touchStartX = event.changedTouches[0].clientX;
this.touchStartTime = event.timeStamp;
});this.rendition.on("touchend", event => {
// 滑动x轴偏移量
const offsetX = event.changedTouches[0].clientX - this.touchStartX;
// 滑动时间差
const time = event.timeStamp - this.touchStartTime;
// console.log(offsetX, time);
if (time < 500 && offsetX > 40) {
this.prevPage(); // 上一页
} else if (time < 500 && offsetX < -40) {
this.nextPage(); // 下一页
} else {
this.toggleTitleAndMenu(); // 切换标题栏和菜单栏
}
// 停止事件默认动作及传播
// event.preventDefault();
event.stopPropagation();});
```算了,get 不到`Epub`使用方法,学习了解其手势操作的思想才是正道。这个项目最大的学习收获我觉得应该是 **`Vuex`**
> **`Vuex在项目中的具体使用`**
`Vuex` 的状态存储是响应式的。当 `Vue` 组件从 `store` 中读取状态的时候,若 `store` 中的状态发生变化,那么相应的组件也会相应地得到高效更新。所以说,这就十分有利于使用函数操作`store`里的变量。
在没有使用 **`mixins`** 时,使用`$store.dispatch`分发
```python
this.$store.dispatch("setMenuVisible", !this.menuVisible);
```引入 **`mixins`** 后
```python
this.setMenuVisible(!this.menuVisible);
```#### 3.2、WebStorageCache
在 HTML5 中,新加入了一个`localStorage`特性,这个特性主要是用来作为`本地存储`来使用的,解决了`cookie`存储空间不足的问题(cookie 中每条 cookie 的存储空间为`4k`),`localStorage`中一般浏览器支持的是`5M`大小,这个在不同的浏览器中 localStorage 会有所不同。
**`WebStorageCache`** 对`HTML5 localStorage`和`sessionStorage` 进行了扩展,添加了超时时间,序列化方法。可以直接存储`json`对象,同时可以非常简单的进行超时时间的设置。
具体使用方法可查看[WebStorageCache](https://www.npmjs.com/package/web-storage-cache)
- 安装**WebStorageCache**
```python
import Storage from "web-storage-cache"
```
- 在`src/utils` 下创建`localStorage.js`,并导入**WebStorageCache**
```python
import Storage from "web-storage-cache";
const localStorage = new Storage()
```**`WebStorageCache API`**
- **set** 往缓存中插入数据
```python
export function setLocalStorage(key, value) {
return localStorage.set(key, value)
}
```- **get** 根据`key`获取缓存中未超时数据。返回相应类型`String`、`Boolean`、`PlainObject`、`Array`的值。
```python
// 根据key获取缓存中未超时数据
export function getLocalStorage(key) {
return localStorage.get(key)
}
```- **delete** 根据`key`删除缓存中的值
```python
// 根据key删除缓存中的值。
export function removeLocalStorage(key) {
return localStorage.delete(key)
}
```- **clear** 清空缓存中全部的值
```python
// 清空缓存中全部的值
export function clearLocalStorage() {
return localStorage.clear()
}
```- **WebStorageCache** 在组件中的具体使用
```python
import {saveFontFamily,getFontFamily,getFontSize,saveFontSize} from "../../utils/localStorage";直接调用localStorage中的函数就ok了
// 设置字体样式
initFontFamily() {
let font = getFontFamily(this.fileName);
if (!font) {
saveFontFamily(this.fileName, this.defaultFontFamily);
} else {
this.rendition.themes.font(font);
this.setDefaultFontFamily(font);
}
}
```#### 3.3、国际化——Vue-i18n 的使用
`vue-i18n`是一个`vue`插件,主要作用就是让项目支持国际化多语言。
- 安装 **`vue-i18n`**
```python
// 安装vue-i18n
npm install vue-i18n --save
```
- 引入 **`vue-i18n`**在`src/lang`下创建`index.js`作为根路径,引入`vue-i18n`
```python
import Vue from 'vue'
import VueI18N from 'vue-i18n'
import en from './en'
import cn from './cn'// 加载插件
Vue.use(VueI18N)const messages = {
en, // 英文语言包
cn // 中文语言包
}export default i18n
```- 定义语言包
```python
const messages = {
home: {
title: '书城',
......
},
category: {},
.....
}
```- 在 **`main.js`** 中引入 **`lang`**
```python
// 引入多语言
import i18n from './lang'new Vue({
router,
store,
i18n,
render: h => h(App
)}).$mount('#app')
```#### 3.4、全局样式的改变
上次实现了标题栏和菜单栏样式的改变,但并没有涉及到全局样式的改变。这次实阅读器全局样式的改变。
采用链接的方式在 HTML 中导入 CSS,即使用 HTML 头部的 ``标签引入外部的 CSS 文件```python
```
1. 将外部 CSS 文件放入 Nginx 中,获取其链接
2. 在`src/utils/book.js`下使用 **`setAttribute`** 构建导入 CSS 文件的链接方式```python
export function addCss(href) {
const link = document.createElement('link')
link.setAttribute('rel', 'stylesheet')
link.setAttribute('type', 'text/css')
link.setAttribute('href', href)
document.getElementsByTagName('head')[0].appendChild(link)
}
```3. 在`EbookReader.vue`组件中,当 Epub 加载好后调用 **`initGlobalStyle`** 方法
```python
// 初始化字体样式、字体大小、主题、全局主题
this.rendition.display().then(() ={
this.initFontSize();
this.initFontFamily();
this.initTheme();
this.initGlobalStyle();
});
```**`initGlobalStyle()`**
```python
initGlobalStyle() {
removeAllCss()
switch (this.defaultTheme) {
case "Default":
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`);
break;
case "Eye":
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`);
break;
case "Gold":
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`);
break;
case "Night":
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`);
break;
default:
addCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`);
break;
}
}
```4. 但是,当多次点击切换样式时,header 中会逐个加载 CSS 样式,后面的覆盖前面的,影响渲染速度,故需要清楚
```python
export function removeCss(href) {
const links = document.getElementsByTagName('link')
for (let i = links.length; i >= 0; i--) {
const link = links[i]
if (link && link.getAttribute('href') && link.getAttribute('href') === href) {
link.parentNode.removeChild(link)
}
}
}
``````python
export function removeAllCss() {
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`)
removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`)
}
```5. 由于 **`initGlobalStyle()`** 方法需要在`EbookReader.vue`组件和`EbookSettingTheme.vue` 组件中调用,为提高代码复用性,故将 **`initGlobalStyle()`** 放入`src/utils/mixin.js`中。
#### 3.5、进度条组件
在`src/components/ebook` 下新建`EbookSettingProgress.vue`作为进度条组件。
进度条组件主要用来滑动进度条切换阅读进度和查看进度的,核心部分由`input`完成。```python
```真心觉得`CSS`还挺难的...
##### input type 与 step 属性
作为一个滑块组件,主要用到 **`input`** **type** 和 **step** 属性
| type | 返回滑块控件的表单元素类型 |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
| step | 设置或返回滑块控件的 step 属性值 ;step 属性规定输入字段的合法数字间隔;step 属性可以与 max 以及 min 属性配合使用,以创建合法值的范围 |再调用函数监听 **`input`** 数据变化:
```python
@change="onProgressChange($event.target.value)"
@input="onProgressInput($event.target.value)"
```##### ref
[Vue 官网上对 ref 详解](https://cn.vuejs.org/v2/api/#ref):**`ref`** 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 **`$refs`** 对象上。如果在普通的 **DOM** 元素上使用,引用指向的就是 **DOM**元素;如果用在子组件上,引用就指向组件实例:
```python
hello
```关于 **`ref`** 注册时间的重要说明:因为 **`ref`** 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!**`$refs`** 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。
**`ref`** 有三种用法:
1. **`ref`** 放在普通的元素上,用 **`this.ref.name`** 获取的是 **DOM** 元素
2. **`ref`** 放在**子组件**上,用 **`this.ref.name`** 获取的是 **`组件实例,可以使用组件的所有方法`**
3. 利用**v-for** 和 **ref** 获取**一组数组**或者**DOM 节点**