iReader v2

Online: iReader.liumin.me

Github:github.com/liumin1128/…

Front-end technology communication QQ new group: 648060073 (fire stealing)


Unexpectedly, I hu Hansan back!

Remember that I didn’t graduate when I wrote this last time, but now THAT I’m out of school, it’s time to fill in the hole I left behind.

The last version received no (Gen) less (Ben) and no (MEI) feedback (you), most of the children’s shoes feel very fun, but the experience is not good, slow loading, lag, etc. So I just implemented the core function of reading books this time, and spent my time optimizing the experience. In this article, I try not to keep a running account, less nonsense, if there is a mistake welcome correction, exchange.

let’s go

This time, I did not use any ready-made handstand tools and UI framework, because this project is relatively small, and I still hope to go through the process by myself as much as possible if time permits.

React15.6, React-Router4, Redux, redux-Saga, react15.6, React-Router4, Redux, Redux-Saga There were some minor glitches in the process, such as hot updates, DLL dynamic link libraries, preact incompatibilities, and incompatibilities with the latest version, but these have been fixed by the community.

The initialization project, which is the development environment mentioned above, has been packaged into the scaffolding tool CAT-CLI for my own use. This is a very simple and easy to use scaffold tool, which is full of dry goods, you can enjoy it.

NPM I -g cat-cli cat-cli new iReader // Create a project, default use webpack3 template CD ireader NPM I NPM run DLL // build a dynamic link library NPM start // start the project, Default port 8000 // Please be sure to read this paragraph, because this tool does not have any hints...Copy the code

Design and implementation of store

First to implement the reader part, with last experience and combined with the existing API, we can sum up three core concepts of an e-book reader: book source, chapter list and chapter content. To change source is to switch between book source and chapter, which is to switch between chapter list. We only need to record the current book source and chapter to completely save the user’s reading progress. As for the details of the book, we also need to know which book we are currently reading.

“Reader” stands for “reader” and “current book”, and we’re going to skip the good books here, for obvious reasons. ╮( ̄▽ ̄)╭

reader

