重构 Vue 阅读器

早期项目因为赶进度之类的,一些代码和模块都是挤在一起,显得很冗余,也很不方便之后的维护,所以打算将公司的项目进行一次重构,本次重构使用vue-cli3.0为模板进行升级重构。

安装

直接使用命令安装新版本的vue-cli

npm install -g @vue/cli
# 或者
yarn global add @vue/cli

安装成功后,直接通过vue create project来创建项目即可。

提示

有趣的是,vue-cli3.0提供了一个可视化的操作面板,可以通过这个面板创建、打包、分析以及管理项目,也可以进行包的管理。感兴趣的朋友可以直接通过命令vue ui玩耍玩耍~

配置

不同于vue-cli之前版本的是,vue-cli3.0缩小的大幅度的配置,因为内部已经配置了大多数常用的webpack配置了,而我们只需要在根目录配置一个vue.config.js文件就可以管理以及配置我们的项目。

这里我们也不细说怎么去配置vue.config.js官网其实说的也很清楚了,这里就直接上一些简单的配置:

// vue.config.js 配置
module.exports = {
  baseUrl: "./",
  lintOnSave: false,
  indexPath: "index.html",
  chainWebpack: config => {
    // 修改webpack内部配置
    if (process.env.NODE_ENV === "production") {
      // 修改打包模板
      config.plugin("html").tap(args => {
        args[0].template = "public/index.prod.html";
        // 加上属性引号
        args[0].minify.removeAttributeQuotes = false;
        return args;
      });
      // 忽略文件设置
      config.plugin("copy").tap(args => {
        args[0][0].ignore = [...args[0][0].ignore, "index.prod.html"];
        return args;
      });
    }
  },
  configureWebpack: config => {
    // 增加webpack配置
    // 打包忽略项
    if (process.env.NODE_ENV === "production") {
      config.externals = {
        vue: "Vue",
        "vue-router": "VueRouter"
      };
    }
  }
};

重构 -- 阅读器

整个项目逻辑最复杂的就是阅读器这块,而阅读器这块,这里打算使用黄轶老师出品的better-scroll进行封装重构。

之所以使用better-scroll我也想了挺久的,better-scroll在滚动上体验上确实没得说,相当的好,唯一让我犹豫的就是它太大了,打包进vendor的话无疑会大大加大包的体积,但是后来又灵光一闪,诶,我打包的时候抽离出来用 cdn 的方式引入不就好了?毕竟gzip只有9kb,就小多了。于是就美滋滋的决定了~。

扯多了,开始吧~

阅读器的核心功能,也就是比较难的一个就是阅读模式的切换,分竖屏阅读横屏滚动阅读。

竖屏阅读倒是简单,通过封装better-scroll,默认竖屏滚动即可。

先简单封装下阅读器的滚动组件reader-wrapper.vue

<template>
  <div class="reader-wrapper"
       ref="wrapper">
    <div :class="[
      'wrapper-content',
      direction === 'horizontal' ? 'horizontal' : 'vertical']"
         ref="readerWrapper">
      <div class="scroll-content"
           ref="content">
        <div class="content"
             ref="chapter">
          <slot></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from "better-scroll";

// * 滚动方向
const DIRECTION_V = "vertical";
const DIRECTION_H = "horizontal";

