The writer is the front end team of China Literature Group

Original statement: This article is produced by members of Yuewen front-end team YFE, please respect the original, reprint please contact the public number (ID: yuewen_YFE) for authorization, and indicate the author, source and link.

preface

After more than three months of intensive development, China Literature Group’s “Yuanqi Reading” APP is finally available on major APP stores. Most of the function modules of “Yuanqi Reading” APP are developed based on React Native. During the whole development process, the front end team went through many pits of React Native and accumulated a lot of practical experience to share with everyone.

1. Business background and technology selection

Before using React Native (hereinafter referred to as RN), like most teams in the industry, our APP development mode adopted the Hybrid development mode of H5 embedded in the client (iOS/Android). At the beginning, in addition to using a mature offline package scheme to manage static resources, we also did a lot of optimization work on the first screen loading experience. However, we found that the experience and performance data of H5 online were still far behind the original one, so we decided to introduce a new scheme.

RN and Weex are two relatively mature Hybrid solutions in the industry, which can basically meet our needs:

  • User experience: Compared to H5 pages, RN and Weex have a great improvement in user experience, and the experience is almost native
  • Manpower cost: Compared with the client side, a set of RN and Weex code can run on both sides of iOS and Android, and the code reuse rate is high
  • Flexible release: Both RN and Weex have hot update capabilities

Finally, we chose RN as the solution, mainly considering several factors:

  • Community status: Compared to Weex, RN has better community activity and ecosystem around Facebook React
  • Dacang endorsement: Tencent, JD.com, Baidu and Ctrip all have large products running online
  • Team status: As early as the first half of 2017, China Literature front-end team has selected React as the main R&D technology stack of our front-end product line, and most members can handle React

Ii. Application Scenarios

About 70 percent of the application scenarios in the APP “Vitality Reading” are developed using RN. In the pages that users can see, except for bookshelf, registration and login and reading engine, almost all other modules are developed using RN. “Yuanreading” APP has been a super-large RN application among large domestic products. You are welcome to search for “Gengenread” in any app store (iOS, Android) to download the experience.

▲ Novel bookstore

▲ Comic Book City

Bring the vitality circle

▲ Cartoon details

Bring list

Bring about classification

Third, navigation management

For the development of RN, the pre-planning of navigation is very important, which usually needs to be considered in advance when building a project. In terms of navigation component selection, React-Navigation is a good choice and we hope that React-Navigation will be more versatile in business scenarios.

1. Unify the jump rules

Hop between Native and RN is the most common requirement. With a uniform URL, you can easily jump between Native and RN by maintaining a sitemap and implementing an open interface.

React-navigation is react-navigated using routeName + params, so you need to make a little adjustment before calling router.getStateForAction:

// Fix action: Allow navigate/push/reset actions to send urls
if (isPushLikeAction(action) || isReplaceAction(action)) {
  if (isRouteUrl(action.routeName)) {
    // Use the path-to-regexp library to determine the routeName + params corresponding to the URL
    const route = parseRouteByUrl(action.routeName) 
    if (route) {
      action.routeName = route.name
      action.params = route.params
    }
  }
}
Copy the code

2. Realize the 404 jump

In Web development, a 404 page is a very common logic. In the same way as above, RN can implement this:

// Fix action: Navigate to the defined notFoundRouteName when navigate/push/replace goes to an unknown routeName
if (isPushLikeAction(action) || isReplaceAction(action)) {
  // Fix action: provide 404 capability
  if (allRouteNames.indexOf(action.routeName) === - 1) {
    constoldAction = { ... action } action.routeName = notFoundRouteName action.params = {action: oldAction }
  }
}
Copy the code

3. Control the page life cycle

In the process of project development, we often encounter such a need to refresh the data of the original page after returning to the original page, such as after logging in, entering the details page and returning to the list page after completing some operation.

When The React-Navigation project started, it was version 0. X, and only onNavigationStateChange + Context was used to focus/blur pages. 1. After version X, we can listen for didFocus or didBlur events using the built-in addListener method.

