Vue web 读书器是一款以Vue 全家桶为主要的技术栈,媲美原生 App 的读书器。主要技术栈包括Vue 全家桶、Vue-cli3.0、交互动画、LocalStorage+IndexedDB等
已安装好 Vue-cli3.0 后直接创建项目
vue create my-project
首先先去Iconfont选好矢量图标并下载,再去Icomoon将字体图标设置为 svg 格式并下载到本地。
具体步骤为: New Empty Set
--> Import to Set
-->选择已下载的图标
-->Generate Font
--> Download
--> 解压后将style.css
和fonts
复制至assets/styles
最后在main.js
中引入图标
// 引入字体图标
import './assets/styles/icon.css'
将提前准备好的字体引入src/assets/fonts
例如:
// 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
中引入<link rel="stylesheet" href="<%= BASE_URL %>fonts/daysOne.css">
- 在
src/main.js
中引入使用时则需注明import './assets/fonts/daysOne.css'
font-family
.text {
font-family: "Days One";
color: orange;
}
这里的font-family
需和引入字体的font-family
一致
rem
是指根元素(root element,html)的字体大小,在移动端中需要根据屏幕大小自动适应根源素大小从而响应其他fong-size
在src/App.vue
页面下进行rem
设置
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";
});
ePub
是一种电子书的标准格式,该项目主要使用 Vue
和 epub.js
制作电子书阅读器
安装epubjs
npm i --save epubjs
引入并使用epubjs
import Epub from "epubjs";
global.Epub = Epub;
安装Nginx作为代理服务器,具体配置以及跨域问题询问度娘
在router.js
下重定向到/ebook
,并在views/ebook
下创建index.vue
作为根页面,在components/ebook/
创建EbookReader.vue
二级路由
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
进行处理
vuex
是 vue
用来集中管理状态的容器,用来管理全局的状态的,实现不同组件之间相互的数据访问。如果一个项目非常大的话状态就会非常的多,如果不进行分类处理,所有的状态都维护在一个state
里面的话,状态管理就会变得非常的混乱,这样非常不利于项目的后期维护。这里需要使用到 vuex模块化管理
。
在store/modules/book.js
模块下
-
states
保存应用的状态值state: { fileName: '' }
-
mutations
定义对状态值的操作 更改Vuex
的store
中的状态的唯一方法是提交 mutation
。Vuex
中的mutation
非常类似于事件:每个mutation
都有一个字符串的事件类型 (type)
和 一个回调函数 (handler)
。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state
作为第一个参数。且这里要注意不要在mutations
里面进行异步操作。mutations: { 'SET_FILENAME': (state, fileName) => { state.fileName = fileName } }
-
actions
将mutations
中定义的方法进行了一次封装actions
定义的方法只是将mutations
中定义的方法进行了一次封装,就是去触发mutations
中的方法。如果传递的参数需要异步获取的话,可以在这里等待异步返回成功,然后在触发mutations
中的方法。actions
通过调用store.commit
提交载荷(fileName
这个对象)到名为SET_FILENAME
的mutation
中actions: { setFileName: ({commit}, fileName) => { return commit('SET_FILENAME', fileName) // 需要return才能返回一个promise对象 } }
且在
components/ebook/EbookReader.vue
中,actions
通过调用store.dispatch
方法提交载荷(fileName
这个对象)触发actions
中的setFileName
this.$store.dispatch("setFileName", fileName)
在store/
模块下
-
getters
将states
中定义的值暴露在store.getters对象
中const book = { fileName: state => state.book.fileName } export default book
这里将
getters
提取出来作为多个模块之间共享的方法,故提取出来。 在components/ebook/EbookReader.vue
中,mapGetters 辅助函数
仅仅是将 store 中的getter
映射到局部计算属性,且使用对象展开运算符将getter
混入computed
对象中import { mapGetters } from "vuex"; computed: { ...mapGetters(["fileName"]) }
-
index.js
入口文件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'
混入 mixins
是一种分发 Vue
组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
全局注册一个混入,影响注册之后所有创建的每个 Vue
实例
mapActions
组件中使用this.$store.dispatch('xxx')
分发action
,或者使用 mapActions
辅助函数将组件的methods
映射为 store.dispatch
-
在
src/store/actions.js
中定义actions
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
const book = { fileName: state => state.book.fileName, menuVisible: state => state.book.menuVisible } export default book
-
在
src/utils/mixin.js
中创建mixin.js
import { mapGetters, mapActions } from 'vuex' export const ebookMixin = { computed:{ ...mapGetters([ "fileName", "menuVisible", // 这里是 getters 暴露 state中的对象 ]) }, methods: { ...mapActions([ "setMenuVisible", "setFileName", // 这里是定义 actions.js中分发 mutations 中的方法 ]) } }
最后在组件中引入mixins
并使用mixins
import { ebookMixin } from "../../utils/mixin";
// 使用mixins
mixins: [ebookMixin]
使用mixins
中定义的方法时,直接this.+函数名
,如this.setFileName()
transition
在src/components/ebook
下分别新建EbookTitle.vue
、EbookMenu.vue
作为顶部标题栏,底部菜单栏。为了使其过渡流程,需使用 transition
<transition>
元素作为单个元素/组件的过渡效果。<transition>
只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM
元素,也不会出现在检测过的组件层级中。
动态组件
<transition name="fade" mode="out-in" appear>
<component :is="view"></component>
</transition>
transition.css
.slide-down-enter, .slide-down-leave-to {
transform: translate3d(0, -100%, 0)
}
切换标题栏和菜单栏
如同电子阅读类 App 一样,通过手势来实现上/下页滑动、切换标题栏和菜单栏等。Epub
集成了手势操作类方法。
// 手势操作
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
分发
this.$store.dispatch("setMenuVisible", !this.menuVisible);
引入 mixins
后
this.setMenuVisible(!this.menuVisible);
在 HTML5 中,新加入了一个localStorage
特性,这个特性主要是用来作为本地存储
来使用的,解决了cookie
存储空间不足的问题(cookie 中每条 cookie 的存储空间为4k
),localStorage
中一般浏览器支持的是5M
大小,这个在不同的浏览器中 localStorage 会有所不同。
WebStorageCache
对HTML5 localStorage
和sessionStorage
进行了扩展,添加了超时时间,序列化方法。可以直接存储json
对象,同时可以非常简单的进行超时时间的设置。
具体使用方法可查看WebStorageCache
- 安装WebStorageCache
import Storage from "web-storage-cache"
- 在
src/utils
下创建localStorage.js
,并导入WebStorageCacheimport Storage from "web-storage-cache"; const localStorage = new Storage()
WebStorageCache API
- set 往缓存中插入数据
export function setLocalStorage(key, value) {
return localStorage.set(key, value)
}
- get 根据
key
获取缓存中未超时数据。返回相应类型String
、Boolean
、PlainObject
、Array
的值。
// 根据key获取缓存中未超时数据
export function getLocalStorage(key) {
return localStorage.get(key)
}
- delete 根据
key
删除缓存中的值
// 根据key删除缓存中的值。
export function removeLocalStorage(key) {
return localStorage.delete(key)
}
- clear 清空缓存中全部的值
// 清空缓存中全部的值
export function clearLocalStorage() {
return localStorage.clear()
}
- WebStorageCache 在组件中的具体使用
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);
}
}
vue-i18n
是一个vue
插件,主要作用就是让项目支持国际化多语言。
-
安装
vue-i18n
// 安装vue-i18n npm install vue-i18n --save
-
引入
vue-i18n
在
src/lang
下创建index.js
作为根路径,引入vue-i18n
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
-
定义语言包
const messages = { home: { title: '书城', ...... }, category: {}, ..... }
-
在
main.js
中引入lang
// 引入多语言 import i18n from './lang' new Vue({ router, store, i18n, render: h => h(App )}).$mount('#app')
上次实现了标题栏和菜单栏样式的改变,但并没有涉及到全局样式的改变。这次实阅读器全局样式的改变。
采用链接的方式在 HTML 中导入 CSS,即使用 HTML 头部的 <head>
标签引入外部的 CSS 文件
<link rel="stylesheet" type="text/css" href="style.css">
-
将外部 CSS 文件放入 Nginx 中,获取其链接
-
在
src/utils/book.js
下使用setAttribute
构建导入 CSS 文件的链接方式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) }
-
在
EbookReader.vue
组件中,当 Epub 加载好后调用initGlobalStyle
方法// 初始化字体样式、字体大小、主题、全局主题 this.rendition.display().then(() ={ this.initFontSize(); this.initFontFamily(); this.initTheme(); this.initGlobalStyle(); });
initGlobalStyle()
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; } }
-
但是,当多次点击切换样式时,header 中会逐个加载 CSS 样式,后面的覆盖前面的,影响渲染速度,故需要清楚
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) } } }
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`) }
-
由于
initGlobalStyle()
方法需要在EbookReader.vue
组件和EbookSettingTheme.vue
组件中调用,为提高代码复用性,故将initGlobalStyle()
放入src/utils/mixin.js
中。
在src/components/ebook
下新建EbookSettingProgress.vue
作为进度条组件。
进度条组件主要用来滑动进度条切换阅读进度和查看进度的,核心部分由input
完成。
<div class="progress-wrapper">
<div class="progress-icon-wrapper" @click="prevSection()">
<span class="icon-back"></span>
</div>
<input
class="progress"
type="range"
max="100"
min="0"
step="1"
@change="onProgressChange($event.target.value)"
@input="onProgressInput($event.target.value)"
:value="progress"
:disabled="!bookAvailable"
ref="progress">
<div class="progress-icon-wrapper" @click="nextSection()">
<span class="icon-forward"></span>
</div>
</div>
真心觉得CSS
还挺难的...
作为一个滑块组件,主要用到 input
type 和 step 属性
type | 返回滑块控件的表单元素类型 |
---|---|
step | 设置或返回滑块控件的 step 属性值 ;step 属性规定输入字段的合法数字间隔;step 属性可以与 max 以及 min 属性配合使用,以创建合法值的范围 |
再调用函数监听 input
数据变化:
@change="onProgressChange($event.target.value)"
@input="onProgressInput($event.target.value)"
Vue 官网上对 ref 详解:ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM元素;如果用在子组件上,引用就指向组件实例:
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>
<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
关于 ref
注册时间的重要说明:因为 ref
本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs
也不是响应式的,因此你不应该试图用它在模板中做数据绑定。
ref
有三种用法:
ref
放在普通的元素上,用this.ref.name
获取的是 DOM 元素ref
放在子组件上,用this.ref.name
获取的是组件实例,可以使用组件的所有方法
- 利用v-for 和 ref 获取一组数组或者DOM 节点