Const initState = {currentSource: 1, // currentChapter subscript source: [], // source list chapters: [], / / section list chapter: {}, / / the current section detail: {}, / / book details}; function reader(state = initState, action) { switch (action.type) { case 'reader/save': return { ... state, ... action.payload, }; case 'reader/clear': return initState; default: return { ... state, }; }}Copy the code

A bookcase

We don’t want to be able to read only one book, we want to be able to quickly switch between multiple books, not only save reading progress (current source and current chapter), but also read data from the cache and filter out unnecessary server requests.

To achieve this function, we can imitate the real bookshelf: the reader mentioned above is the book currently being read, which is a complete individual containing all the information about a book, and the bookshelf is a collection of many such individuals. Therefore, the action of switching books is actually the process of putting the books back on the shelf and taking out a book from the shelf. If the book is found in the shelf, it will be directly taken out and then get all the data of the book read last time. If the book is not found, it will get and initialize the reader from the server.

function store(state = {}, action) { switch (action.type) { case 'store/put': If (action.key) {return {... state, [action.key]: { ... state[action.key], ... action.payload, }, }; } else { return { ... state, }; }} case 'store/save': // initialize bookshelf return {... state, ... action.payload, }; Case 'store/delete': // delete books return {... state, [action.key]: undefined, }; Case 'store/clear': return {}; default: return { ... state, }; } } export default store;Copy the code

effects

Getting access to the book, arguably the most central feature of the project, started with only two or three lines and soon became very lengthy. In fact, this method is a bit misnamed to call the source, should be called the book, the code is generally written, let’s think of console as a comment. The main function is to put the current reading books back on the shelf and take out the new books mentioned above. And this method is only called when reading a new book.

The case to be considered is basically that the user opens the app for the first time and does not currently read the book. At this time, the user can directly obtain the book source and proceed to the next step. When the user is already looking at a book, and switch to the same book, directly return, if another book, the current data, together with the book information package back to the shelf, of course, before this to check whether there is a book in the shelf, take out, if not, continue to obtain the book source. Note that instead of using an array, the book ID is stored in the bookshelf as a key value, which makes it easy to fetch and find.

It is important to note that the project is a Web application by nature, and users can go to any page from the URL, so be prepared for exceptions such as no book details.

Access to the source

function* getSource({ query }) { try { const { id } = query; const { reader: { id: currentId, detail: { title } } } = yield select(); if (currentId) { if (id ! == currentId) { const { reader, store: { [id]: book } } = yield select(); Console. log(' put ${title} back on the shelf '); yield put({ type: 'store/put', payload: { ... reader }, key: currentId }); yield put({ type: 'reader/clear' }); If (book && book.detail && book.source) {console.log(' Fetch ${book.detail. Title} 'from bookshelf); yield put({ type: 'reader/save', payload: { ... book } }); return; } } else { return; } } let { search: { detail } } = yield select(); if (! Detail. _id) {console.log(' details don't exist, go get them '); detail = yield call(readerServices.getDetail, id); } const data = yield call(readerServices.getSource, id); Console. log(' get ${detail.title} 'from the network); yield put({ type: 'reader/save', payload: { source: data, id, detail } }); Console. log(' read: ${detail.title} '); yield getChapterList(); } catch (error) { console.log(error); }}Copy the code

Chapter List & Chapter Content Getting the chapter list and chapter content is easy with a little exception handling.

function* getChapterList() { try { const { reader: { source, currentSource } } = yield select(); If (currentSource >= source.length) {console.log(' What the ghost? How did you make the mistake '); yield put({ type: 'reader/save', payload: { currentSource: 0 } }); yield getChapterList(); return; } const {_id, name = 'source'} = source[currentSource]; Console. log(' book source: ${name} '); const { chapters } = yield call(readerServices.getChapterList, _id); yield put({ type: 'reader/save', payload: { chapters } }); yield getChapter(); } catch (error) { console.log(error); } } function* getChapter() { try { const { reader: { chapters, currentChapter } } = yield select(); const { link } = chapters[currentChapter || 0]; const { chapter } = yield call(readerServices.getChapter, link); If (chapter) {console.log(' ${chapter. Title} '); yield put({ type: 'reader/save', payload: { chapter } }); window.scrollTo(0, 0); } else {console.log(' section fetching failed '); yield getNextSource(); } } catch (error) { console.log(error); }}Copy the code

In the source implementation

As a core function, this has to be there. In the last version, we have implemented manual switching of book sources in the list, but the experience is not very good, because the user does not know that the book source is available, it is possible to switch 34 book sources in a row, so this time we simply do a wise (SHA) energy (GUA) change of source.

Swapping is simply manipulating the pointer that marks the source of the book. It’s easy, but what we care about is when to swap. After testing, it was found that there was almost no problem in the step of obtaining the chapter list, and the error basically occurred in the step of obtaining the chapter. Therefore, we just need to make a little judgment in the section list to implement automatic source switching. The method of switching sources is as follows.

function* getNextSource() { try { const { reader: { source, currentSource } } = yield select(); let nextSource = (currentSource || 1) + 1; console.log(nextSource); If (nextSource >= source.length) {console.log(' no available source, switch back to good source '); nextSource = 0; } console.log(' trying to switch to book source: ${source[nextSource] && source[nextSource].name} '); yield put({ type: 'reader/save', payload: { currentSource: nextSource } }); yield getChapterList(); } catch (error) { console.log(error); }}Copy the code

The effect is as follows, when the first book source error we automatically jump to the next book source, very convenient.

Switch section

It’s very simple, just do a little exception handling.

function* goToChapter({ payload }) { try { const { reader: { chapters } } = yield select(); const nextChapter = payload.nextChapter; If (nextChapter > chapters.length) {console.log(' there is no nextChapter '); return; } if (nextChapter < 0) {console.log(' no previous chapter '); return; } yield put({ type: 'reader/save', payload: { currentChapter: nextChapter } }); yield getChapter(); } catch (error) { console.log(error); }}Copy the code

The above is basically a complete implementation of the core of the reader, as for the search and details page, there is no space to go into details.

The UI parts

If you have seen the last version, you should know that the last version uses the Material – UI. But it was too heavy, and we wanted the project to be lightweight and efficient, so we decided to design the UI ourselves. It wasn’t a lot of work.

Home page

The home page is rather tangled. It used to put a lot of Gaussian blur and animation which thought it was cool. However, too many effects would reduce the experience.

The top half is the book currently read, showing only some key information. The lower part is the bookshelf, which stores the past reading progress.

! [layer 2] (ooi7vpwhj.bkt.clouddn.com/ layer 2. PNG)

Get the data from redux

const { detail } = state.reader; const list = state.store; Const store = object.keys (list).map((id) => {return list[id]? list[id].detail : {}; }). The filter ((I) = > {/ / filter out the abnormal data and reading the return current i. _id && i. _id! == detail._id; });Copy the code

reader

Ok, for a long time, finally see the original, this is the core of the reader page, there is no design, is the pursuit of simple and easy to use.

The body part is the native body, so it scrolls smoothly. Notice how the data provided by the API is displayed in React. The code is very short, and the general idea is to convert the newline character as a basis to an array display, which makes it easy to set CSS styles.

export default ({ content, style }) => (<div className={styles.content} style={style}>
  { content && content.split('\n').map(i => <p>{i}</p>) }
</div>);Copy the code

After a bit of experimentation, the head can be retracted, showing the current book and chapter, and a close button. Implemented based on the React-Headroom component.

For simplicity, we put the menu at the bottom of the page so it’s easy to scroll to the bottom and click on the next chapter, but it’s a bit inconvenient to scroll to the bottom if you just want to set things up.

There are only four menus: Settings, chapter list, previous chapter and Next Chapter. Click Settings and a pop-up box will pop up, supporting skin change and font size adjustment, these are just basic, have time to do brightness adjustment automatic page turning and voice reading bar. The implementation method is very simple, post this code you must understand in seconds.

This.stopevent = (e) => {// Prevent bubbles between composite events. / / stop the synthesis between events and the document on the outermost layers of the bubbling e.n ativeEvent. StopImmediatePropagation (); e.preventDefault(); return false; };Copy the code

The chapter list is more mei plus you, so pay a little attention to how the current chapter is displayed in the list. I use the anchor link to achieve, and with a SIDER component, a monk to pass thousands of chapters jump up is also very easy.

this.skip = () => {
      setTimeout(() => {
        document.getElementById(this.range.value).scrollIntoView(false);
      }, 100);
 }Copy the code

In the skin

Say very good implementation, is nothing more than a preset set of theme parameters, need which point that.

export const COLORS = [ { background: '#b6b6b6', }, { background: '#999484', }, { background: '#a0b89c',}, {background: '#cec0a4',}, {background: '# b5b2be ',}, {color: 'rgba(255,255,255,0.8)', background: '# 011721}, {color:' rgba (255255255,0.7), background: '# 2 c2926'}, {background: '# c4ada4,},];Copy the code

Maintain a setting field in redux for user Settings. Get it in the reader and set it to a theme.

function mapStateToProps(state) { const { chapter, currentChapter = 0, detail } = state.reader; const { logs } = state.common; return { logs, chapter, detail, currentChapter, ... state.setting, }; } "" javascript changes the skin to save the new data to redux to implement the skin function. This. Setting = (key, val) => {this.props. Dispatch ({type: 'setting/save', payload: {[key]: val, }, }); }; / / adjust font size enclosing setStyle = (num) = > {const fontSize = this. Props. The style.css. FontSize + num. this.props.dispatch({ type: 'setting/save', payload: { style: { ... this.props.style, fontSize, }, }, }); };Copy the code

Delete the implementation

In order not to add a new UI, we decided to delete with a long press. But the list needed to support not only long and short presses, but also scrolling, and I didn’t want to use a heavy-duty library like hammer.js, so I wrote a component by hand that supported both long and short presses. It’s just fine. It’s not gonna get any better.

export default ({ children, onPress, onTap }) => { let timeout; let pressed = false; let cancel = false; function touchStart() { timeout = setTimeout(() => { pressed = true; if (onPress) onPress(); }, 500); return false; } function touchEnd() { clearTimeout(timeout); if (pressed) { pressed = false; return; } if (cancel) { cancel = false; return; } if (onTap) onTap(); return false; } function touchCancel() { cancel = true; } return (<div onTouchMove={touchCancel} onTouchCancel={touchCancel} onTouchStart={touchStart} onTouchEnd={touchEnd} > {  children } </div>); };Copy the code

As for the long popover UI, I’m too lazy to design it. It won’t work in a short time, so I’ll stick with Sweet-Alert2. It’s a great plugin.

At this point we have all the functionality and UI implemented.

To optimize the

Mobile optimization

<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0; name="viewport" /> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta name="apple-mobile-web-app-capable" Content ="yes"> // This is important and can be added to the home screen in Safari on ios. This setting will enable full screen mode. <meta name="apple-mobile-web-app-status-bar-style" content="black"> <link rel="apple-touch-icon-precomposed" href="icon.png"/> <link rel="apple-touch-startup-image" sizes="2048x1496" href=""> <link rel="apple-touch-icon" href="icon.png"/>Copy the code

css

* { user-select: none; // Forbid the user to select text-webkit-appearance: none; // Change button default style -webkit-touch-callout: none; // Disable system default menu} INPUT {user-select: auto; -webkit-touch-callout: auto; // Unblock the input component, otherwise it can't be typed}Copy the code

fastclick

import FastClick from 'fastclick';
FastClick.attach(document.body);Copy the code

You know, removing the 300 ms delay on the mobile side, but that brings other issues like long press exceptions, scroll exceptions and so on. Because sliding touchMove triggers the TouchEnd event, you need to unmount the action on TouchStart first.

The volume

In the last version, the project was 700K + when packaged, which was a terrible first load speed. As mentioned earlier, the volume has been greatly reduced since the abandonment of various frames and animations. React, react-Router, redux, and redux-saga depend on things like react, react-Router, redux, and redux-saga. The good news is that we can use Preact instead of React, saving around 120KB.

Simply install Preact and set the alias. There are several small pits here, one is the third sentence of alias, it took a long time to find it under an issue, without which it cannot run. Second, preact and React-hot-loader are incompatible, and hot updates will fail if used together. Third, Preact is still incompatible with React, which needs to be verified carefully.

npm i -S preact preact-compat resolve: { alias: { react: 'preact-compat', 'react-dom': 'preact-compat', 'preact-compat': 'preact-compat/dist/preact-compat',},Copy the code

After a series of optimizations and gzip, the project index.js was reduced to 74KB, a tenth of the size of the previous version.

The last

All the data in the project comes from the book chasing artifact, thank you very much!! This project is only for learning front-end technology in actual combat, please do not use it. Maybe next the author will make a music player? Experienced friends can join together. Making you understand


Online: iReader.liumin.me

Github:github.com/liumin1128/…

Big ye ~ often come to play, welcome to contribute code ~

cnpm i -D babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react webpack Webpack-dev-server html-webpack-plugin eslint@^3.19.0 eslint-plugin-import eslint-loader eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react babel-plugin-import file-loader babel-plugin-transform-runtime babel-plugin-transform-remove-console redux-devtools style-loader less-loader css-loader postcss-loader autoprefixer rimraf extract-text-webpack-plugin copy-webpack-plugin react-hot-loader@next less cnpm i -S react react-dom react-router  react-router-dom redux react-redux redux-saga material-ui@next material-ui-icons cnpm i -S preact preact-compat react-router react-router-dom redux react-redux redux-saga proxy: { '/api': { target: 'http://api.zhuishushenqi.com/', changeOrigin: true, pathRewrite: { '^/api': '' }, }, '/chapter': { target: 'http://chapter2.zhuishushenqi.com/', changeOrigin: true, pathRewrite: { '^/api': '' }, }, },Copy the code