我是如何用 React 实现一个电子书阅读器的(一)

3,007 阅读5分钟
原文链接: liumin.me

3123123wqedeasda

在线地址:ireader.liumin.me

github开源地址:github.com/liumin1128/…


( ̄▽ ̄)~*先声明哈,API来自追书神器,作者绝对支持正版,支持追书神器,本文只是为了在实战中探讨学习前端技术。

项目中会使用到的技术框架:React,Dva,material-ui

准备工作:Node环境,VSCode

先新建项目,装上依赖,没有dva的同学需要先安装dva

dva new ireader  
cd ireader  
cnpm i material-ui --save  
1. 首先我们添加最核心的功能:获取书源

新建阅读器的modals:reader

dva g model reader  

我们可以看到modals文件夹下已经出现了reader.js文件,这是dva脚手架生成的modal文件,用以封装redux相关功能,所有项目中涉及的数据,变量都要保存在这里。

查看reader.js

export default {  
 namespace: 'reader',  // 命名空间,区别于其他modals
 state: {}, // redux中的state,数据就存储在这里

 subscriptions: { // 用于订阅事件
   setup({ dispatch, history }) {  
   },
 },

 effects: { // 用于获取异步数据,进行流程控制
   *fetch({ payload }, { call, put }) {  
     yield put({ type: 'save' });
   },
 },

 reducers: { // 定义操作state的方法
   save(state, action) {
     return { ...state, ...action.payload };
   },
 },
};

在不考虑复杂场景的情况下,阅读器modals,其实就是对应了一本书的类,而一本电子书类需要保存那些信息呢?(O_O)?

  • 当前章节(章节内容,章节标题,vip信息等等)
  • 章节列表
  • 源列表(目标就是实现换源功能,这个必须有)
  • 以及浏览历史记录(章节位置,当前源等)

由此,我们在state中加入:

state: {  
    chapter: {},            // 当前章节
    chapterList: [],        // 章节列表
    bookSource: [],         // 源
    book: {
        currentChapter: 0,  // 当前章节
        currentSource: 0,   // 当前源
    },
},

ok,我们的阅读器modals就成型了,我们现在需要获取一点数据。

获取数据的流程:书籍id => 源 => 章节列表 => 章节

获取id我们先不做,网上随便找个即可,比如《凡人》的id:508662b8d7a545903b000027,下文常用此id来实验。

有了id,我们直接获取源即可,在effects中加入以下代码:

effects: {  
    *getSource({ query }, { call, put }) {
        const { data: bookSource } = yield call(bookReaderService.getSource, { query });
        yield put({ type: 'save', payload: { bookSource } });
    },
},

年轻司机们可能会感到疑惑,这代码怎么和平时见到的不一样?(O_o)??

确实,这里代码虽短,却使用了好几个es6新特性,一个一个来解释:

  • 方法前面的*号,dva是基于redux-saga的封装,用来表示这个方法是异步用法,相当于async/await中的async
  • 参数{ payload }, { call, put },这其实是两个参数,但每个参数都有自己的属性,即es6中的解构,这样的好处是不在乎参数的数量,参数的位置,以及参数中不需要的部分,还有就是不需要再let id = xxx.id 了。
  • const { bookSource } =,依然是解构,从promise返回值中获取bookSource
  • yield,也是redux-saga中的关键字,后面接一个异步方法,一般是promise,表示等待其执行完成,才会继续执行下面的代码。相当于async/await中的await
  • callput,都是redux-saga中的关键字,call接收一个异步方法,以及这个方法的参数,返回这个方法的结果;putredux中负责调用一个action,在dva则表示调用一个effectsreducers,包括自身。

到这里,我们大概明白了getSource方法是用来干什么的了,就是接收一个参数(就是书籍id),调用获取书源的方法,把返回结果保存到state中。

我们先写保存书籍到state的方法,在reducers中加入:

reducers: {  
    save(state, { payload }) {
        return { ...state, ...payload };
    },
},

功能很简单,就是将新的数据与原来的state合并。

当然现在代码还不能跑,我们还没写bookReaderService,在service中建立bookReader.js文件,添加以下代码:

import request from '../utils/request';

export function getSource({ query }) {  
  return request(`/api/toc?view=summary&book=${query.id}`);
}

回到modals/reader.js中引用:

import * as bookReaderService from '../services/bookReader.js';  

同时设置一下代理,毕竟要跨域访问外网api嘛,在.roadhogrc中加入如下代码:

"proxy": {
    "/api": {
        "target": "http://api.zhuishushenqi.com/",
        "changeOrigin": true,
        "pathRewrite": { "^/api" : "" }
    },
    "/chapter": {
        "target": "http://chapter2.zhuishushenqi.com/",
        "changeOrigin": true,
        "pathRewrite": { "^/api" : "" }
    }
},

