xsqz / vue-ebook

实战微信读书Vue-cli 3.2.1 + Vue2.5 + Vue全家桶 + epubjs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Vue web 读书器

Vue web 读书器是一款以Vue 全家桶为主要的技术栈,媲美原生 App 的读书器。主要技术栈包括Vue 全家桶、Vue-cli3.0、交互动画、LocalStorage+IndexedDB

1、初始化项目

1.1、使用 Vue-cli3.0 搭建开发环境

已安装好 Vue-cli3.0 后直接创建项目

 vue create my-project

1.2、导入 Iconfont svg 图标

首先先去Iconfont选好矢量图标并下载,再去Icomoon将字体图标设置为 svg 格式并下载到本地。 具体步骤为: New Empty Set --> Import to Set -->选择已下载的图标 -->Generate Font --> Download--> 解压后将style.cssfonts 复制至assets/styles

最后在main.js中引入图标

// 引入字体图标
import './assets/styles/icon.css'

1.3、引入字体

将提前准备好的字体引入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一致

1.4、设置响应式 rem

rem 是指根元素(root element,html)的字体大小,在移动端中需要根据屏幕大小自动适应根源素大小从而响应其他fong-sizesrc/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";
});

1.5、安装 epub

ePub 是一种电子书的标准格式,该项目主要使用 Vueepub.js 制作电子书阅读器 安装epubjs

npm i --save epubjs

引入并使用epubjs

import Epub from "epubjs";
global.Epub = Epub;

1.6、安装 Nginx 作为代理服务器

安装Nginx作为代理服务器,具体配置以及跨域问题询问度娘

2、电子书解析与渲染

2.1、动态路由

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、joinfileName进行处理

2.2、vuex 模块化管理

vuexvue 用来集中管理状态的容器,用来管理全局的状态的,实现不同组件之间相互的数据访问。如果一个项目非常大的话状态就会非常的多,如果不进行分类处理,所有的状态都维护在一个state里面的话,状态管理就会变得非常的混乱,这样非常不利于项目的后期维护。这里需要使用到 vuex模块化管理

store/modules/book.js 模块下

  • states 保存应用的状态值

    state: {
        fileName: ''
    }
  • mutations 定义对状态值的操作 更改Vuexstore 中的状态的唯一方法是提交 mutationVuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的  事件类型 (type)  和 一个  回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。且这里要注意不要在 mutations 里面进行异步操作。

    mutations: {
        'SET_FILENAME': (state, fileName) => {
             state.fileName = fileName
        }
    }
  • actionsmutations 中定义的方法进行了一次封装

    actions 定义的方法只是将 mutations 中定义的方法进行了一次封装,就是去触发 mutations 中的方法。如果传递的参数需要异步获取的话,可以在这里等待异步返回成功,然后在触发 mutations 中的方法。

    actions 通过调用 store.commit 提交载荷(fileName这个对象)到名为SET_FILENAMEmutation

    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/ 模块下

  • gettersstates 中定义的值暴露在 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'

2.3、mixins 混入

混入 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()

3、阅读器开发

3.1、标题栏和菜单栏实现

transition

src/components/ebook下分别新建EbookTitle.vueEbookMenu.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);

3.2、WebStorageCache

在 HTML5 中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie 中每条 cookie 的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中 localStorage 会有所不同。

WebStorageCache  对HTML5 localStoragesessionStorage  进行了扩展,添加了超时时间,序列化方法。可以直接存储json对象,同时可以非常简单的进行超时时间的设置。

具体使用方法可查看WebStorageCache

  • 安装WebStorageCache
    import Storage from "web-storage-cache"
  • src/utils 下创建localStorage.js,并导入WebStorageCache
    import Storage from "web-storage-cache";
    const localStorage = new Storage()

WebStorageCache API

  • set 往缓存中插入数据
export function setLocalStorage(key, value) {
    return localStorage.set(key, value)
 }
  • get 根据key获取缓存中未超时数据。返回相应类型StringBooleanPlainObjectArray的值。
// 根据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);
    }
 }

3.3、国际化——Vue-i18n 的使用

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')

3.4、全局样式的改变

上次实现了标题栏和菜单栏样式的改变,但并没有涉及到全局样式的改变。这次实阅读器全局样式的改变。 采用链接的方式在 HTML 中导入 CSS,即使用 HTML 头部的  <head>标签引入外部的 CSS 文件

<link rel="stylesheet" type="text/css" href="style.css">
  1. 将外部 CSS 文件放入 Nginx 中,获取其链接

  2. 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)
    }
  3. 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;
         }
     }
  4. 但是,当多次点击切换样式时,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`)
     }
  5. 由于 initGlobalStyle() 方法需要在EbookReader.vue组件和EbookSettingTheme.vue 组件中调用,为提高代码复用性,故将 initGlobalStyle() 放入src/utils/mixin.js中。

3.5、进度条组件

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 属性

作为一个滑块组件,主要用到 input typestep 属性

type 返回滑块控件的表单元素类型
step 设置或返回滑块控件的 step 属性值 ;step 属性规定输入字段的合法数字间隔;step 属性可以与 max 以及 min 属性配合使用,以创建合法值的范围

再调用函数监听 input 数据变化:

 @change="onProgressChange($event.target.value)"
 @input="onProgressInput($event.target.value)"
ref

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 有三种用法:

  1. ref 放在普通的元素上,用 this.ref.name 获取的是 DOM 元素
  2. ref 放在子组件上,用 this.ref.name 获取的是 组件实例,可以使用组件的所有方法
  3. 利用v-forref 获取一组数组或者DOM 节点

About

实战微信读书Vue-cli 3.2.1 + Vue2.5 + Vue全家桶 + epubjs


Languages

Language:Vue 48.3%Language:JavaScript 32.0%Language:CSS 19.1%Language:HTML 0.7%