Vue-微信读书项目笔记-demo篇[开发]

  • 开发

      • 一、epub的核心工作原理解析
        • 1.1 epub分析
        • 1.2 实现电子书的解析和渲染
      • 二、功能实现
        • 2.1 电子书翻页功能的实现
        • 2.2 标题栏和菜单栏实现
        • 2.3 标题栏和菜单栏的显隐
        • 2.4 标题栏和菜单栏的过渡动画
        • 2.5 完成字体设置
        • 2.6 完成字体小球的设置
        • 2.7 实现主题切换
        • 2.8 阅读进度功能实现
        • 2.9 实现目录功能

一、epub的核心工作原理解析

1.1 epub分析

  • epub实例化一个Book对象

  • Book对象通过renderTo方法生成一个rendition对象(负责电子书的渲染)

  • Book对象的location对象:负责电子书的定位

  • Book对象的Navigation对象:负责电子书目录,并提供目录所在的路径

    Vue-微信读书项目笔记-demo篇[开发]_第1张图片

1.2 实现电子书的解析和渲染

通过epub生成Book对象,通过renderTo方法生成rendition对象的过程

this.rendition = this.book.renderTo('read', {
  width: window.innerWidth,
  height: window.innerHeight
})

为了方便开发,在vscode中配置vue的代码段preferences-user snippets

新建一个vue的片段

{
	"Print to console": {
		"prefix": "vue",
		"body": [
			""
			""
			""
			""
			""
		],
		"description": "Log output to console"
	}
}
  • 在src下创建Ebook组件

  • 在router下配置路由,配置成功后可以通过url访问页面

    routes: [
        {
            path: '/',
            redirect: '/ebook'
        },
        {
            path: '/ebook',
            component: Ebook
        }
    ]
    
  • 在Ebook.vue文件下编写代码

    this.book = new Epub('/static/电子书名称');
    console.log(this.book);
    

    生成Book对象

    Vue-微信读书项目笔记-demo篇[开发]_第2张图片

    1. container就是contain.xml
    2. container指向的rootFile就是packagePath:content.opf
    3. content.opf会解析成package对象
    4. package对象中的metadata就是出版信息
    5. manifest是所有用到的文件信息
    6. ncxPath是目录的路径
    7. spine是阅读顺序
  • 生成rendition,通过book.renderTo

    renderTo方法(两个参数)

    参一:DOM的id,生成的电子书会以DOM的形式挂载到这个id上
    
    参二:是一个配置对象,定义渲染电子书的宽高
    
    import Epub from 'epubjs'
    const DOWNLOAD_URL = '/static/2018_Book_AgileProcessesInSoftwareEngine.epub'
    global.ePub = Epub
    export default {
        methods: {
          // 电子书的解析和渲染
          showEpub() {
              // 生成bok
              this.book = new Epub(DOWNLOAD_URL)
              // 生成rendition 通过book.renderTo
              this.rendition = this.book.renderTo('read', {
                  width: window.innerWidth,
                  height: window.innerHeight
              })
              // 通过rendition.display渲染电子书
              this.rendition.display()
          }
        },
        mounted() {
            this.showEpub()
        }
    }
    

    可以取消eslint检查 - 函数后的空格

    在eslint的配置文件中配置:'space-before-function-paren': 'off'


    挂载在read这个id上的电子书,epub实际上是使用了iframe实现的,内部有一个完整的document对象

二、功能实现

2.1 电子书翻页功能的实现

思路:

电子书是在idreaddiv,填充了整个屏幕

可以在div的上层使用绝对定位做一个浮层,点击浮层的左侧,进入上一页,点击浮层的右侧,进入下一页

<div id="read">div>
<div class="mask">
  <div class="left" @click="prevPage">div>
  <div class="center" @click="toggleTitleAndMenu">div>
  <div class="right" @click="nextPage">div>