export default {
  name: "x-reader-wrapper",
  props: {
    probeType: {
      type: Number,
      default: 1
    },
    click: {
      type: Boolean,
      default: true
    },
    listenScroll: {
      type: Boolean,
      default: false
    },
    direction: {
      type: String,
      default: DIRECTION_V
    },
    startY: {
      type: Number,
      default: 0
    },
    bounce: {
      default: true
    }
  },
  mounted() {
    setTimeout(() => {
      // * 初始化bs
      this.init();
    }, 20);
  },
  destroyed() {
    this.$refs.wrapper && this.$refs.wrapper.destroy();
  },
  methods: {
    init() {
      if (!this.$refs.wrapper) {
        return;
      }
      // * bs 配置
      let options = {
        probeType: this.probeType,
        click: this.click,
        scrollY: this.direction === DIRECTION_V,
        scrollX: this.direction === DIRECTION_H,
        startY: this.startY,
        bounce: this.bounce,
        momentum: this.direction === DIRECTION_V
      };

      this.scroll = new BScroll(this.$refs.wrapper, options);

      if (this.listenScroll) {
        // * 监听滚动
        this.scroll.on("scroll", pos => {
          this.$emit("scroll", pos);
        });
      }
    },
    refresh() {
      this.scroll && this.scroll.refresh();
    },
    scrollTo() {
      this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments);
    },
    destroy() {
      this.scroll.destroy();
    }
  }
};
</script>

然后新建一个reader.vue用来引用reader-wrapper.vue

<template>
  <div class="reader">
    <reader-wrapper ref="reader"
                    :direction="direction">
      <p class="x-reader-text-name"
         style="font-size: 24px;">第1章 初相见</p>
      <p class="x-reader-text-paragraph">哈哈十二年的冬月初1,是顾轻舟的生日,她今天十六岁整了。</p>
      <!-- 省略若干 -->
      <p class="x-reader-text-paragraph"> “叫!”他命令道,声音嘶哑。</p>
    </reader-wrapper>
  </div>
</template>

<script>
import ReaderWrapper from "./reader-wrapper";

export default {
  components: {
    ReaderWrapper
  },
  data() {
    return {
      direction: "horizontal"
    };
  }
};
</script>

这样竖屏滚动就好了。

横屏滚动

横屏滚动要考虑的东西就有些多了:

  • 页面的布局 类似轮播一样排版,但是又不是轮播
  • 滚动方向
  • 滚动边界的判断 下一页还是下一章 上一页还是上一章

第一点,因为小说内容谁也不知道一页里能放下多少段话,所以对于横屏的分页来说有点小麻烦,不过好在有个css属性能帮助我们解决这个--columm布局。

我们通过column-widthcolumn-gap两个属性即可,因为column-count我们并不知道可以分成几列。

  • column-width - 列宽
  • column-gap - 每列的间隔

具体可以参照菜鸟教程或者 w3school 去了解。这里都没有书写css的内容~。

我们改装下之前封装的reader-wrapper.vue,首先我们需要监听direction,如果方向改变了,则需要去计算滚动宽度:

<script>
export default {
  watch: {
    direction(dir) {
      this.initDir(dir);
    }
  },
  methods: {
    initDir(dir) {
      // * 初始化滚动方向
      let refs = this.$refs;
      if (dir === "horizontal") {
        // * 横向需要一个总内容宽度
        this.$nextTick(() => {
          // * 在 column 布局下,可以直接获取元素的scrollWidth
          refs.content.style.width = refs.chapter.scrollWidth + "px";
        });
      } else {
        // * 竖向清除内容宽度 默认为设备宽度
        refs.content.style.width = "";
      }
      // * 重置滚动位置
      this.scrollTo(0, 0);
    }
  }
};
</script>

总宽度知道的话,我们就能知道一章小说能够被分为多少页,因为设备宽度我们也能获取,可以通过滚动宽度 / 设备宽度 = 页数

<script>
export default {
  methods: {
    countPage(total) {
      // * 计算总页码数
      this.width = Math.min(
        window.innerWidth,
        window.screen.width,
        document.body.offsetWidth
      );
      // * 设置可视距离宽度
      this.$refs.chapter.style.width = this.width + "px";
      // * 页码结果向上取整
      let pageCount = Math.ceil(total / this.width);
      this.$emit("count-page", {
        pageCount,
        disArr: this.perSwiperDis(pageCount), // 每页需要的滚动距离
        dWidth: this.width
      });
    },
    perSwiperDis(totalPage) {
      // * 计算每页的滚动距离
      let sum = 0;
      let arr = [];
      arr.push(sum);
      for (let i = 1; i < totalPage; i++) {
        sum += -this.width;
        arr.push(sum);
      }
      return arr;
    }
  }
};
</script>