4. Optimize the page to open twice

In normal use, it needs to frequently switch from RN to Native or from Native to RN, so there will be multiple RN pages (root component), and the second root component needs to be positioned to the specified page when it is initialized. So with the Native convention, tell RN what page to navigate to by initialRouteUrl or initialRouteName + initialRouteParams:

const navigator = getActiveNavigator() // You need to maintain a Navigator stack globally
let nextState = originGetStateForAction(action, state) // Call the original getStateForAction to get the new/initialized state

if (navigator) {
  const { initialRouteName, initialRouteParams, goBackOnTop } = navigator.props // Read the props of the navigator

  if (isInitAction(action)) {
    // Support initialRouteName & initialRouteParams to the corresponding page
    if (initialRouteName) {
      const initialActionPayload = { routeName: initialRouteName, params: initialRouteParams }
      const initialAction = NavigationActions.navigate(initialActionPayload)
      nextState = router.getStateForAction(initialAction, nextState) 
       if(! isTopNavigator() && nextState.index >0) {
        // Save the last navigate page when a non-level-1 RN instance has two pagesnextState = { ... nextState,index: 0.routes: nextState.routes.slice(- 1),}}}}else if (isBackAction(action)) {
    // On the first page and not the first Navigator, call goBackOnTop to close RN
    if(isTopScren(state) && ! isTopNavigator( ) &&typeof goBackOnTop === 'function') { 
      goBackOnTop()
      if (nextState === state) {
        // Prevent Android's physical return key from causing App exitnextState = { ... nextState } } } } }return nextState
Copy the code

5. Status Local storage

The React-Navigation component added a local store of state in 2.x. After reload, you can redirect to the previous page directly, but there are two points to note:

  • In “vigour reading” this business scenario has multiple root components, each of which is a root componentrootNavigatorThere needs to be a mark distinction, it is recommended to distinguish by index
  • After a page error (red screen), to avoid the reload remaining on the current error page, you can use thecomponentDidCatchIt clears local storage

Iv. State management and data persistence

In Keybook, we often need to cache the user’s information, the details of the books they have read, the messages they have received, etc., so that the user can avoid a blank screen or abnormal situation when accessing Keybook offline, and can also achieve “second to open”.

For example, when the user first opens the book details page, the book details are cached. The second time to open again, you can achieve the second open effect. The second open effect can be seen below:

▲ Jump to the details page

We chose to use redux and Redux-persist together for data sharing and persistent caching.

1, the story

The main reason for choosing Redux is to share data. With the characteristics of redux single data flow, each operation can be traced, which makes it easier to troubleshoot problems.

When you’re writing Redux, you might think you’re going to need a lot of boilerplate code. The redux-Actions library is recommended here to help reduce some of the code. Here’s a simple example:

//
export default (state = {}, action) => {
 switch (action.type) {
  case INCREASE:
   return{... state,total: state.total + 1}
  case DECREASE:
   return{... state,total: state.total - 1}
  default:
   return state
 }
}

// Through handleActions
import { handleActions } from 'redux-actions'

export default handleActions({
 [INCREASE]: state= > {
  ...state,
  total: state.total + 1
 },
 [DECREASE]: state= > {
  ...state,
  total: state.total - 1
 }
}, initialState = {})
Copy the code

2, the story – persist

Redux-persist subscribing to the store triggers the store operation when the store changes. So when we operate on the store, the data will be updated locally.

When developing a project, you may find that some of the data we share in the Store may not need to be cached locally. For example, the search results page, because each search keyword is different, the results are different, such data cached locally does not make sense. So how do we control that some data is not cached locally?

Redux-persist supports the configuration of whitelist and whitelist. This means that only the data in the whitelist is persisted or the data in the blacklist is not persisted. This allows you to configure the whitelist and whitelist as required to determine which data should be cached locally and which data should not be cached. Such as:

import { createStore, applyMiddleware, combineReducers } from 'redux'
import { persistReducer } from 'redux-persist'
import thunkMiddleware from 'redux-thunk'
import storage from 'redux-persist/lib/storage'

const rootPersistConfig = {
 storage,
 key: '* * *'.blacklist: ['* * *'] / / blacklist
}

const enhancer = applyMiddleware(thunkMiddleware)
export const store = createStore(persistReducer(rootPersistConfig, rootReducer), enhancer)
export const persistor = persistStore(store)
Copy the code

5. Performance optimization

A compelling reason for using React Native instead of WebView-based tools is to achieve 60 frames per second and a native look and feel to your apps. Where possible, we would like for React Native to do the right thing and help you to focus on your app instead of performance optimization, but there are areas where we’re not quite there yet, and others where React Native (similar to writing native code directly) cannot possibly determine the best way to optimize for you and so manual intervention will be necessary. We try our best to deliver buttery-smooth UI performance by default, but sometimes that just isn’t possible.

In the RN documentation, there is a performance reading that says, “Currently the RN does not decide how to optimize for you (even if you write in native code), so human intervention is still necessary.” We do spend a lot of effort on performance optimization.

1. First screen optimization

For those of you who have run RN programs, the first time you enter an RN page, there will be a brief white screen, as fast as tens of milliseconds, as slow as 1 or 2 seconds, depending on the performance of the terminal, it is the worst on low-end Android devices, and after you exit and enter again, you will still have this white screen. We implemented several optimization strategies:

1) Preload the Bundle

When the client starts, the bundle of RN is preloaded, and we find that the white-screen operation time is much shorter, especially on Android devices. But it’s not perfect. We’ll still see a brief white screen.

2) Optimize splash screen logic

Most apps must have a splash screen before entering the home page. We can take advantage of this business scenario, and let RN program load under the splash screen until the loading is completed, and inform the client to close the splash screen through Bridge, so as to solve the problem of white screen more skillfully.

Bring about before

Bring about after

2. Interaction first

When a JavaScript thread is doing more than one thing at a time, it’s easy to drop frames in the thread, resulting in page lag and slow animation transitions, which can be optimized using the “interaction first” principle.

1) Perform user-aware operations preferentially, such as page scenario switching

For example, the page transition scenario. We can put the page logic InteractionManager. RunAfterInteractions callback, so we can ensure the transitions of the animation, then our page logic, has been very good transitions caton.

2) Initialize the page to render as few components as possible

When we render a page to the user, must be in the shortest possible time to make the user feel show page has finished, so we in the display page for the first time, you can prioritize the fixed holder information, cooperate with loading or skeleton diagram layout uncertain parts, meanwhile we silently behind a request (encounter complex page, You can split multiple asynchronous requests), but the whole process is to make sure the page is visible and then complete.

3. Long list optimization

▲ Subtree of components

This is a subtree of a component. For each of these components, SCU indicates what is returned for shouldComponentUpdate, vDOMEq indicates whether the React element to be rendered is equal to the original element, and finally, the color of the circle indicates whether the component needs to be rerendered.

It’s not a big performance problem in React if you only render this component subtree once. However, for paging long lists that require hundreds or thousands of times of rendering, a large amount of overhead will be spent on vDOM generation and Diff, which directly leads to serious performance problems of long lists in RN. So what do we need to do to improve? Take a look at this flowchart of component update rendering:

▲ Component update process

When a component’s state or props changes, it enters the lifecycle function shouldComponentUpdate, When shouldComponentUpdate returns true, the render method is called to generate the Virtual Dom, which is then compared to the old Virtual Dom, and finally determines whether to update. Therefore, we can clearly see that THE Diff of SCU and Virtual Dom is the key to the Dom update. Therefore, we optimized these two points respectively:

1) ControlshouldComponentUpdateUpdate logic of

As you can see from the figure above, if shouldComponentUpdate returns false, then the application can skip generating the Virtual Dom and then the Diff, which is a considerable optimization for a large list of scenarios. For example, at present we have a list of 1000 data, pull down to load 20 new data, if not using shouldComponentUpdate control, will render the previous 1000 data again, And in shouldComponentUpdate control good update logic, just need to render the latest 20, is not a great promotion! Be careful with shouldComponentUpdate though, you must consider all the logic that affects updates. Otherwise, there will be times when you really need to update and you can’t.

Let’s take a specific example. The scene is the classification list page in the APP. We print log in render for each list item and count the number of times it entered render. ShouldComponentUpdate () ¶ shouldComponentUpdate (); shouldComponentUpdate ();

shouldComponentUpdate (nextProps, nextState) {
  return true
}
Copy the code

Bring about before

Let’s see what happens when we filter out unnecessary renderings using the image’s URI address in shouldComponentUpdate:

shouldComponentUpdate (nextProps, nextState) {
  if (nextProps.imgSrc.uri === this.props.imgSrc.uri) {
    return false
  } else {
    return true}}Copy the code

Bring about after

It is clear from the console on the left that the filter renders only the latest 20 items on whichever page is loaded, reducing a lot of unnecessary rendering. Compare the time it takes to load a thousand pieces of data under the same conditions:

shouldComponentUpdate

2) Increments the unique key value during array traversal