div>
.read-wrapper {
  .mask {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 100;
    display: flex;
    width: 100%;
    height: 100%;
    .left {
      flex: 0 0 px2rem(100);
    }
    .center {
      flex: 1;
    }
    .right {
      flex: 0 0 px2rem(100);
    }
  }
}

flex布局的知识点

Vue-微信读书项目笔记-demo篇[开发]_第3张图片

翻页功能实际上时利用rendition.prev和rendition.next方法

prevPage() {
  // Rendition.prev
  this.rendition && this.rendition.prev().then(() => this.showProgress())
},
// 下一页
nextPage() {
  // Rendition.prev
  this.rendition && this.rendition.next().then(() => this.showProgress())
},

2.2 标题栏和菜单栏实现

采用绝对定位

标题栏

  • 布局
<div class="title-wrapper" v-show="ifTitleAndMenuShow">
  <div class="left">
    <span class="icon-back icon">span>
  div>
  <div class="right">
    <div class="icon-wrapper">
      <span class="icon-cart icon">span>
    div>
    <div class="icon-wrapper">
      <span class="icon-person icon">span>
    div>
    <div class="icon-wrapper">
      <span class="icon-more icon">span>
    div>
  div>
div>

  • 为图标设置默认样式,在global.scss中

    .icon {
        color: #333;
        font-size: px2rem(22);
    }
    
    
  • 设置阴影样式:设置box-shadow属性:box-shadow: 0 px2rem(8) px2rem(8) rgba(0, 0, 0, .15);

  • 设置左右区域的样式

    .left {
      flex: 0 0 px2rem(60);
      @include center;
    }
    .right {
      flex: 1;
      display: flex;
      justify-content: flex-end;
      .icon-wrapper {
        flex: 0 0 px2rem(40);
        @include center;
        .icon-cart {
          font-size: px2rem(22);
        }
      }
    }
    
    

菜单栏

  • 布局

    <div class="menu-wrapper">
    	<div class="icon-wrapper">
    		<span class="icon-menu">span>
    	div>
    	<div class="icon-wrapper">
    		<span class="icon-progress icon">span>
    	div>
    	<div class="icon-wrapper">
    		<span class="icon-bright icon">span>
    	div>
    	<div class="icon-wrapper">
    		<span class="icon-a icon">Aspan>
    	div>
    div>
    
    
  • 样式

    .menu-wrapper {
      position: absolute;
      bottom: 0;
      left: 0;
      z-index: 102;
      display: flex;
      width: 100%;
      height: px2rem(48);
      background: white;
      box-shadow: 0 px2rem(-8) px2rem(8) rgba(0, 0, 0, .15);
      &.hide-box-shadow {
        box-shadow: none;
      }
      .icon-wrapper {
        flex: 1;
        @include center;
        .icon-progress {
          font-size: px2rem(28);
        }
        .icon-bright {
          font-size: px2rem(24);
        }
      }
    }
    
    

2.3 标题栏和菜单栏的显隐

点击中间,标题栏和菜单栏会显示,再次点击会隐藏

<div class="mask">
	<div class="left" @click="prevPage">div>
	<div class="center" @click="toggleTitleAndMenu">div>
	<div class="right" @click="nextPage">div>
div>


给center绑定方法

data() {
    return {
        ifTitleAndMenuShow: false
    }
},
methods: {
    toggleTitleAndMenu() {
        this.ifTitleAndMenuShow = !this.ifTitleAndMenuShow
    }
}

Transition动画原理

  1. 使用v-show动态显示或隐藏元素时,会触发过渡动画
  2. transition需要指定name,并包裹一个包含v-show的div
  3. vue会为transition包裹的div动态添加class,共6种Vue-微信读书项目笔记-demo篇[开发]_第4张图片