然后在initDir方法中调用下即可:

// ...
refs.content.style.width = refs.chapter.scrollWidth + "px";
// * 计算页码
this.countPage(refs.chapter.scrollWidth);
// ...

然后就是滚动滑页和方向的判断,一开始我是使用监听滚动事件,然后每次滚动的坐标x去和页面滚动距离数组中循环对比,然后去计算判断方向,后来发现这样太傻逼了,也相当耗性能,每次都得去循环判断,累都得累死。

后来发现bs有提供一个movingDirectionX的属性,可以知道用户在滑动的过程中是从哪个方向进行滑动的,然后就有一个简便点的方式,一个是,我不需要滚动的实时坐标,我只要用户离开屏幕后的坐标即可,而且我也不需要循环滚动距离数组去做对比,我只要知道我在哪一页就行了,因为页码-1后就对应滚动距离数组中的值。

可能看着理解起来有些乱,那我们直接就用代码说话吧,找到reader-wrapper.vue

<script>
export default {
  methods: {
    init() {
      // 省略若干代码...
      // * 监听手指离开屏幕的事件 并返回最后的x,y坐标和移动方向
      // * 我们只需要横屏滚动的时候监听
      if (this.direction === DIRECTION_H) {
        this.scroll.on("touchEnd", pos => {
          this.$emit("touch", {
            pos,
            dir: {
              x: this.scroll.movingDirectionX,
              y: this.scroll.movingDirectionY
            }
          });
        });
      }
      // 省略若干代码...
    }
  }
};
</script>

然后进入reader.vue页面:

<template>
  <div class="reader">
    <!-- 绑定上组件返回的事件 -->
    <reader-wrapper ref="reader"
                    :direction="direction"
                    @touch="touch"
                    @count-page="countPage">
      <!-- 省略内部结构 -->
    </reader-wrapper>
  </div>
</template>

<script>
import ReaderWrapper from "./reader-wrapper";

// * 滚动阈值
const THRESHOLD_NEXT = -50;
const THRESHOLD_PREV = 50;

export default {
  components: {
    ReaderWrapper
  },
  data() {
    return {
      direction: "horizontal",
      pageCount: 1,
      curPage: 1,
      distance: 0
    };
  },
  methods: {
    touch({ pos, dir }) {
      // * 只对横向滚动 边界判断
      if (this.direction === "vertical") return;
      this.distance = pos.x;
      let arr = this.disArr;
      let pageDir = arr[this.curPage - 1];
      let diff = this.distance - pageDir;
      // * 从右向左滑
      if (dir.x === 1) {
        if (diff < THRESHOLD_NEXT) this.nextPage();
        else this.lockPage();
      }
      // * 从左向右滑
      if (dir.x === -1) {
        if (diff > THRESHOLD_PREV) this.prevPage();
        else this.lockPage();
      }
    },
    countPage({ pageCount, disArr, dWidth }) {
      // * pageCount - 总页数
      // * disArr - 每页需要的滚动距离
      // * dWidth - 设备宽度
      this.pageCount = pageCount;
      this.disArr = disArr;
      this.width = dWidth;
    },
    nextPage() {
      // * 翻页逻辑 -- 下一页
      let reader = this.$refs.reader;
      if (this.curPage === this.pageCount) {
        // * 最后一页再往后翻 就获取下一章内容
        console.log("最后一页了");
      } else {
        this.curPage++;
        this.lockPage();
      }
    },
    prevPage() {
      // * 翻页逻辑 -- 上一页
      let reader = this.$refs.reader;
      if (this.curPage === 1) {
        // * 第一页再往前翻 就获取上一章内容
        console.log("第一页了");
      } else {
        this.curPage--;
        this.lockPage();
      }
    },
    lockPage() {
      // * 锁定停留的页面
      let reader = this.$refs.reader;
      let pageDir = this.disArr[this.curPage - 1];
      reader.scrollTo(pageDir, 0, 300);
      this.distance = pageDir;
    }
  }
};
</script>