If updates are inevitable, you can only find ways to improve the Diff efficiency of the Virtual Dom. We can add a unique key value to each item when traversing the number group, so that in the Diff phase, we can know exactly which sub-components to operate and improve the efficiency of Diff.

4. Animation optimization

The proper use of animation is of great help to the improvement of APP experience. However, when we applied animation, we found that in some scenes, there would be lag and frame drop phenomenon. The essential reason is that JavaScript is single-threaded. If the thread is running some heavy tasks, it may affect the performance of animation. Here are a few ways to put the animation as far as possible in the original:

1) Use LayoutAnimation

For one-time animations, it is recommended to use LayoutAnimation, which makes use of the native Core Animation, so that the Animation is not affected by the JS thread and the main thread drop frame.

2) Use setNativeProps

The setNativeProps method allows us to directly modify the properties of a native view-based component without having to use setState to re-render the entire component tree. Avoids the overhead of rendering component structures and synchronizing too many view changes.

3) Use native drivers

In the Animated setting, add the useNativeDriver field and set it to true to leave the execution of the animation to native processing.

6. Release updates

Thanks to the rapid spread of the Internet, things are moving faster and faster, and the ability to iterate quickly and make trial and error is particularly critical. As developers, our challenge is to get the completed features on line quickly. Here’s how we did it:

1, publish,

We chose Jenkins as the automated deployment solution. Automatic packaging is implemented by configuring the packaging script in Jenkins, and the bundle of RN is printed to the specified location. In this way, manual packaging is not needed before each packaging, which greatly improves efficiency.

2. Hot update

Due to the relatively large cost of releasing a new version on the Native end, RN’s hot update capability has become a great highlight. As long as the latest bundle is released to the server, the app in the user’s hand can automatically download the bundle from the remote end, and then update it without awareness, which is particularly convenient.

After some research, we finally chose Microsoft CodePush. It allows RN and Cordova developers to directly deploy mobile app updates to the cloud service on users’ devices, and it also provides an open source RN version. Specific access to the tutorial can be viewed on the official website, here is not a repeat. Here are some points to pay attention to:

1) Register the app

When registering an app in CodePush, it is necessary to distinguish between iOS and Android, such as Appname-ios and Appname-Android, and to release on different platforms.

2) Key configuration: Staging and Production

When registering the app, a set of Deployment keys is returned for the Production and Staging environments (you can also customize the Deployment key names later), which will be used when integrating the CodePush SDK. Production key of the Production environment, Staging key of the test environment. This allows you to update packages for different environments separately. If you want to view the Deployment key table for your app, use the following command:

code-push deployment ls <appName> -k
Copy the code

3) RN access CodePush