2.4 标题栏和菜单栏的过渡动画

  • 标题栏的过渡动画

    <transition name="slide-down">
    	<div class="title-wrapper" v-show="ifTitleAndMenuShow">div>
    transition>
    
    
  • 菜单栏的过渡动画

    <transition name="slide-up">
      <div class="menu-wrapper" v-show="ifTitleAndMenuShow">
    transition>
    
    
  • 样式

    .slide-down-enter,
    .slide-down-leave-to {
      transform: translate3d(0, px2rem(-108), 0);
    }
    
    .slide-down-enter-to,
    .slide-down-leave,
    .slide-up-enter-to,
    .slide-up-leave {
      transform: translate3d(0, 0, 0);
    }
    
    .slide-down-enter-active,
    .slide-down-leave-active,
    .slide-up-enter-active,
    .slide-up-leave-active {
      transition: all .3s linear;
    }
    
    .slide-up-enter,
    .slide-up-leave-to {
      transform: translate3d(0, px2rem(108), 0);
    }
    
    

由于两个过渡动画重复代码非常多,合并重复代码,放在global.scss中,这样以后的transition组件的name属性是slide-downslide-up的就会直接去global.scss中去找了

2.5 完成字体设置

  • 在Ebook文件中配置

    data() {
        return {
            ifTitleAndMenuShow: false,
            fontSizeList: [
                { fontSize: 12 },
                { fontSize: 14 },
                { fontSize: 16 },
                { fontSize: 18 },
                { fontSize: 20 },
                { fontSize: 22 },
                { fontSize: 24 },
            ]
        }
    }
    
    
  • 在MenuBar中接收fontSize属性

    props: {
        ifTitleAndMenuShow: {
            type: Boolean,
            default: false
        },
        fontSizeList: Array
    }
    
    
  • 动态绑定style属性

    <div class="preview" :style="{fontSize: fontSizeList[0].fontSize + 'px'}">Adiv>
    
    
  • 字号选择条的布局与样式

    <div class="setting-font-size" v-if="showTag === 0">
      <div class="preview" :style="{fontSize: fontSizeList[0].fontSize + 'px'}">Adiv>
      <div class="select">
        <div class="select-wrapper" v-for="(item, index) in fontSizeList" :key="index" @click="setFontSize(item.fontSize)">
          <div class="line">div>
          <div class="point-wrapper">
            <div class="point" v-show="defaultFontSize === item.fontSize">
              <div class="small-point">div>
            div>
          div>
          <div class="line">div>
        div>
      div>
      <div class="preview" :style="{fontSize: fontSizeList[fontSizeList.length - 1].fontSize + 'px'}">Adiv>
    div>
    
    
    .setting-font-size {
      display: flex;
      height: 100%;
      .preview {
        flex: 0 0 px2rem(40);
        @include center;
      }
      .select {
        display: flex;
        flex: 1;
        .select-wrapper {
          flex: 1;
          display: flex;
          align-items: center;
          .line {
            flex: 1;
            height: 0;
            border-top: px2rem(1) solid #ccc;
          }
          .point-wrapper {
            position: relative;
            flex: 0 0 0;
            width: 0;
            height: px2rem(7);
            border-left: px2rem(1) solid #ccc;
            }
          }
        }
      }
    }
    
    

效果
在这里插入图片描述

<div class="select-wrapper" v-for="(item, index) in fontSizeList" :key="index">div>


在这里插入图片描述

  • 为了完成左侧和右侧的线消失掉,需要在select-wrapper外面再套一层,用first-child类实现

    &:first-child {
      .line {
        &:first-child {
          border-top: none;
        }
      }
    }
    &:last-child {
      .line {
        &:last-child {
          border-top: none;
        }
      }
    }
    
    
    

设置栏的font-size

<div class="select-wrapper"
	v-for="(item, index) in fontSizeList"
    :key="indxe"
    @click="setFontSize(item.fontSize)"
>div>


不是在MenuBar中操作,而是要传到父类中去操作,因为它需要父类中的book对象中的一些数据去处理要在这个方法中调用父组件的方法,将fontSize传进去,让父组件来完成这个操作

  • 在showEpub中获取到Theme对象:this.themes = this.rendition.themes
  • 设置setFontSize方法
setFontSize(fontSize) {
    this.defaultFontSize = fontSize
    if (this.themes) {
        this.themes.fontSize(fontSize + 'px')
    }
}


2.6 完成字体小球的设置

.point {
  position: absolute;
  top: px2rem(-8);
  left: px2rem(-10);
  width: px2rem(20);
  height: px2rem(20);
  border-radius: 50%;
  background: white;
  border: px2rem(1) solid #ccc;
  box-shadow: 0 px2rem(4) px2rem(4) rgba(0, 0, 0, .15);
  @include center;
  .small-point {
    width: px2rem(5);
    height: px2rem(5);
    background: black;
    border-radius: 50%;
  }
}


注意:当点击设置栏后,点击设置栏的任意一个位置,就会触发之前设置的蒙版的点击事件

原因:蒙版的z-index值大于设置栏的index值

解决:给设置栏设置一个z-index值

2.7 实现主题切换

通过theme对象实现主题切换

  • 设置主题数据

    themeList: [
        {
            name: 'default',
            style: {
                body: {
                    'color': '#000',
                    'background': '#fff'
                }
            }
        },
        {
            name: 'eye',
            style: {
                body: {
                    'color': '#000',
                    'background': '#fff'
                }
            }
        }
    ]
    
    
    
  • 定义注册主题的方法

    registerTheme() {
        this.themeList.forEach(theme => {
            this.themes.register(theme.name, theme.style)
        })
    }
    
    
    

    调用registerTheme方法测试下

    this.registerTheme()
    this.themes.select('eye')
    
    
    

    颜色改变,即为成功

  • 定义设置主题的方法

    setTheme(index) {
        this.themes.select(this.themeList[index].name)
    }
    
    
    
    • 定义一个defaultTheme的属性

      优点:将defaultTheme保存下载,当用户选择了一个主题后,可以保存在cookie、localStorage中,当用户下一次打开的时候,仍然能够默认使用上一次使用的主题

    • 使用setTheme方法

      this.registerTheme()
      this.setTheme(this.defaultTheme)
      
      
      

    这样在Ebook组件上的方法和属性就配置好了

  • 将themeList属性、defaultTheme属性、setTheme方法传到MenuBar组件,在MenuBar组件中接收这些属性和方法,实现主题栏的布局

    接收数据

    props: {
        ifTitleAndMenuShow: {
            type: Boolean,
            default: false
        },
        fontSizeList: Array,
        defaultFontSize: Number,
        themeList: Array,
        defaltTheme: Number
    }
    
    
    
    • 在MenuBar中为showSetting方法添加参数,来实现点击不同的图标出现不同的菜单

      <div class="setting-font-size" v-if="showTag === 0">···div>
      <div class="setting-theme" v-else-if="showTag === 1">div>
      
      
      
      <div class="icon-wrapper">
          <span class="icon-bright icon" @click="showSetting(1)">span>
      div>
      <div class="icon-wrapper">
          <span class="icon-a icon" @click="showSetting(0)">Aspan>
      div>
      
      
      
      showSetting(tag) {
          this.ifSettingShow = true
          this.showTag = tag
      }
      
      
      
      data() {
          return {
              ifSettingShow: false,
              showTag: 0
          }
      }
      
      
      
    • 结构

      <div class="setting-theme" v-else-if="showTag === 1">
        <div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
          <div class="preview" :style="{background: item.style.body.background}" :class="{'no-border': item.style.body.background !== '#fff'}">div>
          <div class="text" :class="{'selected': index === defaultTheme}">{{item.name}}div>
        div>
      div>
      
      
      
    • 样式

      .setting-theme {
        height: 100%;
        display: flex;
        .setting-theme-item {
          flex: 1;
          display: flex;
          flex-direction: column;
          padding: px2rem(5);
          box-sizing: border-box;
          .preview {
            flex: 1;
            border: px2rem(1) solid #ccc;
            box-sizing: border-box;
            &.no-border {
              border: none;
            }
          }
          .text {
            flex: 0 0 px2rem(20);
            font-size: px2rem(14);
            color: #ccc;
            @include center;
            &.selected {
              color: #333;
            }
          }
        }
      }
      
      
    • 设置点击事件

      <div class="setting-theme" v-else-if="showTag === 1">
        <div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
        div>
      div>
      
      setTheme(index) {
          this.$emit('setTheme', index)
      }
      

2.8 阅读进度功能实现

通过epubjs的locations对象实现

打印locations对象

Vue-微信读书项目笔记-demo篇[开发]_第5张图片

发现_locations是空的,epubcfi也是空的,说明locations对象默认是不会生成的,因为locations对象生成比较消耗性能,所以默认情况是不会生成locations对象的

通过epubjs的钩子函数ready来实现,当电子书解析完毕,进行回调的方法,返回一个promise对象,通过异步的方法实现电子书的定位,当电子书解析完毕,就可以生成它的locations对象,通过locations的generate方法生成定位符,參數是每页的字数

this.book.ready.then(() => {
    return this.book.locations.generate()
}).then(result => console.log(result))

在这里插入图片描述

epubcfi:EPUB Canoical Fragment Identifiers 1.1 定位符规范,用来定位的

通过epubcfi和百分比做一个集合,就可以找到指定的页数

  • 新建数据变量:bookAvailable: false,表示图书是否处于可用状态,默认是false,当locations对象生成后,将其设置为true。传递给MenuBar组件
    Vue-微信读书项目笔记-demo篇[开发]_第6张图片

  • 在MenuBar中设置点击事件,点击progress按钮的时候的点击事件

    <div class="icon-wrapper">
        <span class="icon-progress icon" @click="showSetting(2)">span>
    div>
    
  • progress结构

    <div class="setting-progress" v-else-if="showTag === 2">
      <div class="progress-wrapper">
        <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"
               :style="{backgroundSize: calcBackgroundSize}"
               ref="progress">
      div>
      <div class="text-wrapper">
        <span>{{bookAvailable ? progress + '%' : '加载中...'}}span>
      div>
    div>
    

    type=”range” 将控件设置为滑块

    step=1 每移动一次增加的幅度

    @change表示松手的时候触发的事件

    @input表示拖动的时候下面的百分比变化

    .setting-progress {
      position: relative;
      width: 100%;
      height: 100%;
    
      .progress-wrapper {
        width: 100%;
        height: 100%;
        @include center;
        padding: 0 px2rem(30);
        box-sizing: border-box;
    
        .progress {
          width: 100%;
          -webkit-appearance: none;
          height: px2rem(2);
          background: -webkit-linear-gradient(#999, #999) no-repeat, #ddd;
          background-size: 0 100%;
    
          &:focus {
            outline: none;
          }
    
          &::-webkit-slider-thumb {
            -webkit-appearance: none;
            height: px2rem(20);
            width: px2rem(20);
            border-radius: 50%;
            background: white;
            box-shadow: 0 4px 4px 0 rgba(0, 0, 0, .15);
            border: px2rem(1) solid #ddd;
          }
        }
      }
    
      .text-wrapper {
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        color: #333;
        font-size: px2rem(12);
        text-align: center;
      }
    }
    
    onProgressInput(progress) {
        this.progress = progress
        this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
    },
    // 进度条松开后触发事件 根据进度条数值跳转到指定位置
    onProgressChange(progress) {
        this.$emit('onProgressChange', progress)
    }
    

2.9 实现目录功能

首先获取navigation对象

this.book.ready.then(() => {
    this.navigation = this.book.navigation
    console.log(this.navigation)
    return this.book.locations.generate()
}).then(result => {
    this.locations = this.book.locations
    // 标记电子书为解析完毕
    this.bookAvailable = true
})

Vue-微信读书项目笔记-demo篇[开发]_第7张图片

toc:目录的内容

href:目录的链接

将href放入this.rendition.display中就可显示了

你可能感兴趣的:(项目开发笔记)