这样一个阅读器大致的功能就出来了。不过后来测试也发现了一些问题:

  • 切换横竖屏的时候,横屏滚动会出现不流畅的情况
  • 使用padding等让盒子宽度变化的属性时,会加入到滚动距离中,导致滚动到最后一页时,出现偏差

第一个问题,解决方案是,在切换横竖屏后,销毁bs实例,然后重新初始化一个新的实例。

第二个问题就是避免在容器上加例如padding的属性,从内部段落上加入padding等属性即可。

最后实现的效果如下:


显示阅读器的菜单

阅读器的核心部分完成之后,接下来就是显示阅读的操作菜单了。

我们先在reader-wrapper.vue中布置出菜单部分:

<template>
  <div class="reader-wrapper"
       ref="wrapper"
       :style="{fontSize:fz}">
    <!-- 阅读器部分 -->
    <div :class="[
      'wrapper-content',
      direction === 'horizontal' ? 'horizontal' : 'vertical']"
         ref="readerWrapper">
      <div class="scroll-content"
           ref="content">
        <div class="content"
             ref="chapter">
          <slot></slot>
        </div>
      </div>
    </div>
    <!-- 头部菜单和底部菜单 -->
    <transition name="down">
      <div class="wrapper-header" v-show="menu">
        <slot name="header">
          <reader-header></reader-header>
        </slot>
      </div>
    </transition>
    <transition name="up">
      <div class="wrapper-menu" v-show="menu">
        <slot name="menu">
          <reader-menu></reader-menu>
        </slot>
      </div>
    </transition>
  </div>
</template>

之所以菜单部分还是用插槽是因为,之后是有打算将阅读器部分完全抽离出来,封装成独立的组件,方便之后的项目使用,不同的项目,可能菜单部分多少有些不一样,所以使用插槽,让其更具灵活一些。

这里还加入了显示菜单的动画,用过阅读 app 的朋友都能看到这么个效果,呼出操作菜单的时候,顶部是下滑而出,底部是上滑而出,所以我们要做的也是这个效果,直接使用transition即可,动画效果也比较简单:

// 下滑
.down-enter, .down-leave-to {
  opacity 0
  transform translateY(-100%)
}

// 上滑
.up-enter, .up-leave-to {
  opacity 0
  transform translateY(100%)
}

.down-enter-active, .down-leave-active,
.up-enter-active, .up-leave-active {
  transition all 0.3s
}

然后我们建立一个reader-header.vuereader-menu.vue用于放置头部菜单和底部菜单,reader-header部分比较简单:

<!-- reader-header.vue -->
<template>
  <div class="reader-header">
    header
  </div>
</template>

然后就是reader-menu部分:

<template>
  <div class="reader-menu">
    <div class="reader-menu__box">
      <div class="menu-box__text">字号</div>
      <div class="menu-box__item">
        <div class="item-font__operator item-font__subtract">A-</div>
        <div class="item-font__text">19</div>
        <div class="item-font__operator item-font__add">A+</div>
        <div class="item-font__reset">默认</div>
      </div>
    </div>
    <div class="reader-menu__box">
      <div class="menu-box__text">背景</div>
      <div class="menu-box__item">
        <div class="item-bg"></div>
        <div class="item-bg"></div>
        <div class="item-bg"></div>
        <div class="item-bg"></div>
        <div class="item-bg"></div>
      </div>
    </div>
    <div class="reader-menu__box">
      <div class="menu-box__item">
        <div class="item-operator">上一章</div>
        <div class="item-operator">目录</div>
        <div class="item-operator">翻页方式</div>
        <div class="item-operator">夜间</div>
        <div class="item-operator">下一章</div>
      </div>
    </div>
  </div>
</template>

这样,头部和底部的菜单就完成了。

哈哈哈 都是简单的书写哈~