Accessing CodePush on the RN side is as simple as adding a few lines of code to the root file. CodePush parameters can be configured differently depending on the environment. The code looks something like this:

import React, { Component } from 'react'
import codePush from 'react-native-code-push' / / introduce codePush
       
  const codePushOptions = __DEV__ ? {
    updateDialog: true.// Display the update popover
    installMode: codePush.InstallMode.IMMEDIATE // Update immediately (interrupts user action)}, {// Next time the app switches from background to foreground, check for updates and download the latest package
    checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
    // Replace with the latest package the next time you restart
    installMode: codePush.InstallMode.ON_NEXT_RESTART
  }
       
  @codePush(codePushOptions)
  export default class App extends Component { render() { ... }}Copy the code

CheckFrequency and installMode are configurable and can be configured as required.

4) Version control

The default version is the current package version (three-digit version number). To specify the version number, you can add -t to the hot update command, followed by the version number to be updated.

Vii. Abnormal monitoring

We made use of Tencent’s Bugly platform to monitor online anomalies. The Bugly platform provides developers with exception reporting and operational statistics:

  • Bugly will report operation errors, crashes and delays, and provide corresponding data statistics and alarm mechanisms, so that we can perceive online exceptions as soon as possible, and master the overall stability and smoothness of user side operations.
  • The Bugly platform provides log reporting to help locate faults.
  • The Bugly platform can also compare changes in abnormal data by version, model, or system.

For example, the following figure shows the Crash rate statistics:

Crash can also be analyzed in detail by system, device, and APP version.

You can also count the Top questions that affect the most users:

Eight, some pits and tips

Over the course of several months of development, we ran into a number of pitfalls and found some tips that worked or went unnoticed. Here are some of them:

1, the pit

1) Potential memory leak Bug of Image component on Android

In Android, if you load an image that’s much bigger than the container, and all of a sudden the memory goes up, and you swipe up and down on that image, and the app crashes out of memory, how do you solve that? In fact, the method is very simple. You only need to set the resizeMethod attribute of the Image component to resize, as shown in the figure below:

▲ Description of the resizeMethod attribute of Image

2) useInteractionManager.runAfterInteractionsMatters needing attention when

We know InteractionManager. RunAfterInteractions callback is needed to complete the animation before the execution, we found such a program bug, after click a button, You will never get into the runAfterInteractions callback. After a review, it turned out that we had executed an infinite loop animation (loading effect) and had not closed it, so we would never get into the runAfterInteractions callback. So everyone in the development of the cycle animation should pay attention to processing.

3) Using FlatList lists causes page skipping problems

FlatList has an optimized property called getItemLayout. Setting this property can greatly improve the efficiency of rendering your list if you have a fixed height. The problem we encountered was that we also set this property when the height was not true, which caused the actual height in the final rendering to be inconsistent with our preset value, which caused a jump. So, never set the getItemLayout property if you are unsure of the height.

▲ Sliding is not smooth, there will be a jump

▲ Normal sliding

4) Repeat click in a short time to appear multiple same page problems

It’s not just an RN problem, it should be unavoidable at all ends. So usually there are fixes for this in the Navigation libraries of various technology stacks, and our initial expectation was that React Navigation would definitely fix this internally, but it didn’t. If the address of the next route is the same as the address of the last route, it will not be handled:

▲ Repeat jump

Bring resolved

function isInCurrentState (state, nextState, routeName) {
  if(nextState && nextState.routeName === routeName && ! deepDiffer(state.params, nextState.params)) {return true
  }
  if(nextState && nextState.routes) {
    return isInCurrentState(state.routes[state.index], nextState.routes[nextState.index], routeName)
  }
  return false
}

const nextState = originGetStateForAction(action, state)

// Avoid repeated jumps
if (nextState && action.type === StackActions.PUSH) {
  if(isInCurrentState(state, nextState, action.routeName)) {
    return state
  }
}
Copy the code

2. Tips