至此,getSource已经可以正常工作,但现在还没有办法触发这个方法。我们需要通过一个事件,将书籍id传入,并调用getSource方法。 而另一方面,我们希望何时触发呢?我们可以在路由中监听url变化,判断用户一旦开始阅读某个书籍(也就是url中的书籍id发生变化),就触发这个方法获取书源。至于本地储存,性能优化,我们稍后再讲。

我们希望url应该是这个样子reader?id=508662b8d7a545903b000027 当用户访问这个路由时,即触发getSource获取书源。我们并不需要自己去做这件事,dva直接就可以监听路由变化,我们只需要稍作配置,在reader.js中的subscriptions中加入:

subscriptions: {  
    setup({ dispatch, history }) {
       return history.listen(({ pathname, query }) => {
          if (pathname === '/reader') {
              dispatch({ type: 'getSource', query });
          }
       });
    },
},

监听事件写好了,我们只需要用 dva-cli 来生成路由即可:

dva g route reader  

现在我们可以启动项目了:

npm start  

什么也没有发生?年轻司机们要有耐心,访问:http://localhost:8000/#/reader?id=508662b8d7a545903b000027即可在调试工具查看结果。

WX20170429-102039@2x

至此我们已经成功获取到书源,并且存入redux中。

WX20170429-110157@2x

这里需要chrome以及redux调试插件,年轻司机们需要额外安装:

WX20170429-110619@2x

进不了谷歌应用商店的点击这里:hosts

2. 获取章节列表

首先我们在获取书源后,获取章节列表:

yield put({ type: 'getChapterList', query: { id: bookSource[1]._id } });  

这里我们默认传入第二个书源中的id,以便跳过优质书源。 然后增加获取章节列表的方法:

*getChapterList({ query }, { call, put, select }) {
    const { data: { chapters } } = yield call(bookReaderService.getChapterList, { query });
    yield put({ type: 'save', payload: { chapterList: chapters } });
},

别忘了在service/reader.js中增加getChapterList方法:

export function getChapterList({ query }) {  
  return request(`/api/toc/${query.id}?view=chapters`);
}

刷新即可看到,章节列表也被存到redux中了:

WX20170429-114227@2x

3. 获取章节信息

同样的思路,我们需要在获取章节列表后获取具体章节信息:

yield put({ type: 'getChapter', query: { link: chapters[0].link } });  

增加获取章节的方法:

*getChapter({ query }, { call, put }) {
    const { data: { chapter } } = yield call(bookReaderService.getChapter, { query });
    yield put({ type: 'save', payload: { chapter } });
},

同样不要忘了加上service/reader.js中的方法:

export function getChapter({ query }) {  
  return request(`/chapter/${query.link}?k=2124b73d7e2e1945&t=1468223717`);
}

这样就获取到了章节列表,是不是很轻松?

WX20170429-160203@2x

回顾以上内容,我们一步一步获取到了书源,用书源id获取了章节列表,最终获取章节内容,过程还是比较清晰的。

最终reader.js代码如下:

import * as bookReaderService from '../services/bookReader.js';

export default {  
  namespace: 'reader',
  state: {
    chapter: {},            // 当前章节
    chapterList: [],        // 章节列表
    bookSource: [],         // 源
    book: {
      currentChapter: 0,  // 当前章节
      currentSource: 0,   // 当前源
    },
  },
  reducers: {
    save(state, { payload }) { return { ...state, ...payload }; },
  },
  effects: {
    *getSource({ query }, { call, put }) {
      const { data: bookSource } = yield call(bookReaderService.getSource, { query });
      yield put({ type: 'save', payload: { bookSource } });
      yield put({ type: 'getChapterList', query: { id: bookSource[1]._id } });
    },
    *getChapterList({ query }, { call, put, select }) {
      const { data: { chapters } } = yield call(bookReaderService.getChapterList, { query });
      yield put({ type: 'save', payload: { chapterList: chapters } });
      yield put({ type: 'getChapter', query: { link: chapters[0].link } });
    },
    *getChapter({ query }, { call, put }) {
      const { data: { chapter } } = yield call(bookReaderService.getChapter, { query });
      yield put({ type: 'save', payload: { chapter } });
    },
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname, query }) => {
        if (pathname === '/reader') {
          dispatch({ type: 'getSource', query });
        }
      });
    },
  },
};

到目前为止,我们仅仅只是在调试工具中看到了结果,却从来没有在页面上体现内容。年轻司机们不要心急,我们离胜利仅有一步之遥~

我们打开routes/Reader.js,先将redux数据传到组件中:

function Reader({ chapter }) {  
  return (
    <div className={styles.normal}>
      {chapter.title}
      {chapter.body}
    </div>
  );
}

function mapStateToProps(state) {  
  const { chapter } = state.reader;
  return {
    chapter,
  };
}

WX20170429-162553@2x

不出意外的话,章节标题和内容就正常显示出来啦。 这一节就到这里咯,下一节我们就让这款阅读器变的好用起来吧~[]~( ̄▽ ̄)~*

遇到问题的年轻司机们不要气馁,好好检查代码,没有错误才能继续下一步哦。