最后完成的效果如下:


字体大小切换

唔,昨天我们把底部菜单的布局给实现了,今天实现下一些具体功能 -- 字体大小切换

阅读器上肯定会存在一些默认属性,比如默认的字体大小,默认的背景色,默认的阅读方式(竖屏还是横屏)之类的,我们可以将这些默认配置写入到一个配置文件里,这里有两种方式做默认配置:

  • 一个通过vuex来控制这些默认数据
  • 一个是通过一个js文件存储,然后通过bus来操作

两者不管使用哪一种,都得需要借助localStorage来保存用户的操作,避免刷新浏览器后重置了用户的设置。

我个人喜欢将用户容易操作到的变量存入vuex中,然后一些固有数据放在js配置文件中做使用,这里就不做vuex的配置和说明了,看官方文档或者我之前写过的一篇粗略的Vuex 学习笔记也可。

我这里将用户的常用操作变量放在vuex中

// state.js
const str = window.localStorage;

const state = {
  fontSize: str.getItem("fontSize") || 14, // 默认字体
  bgIndex: str.getItem("bgIndex") || 1, // 背景索引
  direction: str.getItem("direction") || "vertical", // 阅读方式
  night: str.getItem("night") || false // 是否夜间模式
};

export default state;

都是优先从缓存中取,如果有就用缓存数据,没有则使用默认数据。

commit部分就不写了,这里就写下action部分,用户操作commit后顺便将这些数据存入缓存中:

// action.js
import * as type from "./mutation-type";
const str = window.localStorage;

// 修改阅读器的设置
export const setReaderOptions = ({ commit, getters }, { name, value }) => {
  switch (name) {
    case "font":
      commit(type.SET_FONTSIZE, value);
      str.setItem("fontSize", value);
      break;
    case "bg":
      commit(type.SET_BG_INDEX, value);
      str.setItem("bgIndex", value);
      break;
    case "direction":
      commit(type.SET_READER_DIRECTION, value);
      str.setItem("direction", value);
      break;
    case "night":
      commit(type.TOGGLE_READER_NIGHT);
      str.setItem("night", getters.night);
      break;
    default:
      return;
  }
};

然后我们再定义一个readerOptions.js来放置固定的配置数据:

// readerOptions.js
const readerOptions = {
  defaultFontSize: 19, // 默认字体大小
  maxFontSize: 36, // 最大字体大小
  minFontSize: 12, // 最小字体大小
  bgColorList: [] // 背景列表,含背景色,对应背景色的字体色,暂时没找对应数据
};

export default readerOptions;

最后我们就在reader-menu.vue中引入使用就行:

<script>
import { mapState, mapGetters, mapActions } from "vuex";
import ReaderOptions from "./readerOptions.js";

export default {
  computed: {
    ...mapState("reader", ["direction"]),
    ...mapGetters("reader", ["fontSize", "night"])
  },
  data() {
    return {
      min: ReaderOptions.minFontSize,
      max: ReaderOptions.maxFontSize,
      default: ReaderOptions.defaultFontSize,
      bgList: ReaderOptions.bgColorList
    };
  },
  methods: {
    subtract() {
      this.font = this.fontSize;
      // * 边界判断
      if (this.font - 1 < this.min) return;
      this.setReaderOptions({ name: "font", value: this.font - 1 });
      this.$emit("font", this.font - 1);
    },
    add() {
      this.font = this.fontSize;
      // * 边界判断
      if (this.font + 1 > this.max) return;
      this.setReaderOptions({ name: "font", value: this.font + 1 });
      this.$emit("font", this.font + 1);
    },
    resetFont() {
      this.setReaderOptions({ name: "font", value: this.default });
      this.$emit("font", this.default);
    },
    toggleNight() {
      this.setReaderOptions({ name: "night" });
    },
    changeDir() {
      let dir = this.direction === "vertical" ? "horizontal" : "vertical";
      this.setReaderOptions({
        name: "direction",
        value: dir
      });
      this.$emit("direction", dir);
    },
    ...mapActions("reader", ["setReaderOptions"])
  }
};
</script>

是不是不难?以为就这样结束了?哎,坑还有着呢~

  • 修改字体大小后,需要重新刷新bs的容器高度或者宽度,不然会出现滚动不到底的情况
  • 横屏下,因为字体变化,所以生成的页码数量也会跟着变动,所以需要重新计算页码
  • 横屏下,如果滑动到第 3 页,再修改字体,页面会默认滚动到最开始的地方,也就是x=0,再次滑动会直接跳过第 2 页,直接到第 3 页,体验不好
  • 横屏下,重新计算宽度后,滚动不到最后

想必你们也想到了怎么解决了,对,就是利用bsrefresh方法:

<script>
export default {
  watch: {
    font() {
      // * 字体变化后需要重新计算宽度
      this.$nextTick(() => {
        if (this.direction === "horizontal") {
          // * 因为重新计算位置后,dom也发生了变化,所以refresh要放在initDir之后
          // * 可以使用setTimeout也可以再使用一次this.$nextTick()方法
          this.initDir(this.direction);
          setTimeout(() => {
            this.refresh();
            // * 重新锁定滚动位置,原来在第几页就还是第几页
            // * false 表示不需要滚动动画,不然修改字体总会从第1页动画滑动到当前页,看起来也不好
            this._lockPage(false);
          }, 20);
        } else this.refresh();
      });
    }
  }
};
</script>

老规矩 最后的实现效果如下:


背景色切换

背景色切换相对来说会简单一些,就是根据选择的背景色,去修改容器的background就 OK 了,我们将背景色的数据放入配置文件ReaderOptions.js中:

// ReaderOptions.js

const ReaderOptions = {
  // * 背景集合
  bgColorStyles: [
    {
      menuColor: "#f5dce4",
      background: "#f5dce4",
      color: "#7B3149"
    },
    {
      menuColor: "#DFF5DC",
      background: "#DFF5DC",
      color: "#1F4D2B"
    },
    {
      menuColor: "#38658C",
      background: "#38658C",
      color: "#fff"
    },
    {
      menuColor: "#e6e6e6",
      background: "#e6e6e6",
      color: "#262626"
    },
    {
      menuColor: "#ffdca3",
      background:
        "url(https://xxx/reader_bg_parchment_paper.jpg) center / 100% 100% fixed",
      color: "#4c3831"
    }
  ],
  // * 夜间模式背景数据
  nightStyle: {
    color: "#999",
    background: "#222"
  }
};

然后我们在reader-menu.vue中,背景色这块循环bgColorStyles这块内容即可,然后每次我们点击色块,就修改对应的键值,如果是在夜间模式修改色块,则取消夜间模式

<template>
  <!-- 省略若干内容 -->
  <div class="reader-menu__box">
      <div class="menu-box__text">背景</div>
      <div class="menu-box__item">
        <template v-for="(bgStyle, i) in bgStyleList">
          <div class="item-bg"
               :key="i"
               :style="{background:bgStyle.menuColor}"
               @click.stop="bgChange(i)">
            <img v-show="i===bgIndex"
                 class="item-bg__check"
                 src="./assets/check.png"
                 alt="">
          </div>
        </template>
      </div>
    </div>
</template>

<script>
import ReaderOptions from "./readerOptions.js";

export default {
  data() {
    return {
      bgStyleList: ReaderOptions.bgColorStyles
    };
  },
  methods: {
    bgChange(i) {
      // * 修改背景色
      if (this.bgIndex === i) return;
      this.setReaderOptions({ name: "bg", value: i });
      // * 如果处于夜间模式 则切换回来
      if (this.night) this.toggleNight();
      this.$emit("bg");
    }
  }
};
</script>

reader-menu.vue这块基本就这样,然后在reader-wrapper.vue中修改背景色:

<template>
  <div class="reader-wrapper"
       ref="wrapper"
       :style="{
         fontSize: fz,
         background: night ? nightStyle.background : bgStyleList[bgIndex].background,
         color: night ? nightStyle.color : bgStyleList[bgIndex].color
        }">
    <!-- 内容省略 -->
  </div>
</template>

提示

需要判断当前是否处于夜间模式,夜间模式采用夜间模式的配色

阅读器的操作行为

根据用户点击的区域不同,阅读器会有不同的行为,比如点击右侧,则滑入下一页,点击中心区域显示菜单等。

不同的阅读器操作行为可能略有所不同,比如在iReader中,阅读器的操作行为如下图:

iReader

而在搜狗阅读中,阅读器的操作行为如下:

搜狗阅读

大多数就是这两种了,而我们这里要实现的,就是像搜狗阅读这种。

一般来说,这种边界距离不会是固定的,都是根据屏幕尺寸来算的,我们在此将这这个比例设置成1/4,并写入配置文件ReaderOptions.js中:

const readerOptions = {
  // * 阅读器边界
  readerTap: {
    tapArea: 1 / 4,             // 比例
    width: Math.min(            // 阅读器的宽
      window.innerWidth,
      window.screen.width,
      document.body.offsetWidth
    ),
    height: Math.min(           // 阅读器的高
      window.innerHeight,
      window.screen.height
    )
  }
}

然后我们来计算区域,我们先将x方向的区域计算下:

  • 左侧区域x1等于 width * tapArea
  • 右侧区域x2等于 width - 左侧区域的x坐标

其次就是y方向的区域:

  • 顶部区域y1等于 height * tapArea
  • 底部区域y2等于 height - 顶部区域

这样中心区域也就直接出来的。

然后左侧和左上的部分加起来就是上一页的操作区域了:

// x,y 为用户点击的坐标
if (x < x1 || (x < y1 && x < x2)) {
  // 左边以及左上部分的区域
}

同样右侧和右下的部分加起来就是下一页的操作区域:

if (x > x2 || (x > x1 && y > y2)) {
  // 右边以及右下部分的区域
}

最后就是中心区域了:

if (x > x1 && x < x2 && y > y1 && y < y2) {
  // 中心区域
}

接下来逻辑也就很简单了,无非就是菜单的显示隐藏,上一页下一页的操作,以及一些细节把控,这里就不多写了。

最终效果可扫码体验:

扫码体验

提示

过程中代码进行过多次整理,所以这里书写的代码未必是最新的,最新的代码可查看github

bugs

  • 横屏下,手指水平滑动一段距离后再向其他方向滑动,则页面会卡在滑动距离那无法切换页面

原因

该bug出现的原因是因为在touchEnd中使用movingDirectionX来判断滑动方向,而该属性是在滑动过程中去判断,比如我水平滑动100px后,手指向上移动,这时候滑动距离还是100px,可movingDirectionX却没有办法获取到你的滑动方向,所以导致了该bug出现。

movingDirectionX改用directionX,不过directionXtouchEnd中获取一直为0,因为directionX是滚动结束后相对于开始的坐标进行判断滚动方向的,所以该属性在scrollEnd中才能正确获取,所以这里暂时将touchEnd事件改为scrollEnd事件,并不影响整体程序运行。

如果使用scrollEnd的话,滑动翻页则会出发两次_touch事件,一次是手指离开屏幕的时候,一次是进行上下页滚动的时候

新解决

使用偏移值distX来判断方向,正值为从左往右负值为从右往左0点击事件

  • 快速跳页,会容易导致来回切换

原因

快速点击的情况下,在下一页还没有完全滚动结束之前,二次点击会打断当前的滚动,并且会从新计算二次点击的坐标,使系统判断二次点击的坐标属于上一页的触发位移距离,因此导致来回切换。

解决

取消dom的绑定事件,通过方向值为0来判断是否是点击事件,每次点击都获取手指的触点坐标,从而进行点击翻页操作。

上次更新: 1/23/2019, 4:37:20 PM