1) Two options you may not know about in the iOS emulator

  • Open the virtual keyboard: When we are developing input related scenarios, the keyboard is not enabled on the iOS simulator by default, so we need to use theHardWare->Keyboard->Toggle Software KeyboardTo switch;
  • Slow animation switch: Many students encounter this problem, do not know which button, any operation in the simulator than very slow, all kinds of restart, clear the cache is invalid. This is due to the fact that we accidentally triggered the shortcut key that turned on the slow animation mode that can be used inDebug->Slow Animations(The shortcut key iscommand+T).

2) The original RN and the native communication can also be synchronous

We know that the communication between RN and native is asynchronous, but if it is some global constants (environment variables, version information, etc.), in fact, it can be hung directly on the NativeModules when starting RN in a synchronous way, which is very convenient to use.

3) Some interesting properties of the Image component

  • DefaultSource (iOS Only)To implement a default graph function, you need to set the default graph link for the image first, and then change the state in the callback when the image has downloaded successfully. This property does that for you, but unfortunately it only supports iOS.
  • getSizeThis API is used when we need to get the height and width of an image and then process the image logic.
  • prefetch: Force caching of images.
  • queryCache: This API gets whether the image is cached, and if so, whether it is delivered to disk or memory. This is useful for handling some caching logic, but it should be noted that there are no official annotationsAndroid OnlyWe’ve only had success on Android, not iOS.

4) Some interesting properties in the Text component

  • AllowFontScaling (iOS Only): This property controls whether to follow the system font size. If your APP layout gets out of control because of font Settings, you can consider enabling this property, but this property is only available on iOS, android needs other solutions.
  • selectableThis property can be used to enable text copying and pasting.

5) How can FlatList achieve multiple columns in one row

FlatList provides a property called numColumns. You can easily arrange multiple columns in a row by simply setting the number of columns in a row.

▲ A row with three columns

6) Debugging tools

React-native debugger is recommended. It is integrated with DevTools and React-DevTools for Chrome. It also supports Redux debugging.

7) Performance testing

You can use the built-in software of the client to perform performance detection. Xcode’s own Profile is recommended for iOS. Android Profiler comes with Android Studio.

Nine,

Although RN still has some deficiencies at present, through the practice of “Vitality Reading” project, the results show that RN meets our expectations in terms of manpower, performance and efficiency. For the best use of RN in business scenarios, we also summarize the following points:

  • Re-operation scenario: The scenario with operation requirements is suitable for implementation by RN, such as book city page, welfare page, etc
  • Rapid iteration scenarios: Functional scenarios where the functions are not sound and the product needs iterative trial and error are suitable for implementation with RN, such as Yuanquan, novel bookstore, comic bookstore, etc
  • Fixed information display scene: Information display page of fixed content, suitable for implementation with RN, such as leaderboard, adult, first-class classification page, etc
  • Long list scenario:
    • Due to the limitations of the rendering mechanism of RN list, there are a lot of pictures of unknown size in the picture + length list, so it is not recommended to implement RN. For example, scenes like Quan Jiao and wechat Circle of Friends may appear flicker white in the violent slide list;
    • A large number of known size long list of pictures, plain text long list, performance is acceptable

Whether a page is implemented using Native or RN, in addition to the staff ratio of each end, business scenario is also an important factor to consider. For example, in a new project, both Native or RN implementation of the work details page can achieve the acceptance goal. However, considering that the product scene of the work details page is very mature, and many modules interact with the core reading page more, and the experience requirements are particularly high, we and the terminal team unanimously choose Native for implementation.

Write in the last

The Airbnb and Udacity teams have recently announced that they are abandoning RN, so I don’t think anyone should worry. Many of the regulations listed by Airbnb can be optimized, or the conclusions need to be studied; Others have their own internal problems. Recently, the Facebook team announced that they are working on a major upgrade, and we are looking forward to the optimized direction of threading model, asynchronous rendering and bridging. We have reason to believe that RN will have a better future, and we hope that more students can join the family of RN through this post. Work together to build a better RN ecology.

To share more, please follow YFE: