├── .watchmanconfig ├── .gitattributes ├── .eslintignore ├── src ├── stores │ ├── index.js │ └── app-store.js ├── assets │ ├── styles │ │ └── colors-theme.js │ └── animations │ │ └── loading.json ├── common │ ├── service-error.js │ ├── code.js │ ├── constants.js │ ├── event.js │ ├── loading.js │ ├── global-error-handler.js │ ├── history.js │ ├── tab-nav.js │ └── notification-center.js ├── services │ ├── list-service.js │ └── base-service.js ├── pages │ ├── page1 │ │ └── page1.js │ ├── page2 │ │ └── page2.js │ ├── page3 │ │ └── page3.js │ ├── detail │ │ └── detail.js │ ├── home │ │ └── home.js │ └── list │ │ └── list.js ├── router.js ├── hocs │ └── loading-hoc.js └── components │ └── navi-bar.js ├── app.json ├── images-folder └── app.gif ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── antfill.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── antoutline.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── reactnativeappdemo │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── build_defs.bzl │ ├── proguard-rules.pro │ ├── BUCK │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── keystores │ ├── debug.keystore.properties │ └── BUCK ├── settings.gradle ├── gradle.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── ios ├── ReactNativeAppDemo │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.xib ├── ReactNativeAppDemoTests │ ├── Info.plist │ └── ReactNativeAppDemoTests.m ├── ReactNativeAppDemo-tvOSTests │ └── Info.plist ├── ReactNativeAppDemo-tvOS │ └── Info.plist └── ReactNativeAppDemo.xcodeproj │ └── xcshareddata │ └── xcschemes │ ├── ReactNativeAppDemo.xcscheme │ └── ReactNativeAppDemo-tvOS.xcscheme ├── .buckconfig ├── index.js ├── __tests__ └── App-test.js ├── metro.config.js ├── babel.config.js ├── .gitignore ├── package.json ├── .flowconfig ├── App.js ├── .eslintrc └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | android 2 | ios 3 | node_modules -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | import {appStore} from './app-store' 2 | 3 | export {appStore} -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeAppDemo", 3 | "displayName": "ReactNativeAppDemo" 4 | } -------------------------------------------------------------------------------- /images-folder/app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/images-folder/app.gif -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeAppDemo 3 | 4 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/antfill.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/antfill.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/antoutline.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/antoutline.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /src/assets/styles/colors-theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 控制全app的颜色 3 | * @type {{statusBarColor: string}} 4 | */ 5 | export const colors = { 6 | statusBarColor: '#23A2FF' 7 | } -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niunai2016/ReactNativeAppDemo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | console.disableYellowBox = true; 10 | 11 | AppRegistry.registerComponent(appName, () => App); 12 | -------------------------------------------------------------------------------- /src/common/service-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务报错处理 3 | */ 4 | 5 | export default class ServiceError extends Error{ 6 | constructor(code, message){ 7 | super(message); 8 | this.code = code; 9 | this.hash = Math.random() * 100000000000000000; 10 | this.signature = 'ServiceError'; 11 | } 12 | } -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/common/code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * code.js提供全局的请求服务字段处理 3 | */ 4 | export default { 5 | SUCCESS: 'SUCCESS', //请求成功 6 | REQUEST_FAILED: 'REQUEST_FAILED', //请求失败 7 | REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', //请求超时 8 | UN_KNOWN_ERROR: 'UN_KNOWN_ERROR', //未知错误 9 | TOKEN_INVALID: 'TOKEN_INVALID', //token失效 10 | SESSION_TIMEOUT: 'SESSION_TIMEOUT', //会话超时 11 | } -------------------------------------------------------------------------------- /src/services/list-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 列表页服务 3 | */ 4 | import BaseService from './base-service'; 5 | 6 | export default class ListService extends BaseService { 7 | /** 8 | * 获取列表 9 | * @return {Promise} 10 | */ 11 | async getList() { 12 | const res = await this.postJson('qryList.do', {}) 13 | 14 | return res; 15 | } 16 | } -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/stores/app-store.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx' 2 | 3 | class AppStore { 4 | @observable 5 | list = [] 6 | 7 | @observable 8 | timer = 0 9 | 10 | @action 11 | setList(data){ 12 | this.list = data 13 | } 14 | 15 | @action 16 | resetTimer() { 17 | this.timer = 0 18 | } 19 | 20 | @action 21 | tick() { 22 | this.timer += 1 23 | } 24 | } 25 | 26 | const appStore = new AppStore() 27 | export {appStore} -------------------------------------------------------------------------------- /src/common/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 提供基础配置信息 3 | * constants.js 提供如服务器地址、分页数量、设备类型、设备号、版本号等配置 4 | */ 5 | import { Platform } from 'react-native' 6 | import DeviceInfo from 'react-native-device-info' 7 | 8 | export default { 9 | serverUrl: 'http://127.0.0.1:3600/portal', 10 | pageSize: 10, 11 | deviceType: Platform.OS.toUpperCase(), 12 | deviceNo: DeviceInfo.getUniqueID().replace('-').substr(0, 12), 13 | versionName: DeviceInfo.getVersion(), //也可写死如'1.0.0' 14 | } -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativeappdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeappdemo; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "ReactNativeAppDemo"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/page1/page1.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text } from 'react-native' 3 | import NaviBar from '../../components/navi-bar'; 4 | 5 | export default class Page1 extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | Page1 12 | 13 | 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/pages/page2/page2.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text } from 'react-native' 3 | import NaviBar from '../../components/navi-bar'; 4 | 5 | export default class Page2 extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | Page2 12 | 13 | 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/pages/page3/page3.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text } from 'react-native' 3 | import NaviBar from '../../components/navi-bar'; 4 | 5 | export default class Page3 extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | Page3 12 | 13 | 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'], 3 | plugins: [ 4 | '@babel/transform-flow-strip-types', 5 | [ 6 | '@babel/plugin-proposal-decorators', { 'legacy' : true } 7 | ], 8 | [ 9 | '@babel/plugin-proposal-class-properties', {'loose': true} 10 | ], 11 | [ 12 | '@babel/plugin-transform-runtime', {} 13 | ], 14 | ['import', { 'libraryName': '@ant-design/react-native' }] 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 一个JavaScript 事件消息总线 3 | */ 4 | import NotificationCenter from './notification-center'; 5 | 6 | export default class Event { 7 | static listen(eventName, callback, observer) { 8 | NotificationCenter.addNotification(eventName, callback, observer); 9 | } 10 | static emit(eventName, params) { 11 | NotificationCenter.postNotificationName(eventName, params); 12 | } 13 | static remove(eventName, observer) { 14 | NotificationCenter.removeNotification(eventName, observer); 15 | } 16 | } -------------------------------------------------------------------------------- /src/pages/detail/detail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text } from 'react-native' 3 | import NaviBar from '../../components/navi-bar'; 4 | import history from '../../common/history'; 5 | 6 | export default class Detail extends Component { 7 | render() { 8 | return ( 9 | 10 | 14 | 15 | Detail 16 | 17 | 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import {createStackNavigator, createAppContainer} from 'react-navigation' 2 | import List from './pages/list/list'; 3 | import Detail from './pages/detail/detail'; 4 | import {TabNav} from './common/tab-nav'; 5 | 6 | function generateRoute(path, screen) { 7 | return { 8 | path, 9 | screen 10 | } 11 | } 12 | 13 | 14 | const stackRouterMap = { 15 | list: generateRoute('/list', List), 16 | detail: generateRoute('/detail', Detail), 17 | main: TabNav 18 | } 19 | 20 | const stackNavigate = createStackNavigator(stackRouterMap, { 21 | initialRouteName: 'main', 22 | headerMode: 'none' 23 | }) 24 | 25 | const Router = createAppContainer(stackNavigate) 26 | 27 | export default Router -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ReactNativeAppDemo' 2 | include ':lottie-react-native' 3 | project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android') 4 | include ':react-native-device-info' 5 | project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android') 6 | include ':react-native-vector-icons' 7 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 8 | include ':react-native-gesture-handler' 9 | project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') 10 | 11 | include ':app' 12 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /src/hocs/loading-hoc.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Dimensions, View} from 'react-native'; 3 | const { width, height } = Dimensions.get('window') 4 | import Event from '../common/event' 5 | 6 | export default function LoadingHoc(WrappedComponent) { 7 | return class ComposedComponent extends Component { 8 | showLoading(){ 9 | Event.emit('SHOW_LOADING') 10 | } 11 | 12 | hideLoading(){ 13 | Event.emit('HIDE_LOADING') 14 | } 15 | 16 | render() { 17 | const props = {...this.props, ...{ 18 | showLoading: this.showLoading.bind(this), 19 | hideLoading: this.hideLoading.bind(this) 20 | }}; 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | supportLibVersion = "28.0.0" 10 | } 11 | repositories { 12 | google() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:3.3.1' 17 | 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | mavenLocal() 26 | google() 27 | jcenter() 28 | maven { 29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 30 | url "$rootDir/../node_modules/react-native/android" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | yarn.lock 39 | npm.lock 40 | package-lock.json 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/ 53 | 54 | */fastlane/report.xml 55 | */fastlane/Preview.html 56 | */fastlane/screenshots 57 | 58 | # Bundle artifact 59 | *.jsbundle 60 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/common/loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {View, Dimensions, StyleSheet} from 'react-native'; 3 | import LottieView from 'lottie-react-native'; 4 | import LoadingAnimation from '../assets/animations/loading'; 5 | 6 | const { width, height } = Dimensions.get('window') 7 | 8 | export default class LoadingView extends Component { 9 | render(){ 10 | if(this.props.visible){ 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | }else{ 19 | return 20 | } 21 | } 22 | } 23 | 24 | const styles = StyleSheet.flatten({ 25 | wrapper: { 26 | position: 'absolute', 27 | top: 0, 28 | left: 0, 29 | width: width, 30 | height: height, 31 | backgroundColor: 'rgba(0, 0, 0, 0.2)' 32 | }, 33 | loading:{ 34 | position: 'absolute', 35 | top: height / 2 - 100, 36 | left: width / 2 - 70, 37 | width: 140, 38 | height: 140 39 | } 40 | }); -------------------------------------------------------------------------------- /src/common/global-error-handler.js: -------------------------------------------------------------------------------- 1 | import code from './code'; 2 | import Event from './event' 3 | 4 | export function handleErrors(error){ 5 | if(error && error.signature && error.signature === 'ServiceError') { 6 | defaultServiceErrorHandler(error); 7 | }else{ 8 | defaultErrorHandler(error); 9 | } 10 | } 11 | 12 | function defaultServiceErrorHandler(error){ 13 | if(error && error.code === code.SESSION_TIMEOUT){ 14 | Event.emit('GLOBAL_ERROR', { 15 | type: 'SESSION_TIMEOUT' 16 | }) 17 | }else if(error && error.message) { 18 | Event.emit('GLOBAL_ERROR', { 19 | type: 'SERVICE_ERROR', 20 | message: error.message 21 | }) 22 | }else { 23 | Event.emit('GLOBAL_ERROR', { 24 | type: 'SERVICE_ERROR', 25 | message: '服务出错,请稍后再试.' 26 | }) 27 | } 28 | } 29 | 30 | function defaultErrorHandler(error){ 31 | if(error && error.message) { 32 | Event.emit('GLOBAL_ERROR', { 33 | type: 'SERVICE_ERROR', 34 | message: error.message 35 | }) 36 | }else { 37 | Event.emit('GLOBAL_ERROR', { 38 | type: 'SERVICE_ERROR', 39 | message: '服务出错,请稍后再试.' 40 | }) 41 | } 42 | } -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.reactnativeappdemo", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.reactnativeappdemo", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 19 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 20 | moduleName:@"ReactNativeAppDemo" 21 | initialProperties:nil]; 22 | 23 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 24 | 25 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 26 | UIViewController *rootViewController = [UIViewController new]; 27 | rootViewController.view = rootView; 28 | self.window.rootViewController = rootViewController; 29 | [self.window makeKeyAndVisible]; 30 | return YES; 31 | } 32 | 33 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 34 | { 35 | #if DEBUG 36 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 37 | #else 38 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 39 | #endif 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativeappdemo/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeappdemo; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.airbnb.android.react.lottie.LottiePackage; 7 | import com.learnium.RNDeviceInfo.RNDeviceInfo; 8 | import com.oblador.vectoricons.VectorIconsPackage; 9 | import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; 10 | import com.facebook.react.ReactNativeHost; 11 | import com.facebook.react.ReactPackage; 12 | import com.facebook.react.shell.MainReactPackage; 13 | import com.facebook.soloader.SoLoader; 14 | 15 | import java.util.Arrays; 16 | import java.util.List; 17 | 18 | public class MainApplication extends Application implements ReactApplication { 19 | 20 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 21 | @Override 22 | public boolean getUseDeveloperSupport() { 23 | return BuildConfig.DEBUG; 24 | } 25 | 26 | @Override 27 | protected List getPackages() { 28 | return Arrays.asList( 29 | new MainReactPackage(), 30 | new LottiePackage(), 31 | new RNDeviceInfo(), 32 | new VectorIconsPackage(), 33 | new RNGestureHandlerPackage() 34 | ); 35 | } 36 | 37 | @Override 38 | protected String getJSMainModuleName() { 39 | return "index"; 40 | } 41 | }; 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | return mReactNativeHost; 46 | } 47 | 48 | @Override 49 | public void onCreate() { 50 | super.onCreate(); 51 | SoLoader.init(this, /* native exopackage */ false); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/common/history.js: -------------------------------------------------------------------------------- 1 | const NAVIGATION_THROTTLE = 1000; // 1s内不准重复跳转 2 | const lastNavigationTimeStamps = {}; 3 | 4 | /** 5 | * 校验页面跳转参数 防止同一个path在很短的时间内被反复调用 6 | * @param path 7 | */ 8 | function validate(path) { 9 | const timestamp = new Date().valueOf(); 10 | if(lastNavigationTimeStamps[path] && (timestamp - lastNavigationTimeStamps[path]) < NAVIGATION_THROTTLE) { 11 | lastNavigationTimeStamps[path] = timestamp 12 | return false 13 | } else { 14 | lastNavigationTimeStamps[path] = timestamp 15 | } 16 | 17 | return true 18 | } 19 | 20 | /** 21 | * 处理路由跳转的状态 22 | * @param prevState 23 | * @param newState 24 | * @param action 25 | */ 26 | export function handleNavigationChange(prevState, newState, action) { 27 | console.log('@@@@@ prevState', prevState) 28 | console.log('@@@@@ newState', newState) 29 | console.log('@@@@@ action', action) 30 | } 31 | 32 | const history = { 33 | push: (instance, path, state) => { 34 | if(validate(path)) { 35 | const navigationController = instance.props.navigation; 36 | const nativePath = 37 | path.charAt(0) === '/' ? path.substring(1, path.length) : path; 38 | 39 | navigationController.push(nativePath, state) 40 | } 41 | }, 42 | replace: (instance, path, state) => { 43 | if(validate(path)) { 44 | const navigationController = instance.props.navigation; 45 | const nativePath = 46 | path.charAt(0) === '/' ? path.substring(1, path.length) : path; 47 | 48 | navigationController.replace(nativePath, state) 49 | } 50 | }, 51 | goBack: (instance) => { 52 | if(instance) { 53 | const navigationController = instance.props.navigation; 54 | navigationController.goBack() 55 | } 56 | }, 57 | pop: (instance, n) => { 58 | if(instance) { 59 | const navigationController = instance.props.navigation; 60 | navigationController.pop(-1 * n || -1) 61 | } 62 | } 63 | } 64 | 65 | export default history; -------------------------------------------------------------------------------- /src/components/navi-bar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text, StyleSheet, Dimensions, TouchableOpacity, Platform } from 'react-native' 3 | import PropTypes from 'prop-types' 4 | import {colors} from '../assets/styles/colors-theme'; 5 | import Icon from 'react-native-vector-icons/Ionicons' 6 | 7 | const { width } = Dimensions.get('window') 8 | 9 | export default class NaviBar extends Component { 10 | static propTypes = { 11 | style: PropTypes.object, 12 | leftItem: PropTypes.node, //原则上控制在宽度40的icon 13 | rightItem: PropTypes.node, //原则上控制在宽度40的icon 14 | title: PropTypes.string, 15 | titleColor: PropTypes.string, 16 | onBack: PropTypes.func, 17 | iconColor: PropTypes.string 18 | } 19 | 20 | render() { 21 | const props = this.props; 22 | 23 | return ( 24 | 25 | 26 | { 27 | props.leftItem ? props.leftItem : ( 28 | props.onBack ? ( 29 | 30 | 35 | 36 | ) : 37 | ) 38 | } 39 | 40 | {props.title} 41 | 42 | { 43 | props.rightItem ? props.rightItem : 44 | } 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | const styles = StyleSheet.flatten({ 52 | naviBar: { 53 | width, 54 | height: Platform.OS === 'ios' ? 44 : 56, //ios原生导航高度是44,android是56 55 | backgroundColor: colors.statusBarColor, 56 | flexDirection: 'row', 57 | alignItems: 'center', 58 | justifyContent: 'space-between' 59 | } 60 | }) -------------------------------------------------------------------------------- /src/pages/home/home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native' 3 | import {colors} from '../../assets/styles/colors-theme'; 4 | import history from '../../common/history'; 5 | import NaviBar from '../../components/navi-bar'; 6 | import { inject, observer } from 'mobx-react'; 7 | 8 | @inject('rootStore') 9 | @observer 10 | export default class Home extends Component { 11 | constructor(props) { 12 | super(props) 13 | this.store = props.rootStore.appStore 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | Home 22 | { 23 | //添加timer的次数 24 | this.store.tick(); 25 | const list = this.store.list; 26 | 27 | list.push({ 28 | number: this.store.timer, 29 | label: '第'+this.store.timer + '次点击' 30 | }) 31 | 32 | this.store.setList(list) 33 | history.push(this, '/list', {name: 'niunai'}) 34 | }}> 35 | 跳转到List 36 | 37 | 38 | 统计跳转到List的次数: {this.store.timer} 39 | 40 | { 41 | this.store.setList([]); 42 | this.store.resetTimer(); 43 | }}> 44 | 重置List和timer 45 | 46 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | const styles = StyleSheet.flatten({ 53 | button: { 54 | marginTop: 20, 55 | width: 100, 56 | height: 40, 57 | alignItems: 'center', 58 | justifyContent: 'center', 59 | backgroundColor: colors.statusBarColor 60 | }, 61 | buttonText: { 62 | color: '#fff' 63 | } 64 | }) -------------------------------------------------------------------------------- /ios/ReactNativeAppDemoTests/ReactNativeAppDemoTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 16 | 17 | @interface ReactNativeAppDemoTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation ReactNativeAppDemoTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 44 | if (level >= RCTLogLevelError) { 45 | redboxError = message; 46 | } 47 | }); 48 | 49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 52 | 53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 55 | return YES; 56 | } 57 | return NO; 58 | }]; 59 | } 60 | 61 | RCTSetLogFunction(RCTDefaultLogFunction); 62 | 63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 65 | } 66 | 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeAppDemo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "@ant-design/react-native": "^3.1.5", 11 | "fetch-intercept": "^2.3.1", 12 | "lottie-react-native": "^2.6.1", 13 | "mobx": "^5.9.4", 14 | "mobx-react": "^5.4.3", 15 | "promise-polyfill": "^8.1.0", 16 | "prop-types": "^15.7.2", 17 | "react": "16.8.3", 18 | "react-native": "0.59.4", 19 | "react-native-device-info": "^1.5.0", 20 | "react-native-gesture-handler": "^1.1.0", 21 | "react-native-vector-icons": "^6.4.2", 22 | "react-navigation": "^3.7.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/cli": "^7.4.3", 26 | "@babel/core": "^7.4.3", 27 | "@babel/plugin-proposal-class-properties": "^7.4.0", 28 | "@babel/plugin-proposal-decorators": "^7.4.0", 29 | "@babel/plugin-proposal-object-rest-spread": "^7.4.3", 30 | "@babel/plugin-transform-classes": "^7.4.3", 31 | "@babel/plugin-transform-flow-strip-types": "^7.4.0", 32 | "@babel/plugin-transform-runtime": "^7.4.3", 33 | "@babel/polyfill": "^7.4.3", 34 | "@babel/preset-env": "^7.4.3", 35 | "@babel/preset-flow": "^7.0.0", 36 | "@babel/preset-react": "^7.0.0", 37 | "@babel/runtime": "^7.4.3", 38 | "babel-eslint": "^10.0.1", 39 | "babel-jest": "^24.7.1", 40 | "babel-loader": "^8.0.5", 41 | "babel-plugin-import": "^1.11.0", 42 | "babel-plugin-module-resolver": "^3.2.0", 43 | "babel-plugin-transform-runtime": "^6.23.0", 44 | "babel-polyfill": "^6.26.0", 45 | "babel-preset-es2015": "^6.24.1", 46 | "babel-preset-react": "^6.24.1", 47 | "babel-preset-react-native": "^4.0.1", 48 | "babel-preset-react-native-stage-0": "^1.0.1", 49 | "babel-preset-react-native-syntax": "^1.0.0", 50 | "eslint": "^5.16.0", 51 | "eslint-config-prettier": "^4.1.0", 52 | "eslint-plugin-flowtype": "^3.6.1", 53 | "eslint-plugin-import": "^2.17.1", 54 | "eslint-plugin-jsx-a11y": "^6.2.1", 55 | "eslint-plugin-prettier": "^3.0.1", 56 | "eslint-plugin-promise": "^4.1.1", 57 | "eslint-plugin-react": "^7.12.4", 58 | "jest": "^24.7.1", 59 | "metro-react-native-babel-preset": "^0.53.1", 60 | "react-test-renderer": "16.8.3" 61 | }, 62 | "jest": { 63 | "preset": "react-native" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | ; Ignore metro 20 | .*/node_modules/metro/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | node_modules/react-native/Libraries/react-native/react-native-interface.js 26 | node_modules/react-native/flow/ 27 | 28 | [options] 29 | emoji=true 30 | 31 | esproposal.optional_chaining=enable 32 | esproposal.nullish_coalescing=enable 33 | 34 | module.system=haste 35 | module.system.haste.use_name_reducers=true 36 | # get basename 37 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 38 | # strip .js or .js.flow suffix 39 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 40 | # strip .ios suffix 41 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 42 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 43 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 44 | module.system.haste.paths.blacklist=.*/__tests__/.* 45 | module.system.haste.paths.blacklist=.*/__mocks__/.* 46 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 47 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 48 | 49 | munge_underscores=true 50 | 51 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 52 | 53 | module.file_ext=.js 54 | module.file_ext=.jsx 55 | module.file_ext=.json 56 | module.file_ext=.native.js 57 | 58 | suppress_type=$FlowIssue 59 | suppress_type=$FlowFixMe 60 | suppress_type=$FlowFixMeProps 61 | suppress_type=$FlowFixMeState 62 | 63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 64 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 65 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 66 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 67 | 68 | [version] 69 | ^0.92.0 70 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ReactNativeAppDemo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSLocationWhenInUseUsageDescription 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UIViewControllerBasedStatusBarAppearance 42 | 43 | NSAppTransportSecurity 44 | 45 | NSAllowsArbitraryLoads 46 | 47 | NSExceptionDomains 48 | 49 | localhost 50 | 51 | NSExceptionAllowsInsecureHTTPLoads 52 | 53 | 54 | 55 | 56 | UIAppFonts 57 | 58 | AntDesign.ttf 59 | Entypo.ttf 60 | EvilIcons.ttf 61 | Feather.ttf 62 | FontAwesome.ttf 63 | FontAwesome5_Brands.ttf 64 | FontAwesome5_Regular.ttf 65 | FontAwesome5_Solid.ttf 66 | Foundation.ttf 67 | Ionicons.ttf 68 | MaterialCommunityIcons.ttf 69 | MaterialIcons.ttf 70 | Octicons.ttf 71 | SimpleLineIcons.ttf 72 | Zocial.ttf 73 | antfill.ttf 74 | antoutline.ttf 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/common/tab-nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Icon from 'react-native-vector-icons/Ionicons' 3 | import {createBottomTabNavigator} from 'react-navigation'; 4 | 5 | import Home from '../pages/home/home'; 6 | import Page3 from '../pages/page3/page3'; 7 | import Page1 from '../pages/page1/page1'; 8 | import Page2 from '../pages/page2/page2'; 9 | import {colors} from '../assets/styles/colors-theme'; 10 | 11 | const TabRouterMap = { 12 | home: { 13 | screen: Home, 14 | navigationOptions: { 15 | tabBarLabel: 'Home', 16 | tabBarIcon:({focused}) => ( 17 | 23 | ) 24 | } 25 | }, 26 | page1: { 27 | screen: Page1, 28 | navigationOptions: { 29 | tabBarLabel: 'Page1', 30 | tabBarIcon:({focused}) => ( 31 | 37 | ) 38 | } 39 | }, 40 | page2: { 41 | screen: Page2, 42 | navigationOptions: { 43 | tabBarLabel: 'Page2', 44 | tabBarIcon:({focused}) => ( 45 | 51 | ) 52 | } 53 | }, 54 | page3: { 55 | screen: Page3, 56 | navigationOptions: { 57 | tabBarLabel: 'Page3', 58 | tabBarIcon:({focused}) => ( 59 | 65 | ) 66 | } 67 | } 68 | } 69 | 70 | export const TabNav = createBottomTabNavigator(TabRouterMap,{ 71 | initialRouteName: 'home', 72 | tabBarOptions: { 73 | //当前选中的tab bar的文本颜色和图标颜色 74 | activeTintColor: colors.statusBarColor, 75 | //当前未选中的tab bar的文本颜色和图标颜色 76 | inactiveTintColor: '#000', 77 | //是否显示tab bar的图标,默认是false 78 | showIcon: true, 79 | //showLabel - 是否显示tab bar的文本,默认是true 80 | showLabel: true, 81 | //是否将文本转换为大小,默认是true 82 | upperCaseLabel: false, 83 | //material design中的波纹颜色(仅支持Android >= 5.0) 84 | pressColor: 'red', 85 | //按下tab bar时的不透明度(仅支持iOS和Android < 5.0). 86 | pressOpacity: 0.8, 87 | //tab bar的样式 88 | // style: { 89 | // backgroundColor: '#fff', 90 | // paddingBottom: 1, 91 | // borderTopWidth: 0.2, 92 | // paddingTop:1, 93 | // borderTopColor: '#ccc', 94 | // }, 95 | //tab bar的文本样式 96 | labelStyle: { 97 | fontSize: 11, 98 | margin: 1 99 | }, 100 | //tab 页指示符的样式 (tab页下面的一条线). 101 | indicatorStyle: {height: 0}, 102 | }, 103 | //tab bar的位置, 可选值: 'top' or 'bottom' 104 | tabBarPosition: 'bottom', 105 | //是否允许滑动切换tab页 106 | swipeEnabled: true, 107 | //是否在切换tab页时使用动画 108 | animationEnabled: false, 109 | //是否懒加载 110 | lazy: true, 111 | //返回按钮是否会导致tab切换到初始tab页? 如果是,则设置为initialRoute,否则为none。 缺省为initialRoute。 112 | backBehavior: 'none' 113 | }) 114 | -------------------------------------------------------------------------------- /src/pages/list/list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {View, Text, TouchableOpacity, StyleSheet, Dimensions} from 'react-native' 3 | import NaviBar from '../../components/navi-bar'; 4 | import history from '../../common/history'; 5 | import {colors} from '../../assets/styles/colors-theme'; 6 | import ListService from '../../services/list-service'; 7 | import LoadingHoc from '../../hocs/loading-hoc'; 8 | import {inject, observer} from 'mobx-react'; 9 | 10 | const { width } = Dimensions.get('window') 11 | 12 | @LoadingHoc 13 | @inject('rootStore') 14 | @observer 15 | export default class List extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.state = { 19 | list: [], 20 | name: props.navigation.state.params.name 21 | } 22 | this.listService = new ListService(props) 23 | this.store = props.rootStore.appStore 24 | } 25 | 26 | async componentDidMount() { 27 | const res = await this.listService.getList(); 28 | 29 | if(res) { 30 | this.setState({ 31 | list: res 32 | }) 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 43 | 44 | 45 | Home页传过来的name:{this.state.name} 46 | 47 | 48 | 统计到{this.store.timer}次跳转到List 49 | 50 | 51 | 52 | 次数 53 | 54 | 55 | 描述 56 | 57 | 58 | { 59 | this.store.list.map((item, index) => { 60 | return ( 61 | 62 | 63 | {item.number} 64 | 65 | 66 | {item.label} 67 | 68 | 69 | ) 70 | }) 71 | } 72 | history.push(this, '/detail', {name: 'suannai'})}> 73 | 跳转到Detail 74 | 75 | 76 | 77 | ) 78 | } 79 | } 80 | 81 | const styles = StyleSheet.flatten({ 82 | button: { 83 | marginTop: 20, 84 | width: 100, 85 | height: 40, 86 | alignItems: 'center', 87 | justifyContent: 'center', 88 | backgroundColor: colors.statusBarColor 89 | }, 90 | buttonText: { 91 | color: '#fff' 92 | }, 93 | number: { 94 | width: 0.3 * width, 95 | height: 40, 96 | alignItems: 'center', 97 | justifyContent: 'center' 98 | }, 99 | label: { 100 | width: 0.7 * width, 101 | height: 40, 102 | alignItems: 'center', 103 | justifyContent: 'center' 104 | } 105 | }) -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {StatusBar} from 'react-native'; 3 | import {SafeAreaView} from 'react-navigation' 4 | import Router from './src/router'; 5 | import {colors} from './src/assets/styles/colors-theme'; 6 | import {handleNavigationChange} from './src/common/history'; 7 | import {Provider as ProviderAntd, Modal} from '@ant-design/react-native' 8 | import LoadingView from './src/common/loading'; 9 | import { Provider, observer } from 'mobx-react' 10 | import * as stores from './src/stores/index'; 11 | import {handleErrors} from './src/common/global-error-handler'; 12 | import Event from './src/common/event'; 13 | 14 | @observer 15 | export default class App extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.timer = null 19 | this.state = { 20 | loadingCount: 0 21 | } 22 | 23 | require('promise/setimmediate/rejection-tracking').enable({ 24 | allRejections: true, 25 | onUnhandled: (id, error) => { 26 | handleErrors(error); 27 | } 28 | }) 29 | 30 | this._handleGlobalError = this.handleGlobalError.bind(this) 31 | this._handleShowLoading = this.handleShowLoading.bind(this) 32 | this._handleHideLoading = this.handleHideLoading.bind(this) 33 | } 34 | 35 | componentDidMount() { 36 | // 监听全局报错 37 | Event.listen('GLOBAL_ERROR', this._handleGlobalError, this) 38 | 39 | // 显示加载动画 40 | Event.listen('SHOW_LOADING', this._handleShowLoading, this) 41 | 42 | // 隐藏加载动画 43 | Event.listen('HIDE_LOADING', this._handleHideLoading, this) 44 | } 45 | 46 | //组件卸载之前移除监听 47 | componentWillUnmount() { 48 | Event.remove('GLOBAL_ERROR', this) 49 | Event.remove('SHOW_LOADING', this) 50 | Event.remove('HIDE_LOADING', this) 51 | } 52 | 53 | render() { 54 | return ( 55 | 56 | 57 | 64 | 70 | 73 | 0} /> 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | /** 81 | * showLoading 82 | */ 83 | handleShowLoading() { 84 | if(this.timer) { 85 | clearTimeout(this.timer); 86 | } 87 | 88 | this.timer = setTimeout(() => { 89 | this.setState({ 90 | loadingCount: this.state.loadingCount + 1 91 | }) 92 | }, 50) 93 | } 94 | 95 | /** 96 | * hideLoading 97 | * @param bForece 98 | */ 99 | handleHideLoading(bForece){ 100 | if(this.timer){ 101 | clearTimeout(this.timer); 102 | } 103 | 104 | this.timer = setTimeout(() => { 105 | if(this.state.loadingCount > 0){ 106 | this.setState({ 107 | loadingCount: (bForece ? 0 : this.state.loadingCount - 1) 108 | }); 109 | } 110 | }, 50) 111 | } 112 | 113 | /** 114 | * 全局报错处理 115 | * @param event 116 | */ 117 | handleGlobalError(event) { 118 | // 报错时,取消加载动画 119 | if(this.state.loadingCount > 0){ 120 | this.handleHideLoading(true) 121 | } 122 | 123 | if(event && event.type){ 124 | switch(event.type){ 125 | case 'SESSION_TIMEOUT': 126 | Modal.alert('会话超时', '您的会话已超时,请重新登录') 127 | break; 128 | case 'SERVICE_ERROR': 129 | if(event.message) { 130 | Modal.alert('出错了', event.message) 131 | } 132 | break; 133 | default: 134 | if(event.message) { 135 | Modal.alert('温馨提示', '系统未知异常') 136 | } 137 | break; 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/services/base-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础服务类封装 3 | * BaseService 4 | */ 5 | import fetchIntercept from 'fetch-intercept' 6 | import constants from '../common/constants'; 7 | import ServiceError from '../common/service-error'; 8 | import code from '../common/code'; 9 | 10 | const fetchApi = fetch; // eslint-disable-line 11 | 12 | fetchIntercept.register({ 13 | request: function (url, config) { 14 | return [url, config]; 15 | }, 16 | requestError: function (error) { 17 | return Promise.reject(error); 18 | }, 19 | response: function (res) { 20 | return res; 21 | }, 22 | responseError: function (error) { 23 | return Promise.reject(error); 24 | } 25 | }); 26 | 27 | export default class BaseService { 28 | constructor(props){ 29 | if(props && props.showLoading){ 30 | this.showLoading = props.showLoading; 31 | } 32 | if(props && props.hideLoading){ 33 | this.hideLoading = props.hideLoading; 34 | } 35 | } 36 | 37 | async request(method, url, params, errorMsgIndex, showLoading = true, acceptType = 'application/json') { 38 | // 如果url不全,则自动补全 39 | if(url.indexOf('http://') < 0 && url.indexOf('https://') < 0){ 40 | url = constants.serverUrl + '/' + url; 41 | } 42 | 43 | if(showLoading && this.showLoading){ 44 | this.showLoading(); 45 | } 46 | 47 | let res = null 48 | let timer = null 49 | 50 | try { 51 | const options = { 52 | method: method, 53 | credentials: 'include', 54 | headers: { 55 | 'content-type': 'application/json', 56 | 'accept': acceptType, 57 | 'Cache-Control': 'no-cache' 58 | } 59 | } 60 | 61 | if(method === 'POST' || method === 'PUT') { 62 | params.DeviceType = constants.deviceType 63 | params.DeviceNo = constants.deviceNo 64 | 65 | options.body = JSON.stringify(params || {}) 66 | } 67 | 68 | res = await fetchApi(url, options) 69 | } catch (e) { 70 | if(this.hideLoading){ 71 | if(timer) { 72 | clearTimeout(timer) 73 | } 74 | timer = setTimeout(() => { 75 | this.hideLoading() 76 | }, 2000) 77 | } 78 | 79 | throw new ServiceError(code.REQUEST_FAILED, '网络请求失败') 80 | } 81 | 82 | if(res.status && res.status >= 200 && res.status < 300) { 83 | const contentType = res.headers.get('Content-Type') 84 | 85 | if(this.hideLoading){ 86 | this.hideLoading() 87 | } 88 | 89 | if(contentType.indexOf('text/plain') >= 0 || contentType.indexOf('text/html') >= 0){ 90 | return res.text() 91 | }else{ 92 | const responseJson = await res.json(); 93 | if (responseJson && !responseJson.jsonError) { 94 | return responseJson 95 | } else { 96 | throw new ServiceError(responseJson.jsonError[0]._exceptionMessageCode || code.REQUEST_FAILED, responseJson.jsonError[0]._exceptionMessage); 97 | } 98 | } 99 | } else { 100 | if(this.hideLoading){ 101 | if(timer) { 102 | clearTimeout(timer) 103 | } 104 | 105 | timer = setTimeout(() => { 106 | this.hideLoading() 107 | }, 2000) 108 | } 109 | 110 | if (res.status === 401) { 111 | throw new ServiceError(code.REQUEST_TIMEOUT, res.data.message); 112 | } else if (res.ok) { 113 | try { 114 | const responseJson = await res.json(); 115 | const { message } = responseJson; 116 | throw new ServiceError(code.REQUEST_FAILED, message); 117 | } catch (e) { 118 | throw new ServiceError(code.REQUEST_FAILED, '服务未知错误'); 119 | } 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * GET 后台数据 126 | * @param url 127 | * @param errorMsg 报错消息 128 | * @returns {Promise<*>} 129 | */ 130 | async fetchJson(url, errorMsg, showLoading = true){ 131 | return await this.request('GET', url, null, errorMsg, showLoading) 132 | } 133 | 134 | /** 135 | * POST请求 136 | * @param url 137 | * @param params 138 | * @param errorMsg 报错消息 139 | * @returns {Promise.} 140 | */ 141 | async postJson(url, params, errorMsg, showLoading = true){ 142 | return await this.request('POST', url, params, errorMsg, showLoading) 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /src/common/notification-center.js: -------------------------------------------------------------------------------- 1 | const __notices = []; 2 | /** 3 | * addNotification 4 | * 注册通知对象方法 5 | * 6 | * 参数: 7 | * name: 注册名,一般let在公共类中 8 | * selector: 对应的通知方法,接受到通知后进行的动作 9 | * observer: 注册对象,指Page对象 10 | */ 11 | function addNotification(name, selector, observer) { 12 | if (name && selector) { 13 | if (!observer) { 14 | console.log( 15 | "addNotification Warning: no observer will can't remove notice" 16 | ); 17 | } 18 | const newNotice = { 19 | name: name, 20 | selector: selector, 21 | observer: observer 22 | }; 23 | 24 | addNotices(newNotice); 25 | } else { 26 | console.log('addNotification error: no selector or name'); 27 | } 28 | } 29 | 30 | /** 31 | * 仅添加一次监听 32 | * 33 | * 参数: 34 | * name: 注册名,一般let在公共类中 35 | * selector: 对应的通知方法,接受到通知后进行的动作 36 | * observer: 注册对象,指Page对象 37 | */ 38 | function addOnceNotification(name, selector, observer) { 39 | if (__notices.length > 0) { 40 | for (let i = 0; i < __notices.length; i++) { 41 | const notice = __notices[i]; 42 | if (notice.name === name) { 43 | if (notice.observer === observer) { 44 | return; 45 | } 46 | } 47 | } 48 | } 49 | this.addNotification(name, selector, observer); 50 | } 51 | 52 | function addNotices(newNotice) { 53 | // if (__notices.length > 0) { 54 | // for (var i = 0; i < __notices.length; i++) { 55 | // var hisNotice = __notices[i]; 56 | // //当名称一样时进行对比,如果不是同一个 则放入数组,否则跳出 57 | // if (newNotice.name === hisNotice.name) { 58 | // if (!cmp(hisNotice, newNotice)) { 59 | // __notices.push(newNotice); 60 | // } 61 | // return; 62 | // }else{ 63 | // __notices.push(newNotice); 64 | // } 65 | 66 | // } 67 | // } else { 68 | 69 | // } 70 | 71 | __notices.push(newNotice); 72 | } 73 | 74 | /** 75 | * removeNotification 76 | * 移除通知方法 77 | * 78 | * 参数: 79 | * name: 已经注册了的通知 80 | * observer: 移除的通知所在的Page对象 81 | */ 82 | 83 | function removeNotification(name, observer) { 84 | console.log('removeNotification:' + name); 85 | for (let i = 0; i < __notices.length; i++) { 86 | const notice = __notices[i]; 87 | if (notice.name === name) { 88 | if (notice.observer === observer) { 89 | __notices.splice(i, 1); 90 | return; 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * postNotificationName 98 | * 发送通知方法 99 | * 100 | * 参数: 101 | * name: 已经注册了的通知 102 | * info: 携带的参数 103 | */ 104 | 105 | function postNotificationName(name, info) { 106 | console.log('postNotificationName:' + name); 107 | if (__notices.length === 0) { 108 | console.log("postNotificationName error: u hadn't add any notice."); 109 | return; 110 | } 111 | 112 | for (let i = 0; i < __notices.length; i++) { 113 | const notice = __notices[i]; 114 | if (notice.name === name) { 115 | notice.selector(info); 116 | } 117 | } 118 | } 119 | 120 | // 用于对比两个对象是否相等 121 | function cmp(x, y) { // eslint-disable-line 122 | // If both x and y are null or undefined and exactly the same 123 | if (x === y) { 124 | return true; 125 | } 126 | 127 | // If they are not strictly equal, they both need to be Objects 128 | if (!(x instanceof Object) || !(y instanceof Object)) { 129 | return false; 130 | } 131 | 132 | // They must have the exact same prototype chain, the closest we can do is 133 | // test the constructor. 134 | if (x.constructor !== y.constructor) { 135 | return false; 136 | } 137 | 138 | for (const p in x) { 139 | // Inherited properties were tested using x.constructor === y.constructor 140 | if (x.hasOwnProperty(p)) { 141 | // Allows comparing x[ p ] and y[ p ] when set to undefined 142 | if (!y.hasOwnProperty(p)) { 143 | return false; 144 | } 145 | 146 | // If they have the same strict value or identity then they are equal 147 | if (x[p] === y[p]) { 148 | continue; 149 | } 150 | 151 | // Numbers, Strings, Functions, Booleans must be strictly equal 152 | if (typeof x[p] !== 'object') { 153 | return false; 154 | } 155 | 156 | // Objects and Arrays must be tested recursively 157 | if (!Object.equals(x[p], y[p])) { 158 | return false; 159 | } 160 | } 161 | } 162 | 163 | for (const p in y) { 164 | // allows x[ p ] to be set to undefined 165 | if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) { 166 | return false; 167 | } 168 | } 169 | return true; 170 | } 171 | 172 | module.exports = { 173 | addNotification: addNotification, 174 | removeNotification: removeNotification, 175 | postNotificationName: postNotificationName, 176 | addOnceNotification: addOnceNotification 177 | }; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // babel parser to support ES6/7 features 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 7, 6 | "ecmaFeatures": { 7 | "legacyDecorators": true, 8 | "experimentalObjectRestSpread": true, 9 | "jsx": true 10 | }, 11 | "sourceType": "module" 12 | }, 13 | "extends": [ 14 | "prettier", 15 | "prettier/react" 16 | ], 17 | "plugins": [ 18 | "promise", 19 | "react" 20 | ], 21 | "env": { 22 | "es6": true, 23 | "node": true 24 | }, 25 | "globals": { 26 | "document": false, 27 | "navigator": false, 28 | "location": false, 29 | "window": false, 30 | // Flow global types 31 | "HTMLInputElement": false, 32 | "ReactClass": false, 33 | "ReactComponent": false, 34 | "ReactElement": false, 35 | "ReactPropsChainableTypeChecker": false, 36 | "ReactPropsCheckType": false, 37 | "ReactPropTypes": false, 38 | "SyntheticEvent": false, 39 | "cancelAnimationFrame": false, 40 | "requestAnimationFrame": false, 41 | "Image": false 42 | }, 43 | "rules": { 44 | "camelcase": 0, 45 | "constructor-super": 2, 46 | "default-case": [2, { "commentPattern": "^no default$" }], 47 | "eqeqeq": [2, "allow-null"], 48 | "handle-callback-err": [2, "^(err|error)$" ], 49 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 50 | "no-alert": 1, 51 | "no-array-constructor": 2, 52 | "no-caller": 2, 53 | "no-case-declarations": 2, 54 | "no-class-assign": 2, 55 | "no-cond-assign": 2, 56 | "no-const-assign": 2, 57 | "no-control-regex": 2, 58 | "no-debugger": 2, 59 | "no-delete-var": 2, 60 | "no-dupe-args": 2, 61 | "no-dupe-class-members": 2, 62 | "no-dupe-keys": 2, 63 | "no-duplicate-case": 2, 64 | "no-empty-character-class": 2, 65 | "no-empty-pattern": 2, 66 | "no-eval": 2, 67 | "no-ex-assign": 2, 68 | "no-extend-native": 2, 69 | "no-extra-bind": 2, 70 | "no-extra-boolean-cast": 2, 71 | "no-fallthrough": 2, 72 | "no-floating-decimal": 2, 73 | "no-func-assign": 2, 74 | "no-implied-eval": 2, 75 | "no-inner-declarations": [2, "functions"], 76 | "no-invalid-regexp": 2, 77 | "no-irregular-whitespace": 2, 78 | "no-iterator": 2, 79 | "no-label-var": 2, 80 | "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], 81 | "no-lone-blocks": 2, 82 | "no-loop-func": 2, 83 | "no-multi-str": 2, 84 | "no-native-reassign": 2, 85 | "no-negated-in-lhs": 2, 86 | "no-new": 2, 87 | "no-new-func": 2, 88 | "no-new-object": 2, 89 | "no-new-require": 2, 90 | "no-new-symbol": 2, 91 | "no-new-wrappers": 2, 92 | "no-obj-calls": 2, 93 | "no-octal": 2, 94 | "no-octal-escape": 2, 95 | "no-path-concat": 2, 96 | "no-proto": 2, 97 | "no-redeclare": 2, 98 | "no-regex-spaces": 2, 99 | "no-return-assign": [2, "except-parens"], 100 | "no-script-url": 2, 101 | "no-self-assign": 2, 102 | "no-self-compare": 2, 103 | "no-sequences": 2, 104 | "no-shadow-restricted-names": 2, 105 | "no-sparse-arrays": 2, 106 | "no-this-before-super": 2, 107 | "no-throw-literal": 2, 108 | "no-undef": 2, 109 | "no-undef-init": 2, 110 | "no-unexpected-multiline": 2, 111 | "no-unmodified-loop-condition": 2, 112 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 113 | "no-unreachable": 2, 114 | "no-unsafe-finally": 2, 115 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 116 | "no-useless-call": 2, 117 | "no-useless-computed-key": 2, 118 | "no-useless-concat": 2, 119 | "no-useless-constructor": 2, 120 | "no-useless-escape": 2, 121 | "no-var": 2, 122 | "no-with": 2, 123 | "prefer-const": 2, 124 | "prefer-rest-params": 2, 125 | "quotes": [2, "single", "avoid-escape"], 126 | "radix": 2, 127 | "use-isnan": 2, 128 | "valid-typeof": 2, 129 | "yoda": [2, "never"], 130 | 131 | // promise 132 | "promise/param-names": 2, 133 | 134 | // react 135 | "react/display-name": 0, 136 | "react/jsx-no-bind": 0, 137 | "react/jsx-no-duplicate-props": 2, 138 | "react/jsx-no-undef": 2, 139 | "react/jsx-pascal-case": 2, 140 | "react/jsx-sort-props": 0, 141 | "react/jsx-uses-react": 2, 142 | "react/jsx-uses-vars": 2, 143 | "react/no-did-mount-set-state": 0, 144 | "react/no-did-update-set-state": 2, 145 | "react/no-direct-mutation-state": 2, 146 | "react/no-multi-comp": 0, 147 | "react/no-string-refs": 2, 148 | "react/no-unknown-property": 2, 149 | "react/prefer-es6-class": 2, 150 | "react/prop-types": 0, 151 | "react/react-in-jsx-scope": 0, 152 | "react/self-closing-comp": 2, 153 | "react/sort-comp": 0, 154 | "react/sort-prop-types": 0, 155 | "react/wrap-multilines": 0 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo.xcodeproj/xcshareddata/xcschemes/ReactNativeAppDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /ios/ReactNativeAppDemo.xcodeproj/xcshareddata/xcschemes/ReactNativeAppDemo-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 37 | * // for example: to disable dev mode in the staging build type (if configured) 38 | * devDisabledInStaging: true, 39 | * // The configuration property can be in the following formats 40 | * // 'devDisabledIn${productFlavor}${buildType}' 41 | * // 'devDisabledIn${buildType}' 42 | * 43 | * // the root of your project, i.e. where "package.json" lives 44 | * root: "../../", 45 | * 46 | * // where to put the JS bundle asset in debug mode 47 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 48 | * 49 | * // where to put the JS bundle asset in release mode 50 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 51 | * 52 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 53 | * // require('./image.png')), in debug mode 54 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 55 | * 56 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 57 | * // require('./image.png')), in release mode 58 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 59 | * 60 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 61 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 62 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 63 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 64 | * // for example, you might want to remove it from here. 65 | * inputExcludes: ["android/**", "ios/**"], 66 | * 67 | * // override which node gets called and with what additional arguments 68 | * nodeExecutableAndArgs: ["node"], 69 | * 70 | * // supply additional arguments to the packager 71 | * extraPackagerArgs: [] 72 | * ] 73 | */ 74 | 75 | project.ext.react = [ 76 | entryFile: "index.js" 77 | ] 78 | 79 | apply from: "../../node_modules/react-native/react.gradle" 80 | 81 | /** 82 | * Set this to true to create two separate APKs instead of one: 83 | * - An APK that only works on ARM devices 84 | * - An APK that only works on x86 devices 85 | * The advantage is the size of the APK is reduced by about 4MB. 86 | * Upload all the APKs to the Play Store and people will download 87 | * the correct one based on the CPU architecture of their device. 88 | */ 89 | def enableSeparateBuildPerCPUArchitecture = false 90 | 91 | /** 92 | * Run Proguard to shrink the Java bytecode in release builds. 93 | */ 94 | def enableProguardInReleaseBuilds = false 95 | 96 | android { 97 | compileSdkVersion rootProject.ext.compileSdkVersion 98 | 99 | compileOptions { 100 | sourceCompatibility JavaVersion.VERSION_1_8 101 | targetCompatibility JavaVersion.VERSION_1_8 102 | } 103 | 104 | defaultConfig { 105 | applicationId "com.reactnativeappdemo" 106 | minSdkVersion rootProject.ext.minSdkVersion 107 | targetSdkVersion rootProject.ext.targetSdkVersion 108 | versionCode 1 109 | versionName "1.0" 110 | } 111 | splits { 112 | abi { 113 | reset() 114 | enable enableSeparateBuildPerCPUArchitecture 115 | universalApk false // If true, also generate a universal APK 116 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 117 | } 118 | } 119 | buildTypes { 120 | release { 121 | minifyEnabled enableProguardInReleaseBuilds 122 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 123 | } 124 | } 125 | // applicationVariants are e.g. debug, release 126 | applicationVariants.all { variant -> 127 | variant.outputs.each { output -> 128 | // For each separate APK per architecture, set a unique version code as described here: 129 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 130 | def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4] 131 | def abi = output.getFilter(OutputFile.ABI) 132 | if (abi != null) { // null for the universal-debug, universal-release variants 133 | output.versionCodeOverride = 134 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 135 | } 136 | } 137 | } 138 | } 139 | 140 | dependencies { 141 | implementation project(':lottie-react-native') 142 | implementation project(':react-native-device-info') 143 | implementation project(':react-native-vector-icons') 144 | implementation project(':react-native-gesture-handler') 145 | implementation fileTree(dir: "libs", include: ["*.jar"]) 146 | implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" 147 | implementation "com.facebook.react:react-native:+" // From node_modules 148 | } 149 | 150 | // Run this once to be able to run the application with BUCK 151 | // puts all compile dependencies into folder libs for BUCK to use 152 | task copyDownloadableDepsToLibs(type: Copy) { 153 | from configurations.compile 154 | into 'libs' 155 | } 156 | -------------------------------------------------------------------------------- /src/assets/animations/loading.json: -------------------------------------------------------------------------------- 1 | {"v":"5.4.2","fr":25,"ip":0,"op":93,"w":120,"h":120,"nm":"橙色球","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"橙色球2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[40,70,0],"e":[60,82,0],"to":[-10.6666669845581,20.25,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":11,"s":[60,82,0],"e":[60,82,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":13,"s":[60,82,0],"e":[60,32,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":27,"s":[60,32,0],"e":[60,32,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":29,"s":[60,32,0],"e":[40,44,0],"to":[-12.5833330154419,-17.25,0],"ti":[-11.1666669845581,-10.25,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":43,"s":[40,44,0],"e":[40,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":45,"s":[40,44,0],"e":[80,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":59,"s":[80,70,0],"e":[80,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":61,"s":[80,70,0],"e":[80,44,0],"to":[18.875,-0.45833334326744,0],"ti":[17.625,0.08333333581686,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":75,"s":[80,44,0],"e":[80,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":77,"s":[80,44,0],"e":[40,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":91,"s":[40,70,0],"e":[40,70,0],"to":[0,0,0],"ti":[0,0,0]},{"t":93}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392156863,0.537254901961,0.098039215686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":249,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"橙色球3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[80,70,0],"e":[40,44,0],"to":[-6.66666650772095,-4.33333349227905,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":11,"s":[40,44,0],"e":[40,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":13,"s":[40,44,0],"e":[40,70,0],"to":[-20.125,-0.29166665673256,0],"ti":[-18.5,0.54166668653488,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":27,"s":[40,70,0],"e":[40,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":29,"s":[40,70,0],"e":[80,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":43,"s":[80,44,0],"e":[80,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":45,"s":[80,44,0],"e":[60,32,0],"to":[11.9166669845581,-13.25,0],"ti":[12.0833330154419,-16.5,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":59,"s":[60,32,0],"e":[60,32,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":61,"s":[60,32,0],"e":[60,82,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":75,"s":[60,82,0],"e":[60,82,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":77,"s":[60,82,0],"e":[80,70,0],"to":[7.58333349227905,21.75,0],"ti":[16.9166660308838,11.75,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":91,"s":[80,70,0],"e":[80,70,0],"to":[0,0,0],"ti":[0,0,0]},{"t":93}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392156863,0.537254901961,0.098039215686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":249,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"橙色球1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[60,32,0],"e":[80,44,0],"to":[5.33333349227905,-20.25,0],"ti":[15.4166669845581,-16,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":11,"s":[80,44,0],"e":[80,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":13,"s":[80,44,0],"e":[80,70,0],"to":[26.875,0.58333331346512,0],"ti":[20,-0.08333333581686,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":27,"s":[80,70,0],"e":[80,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":29,"s":[80,70,0],"e":[60,82,0],"to":[15.6666669845581,15,0],"ti":[6.08333349227905,16.5,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":43,"s":[60,82,0],"e":[60,82,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":45,"s":[60,82,0],"e":[40,70,0],"to":[-14.5833330154419,18.25,0],"ti":[-11.9166669845581,17.75,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":59,"s":[40,70,0],"e":[40,70,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":61,"s":[40,70,0],"e":[40,44,0],"to":[-25.5,-0.33333334326744,0],"ti":[-23.75,-0.41666665673256,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":75,"s":[40,44,0],"e":[40,44,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":77,"s":[40,44,0],"e":[60,32,0],"to":[-17.1666660308838,-12,0],"ti":[-6.58333349227905,-19.75,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"n":"0p667_0p667_0p333_0p333","t":91,"s":[60,32,0],"e":[60,32,0],"to":[0,0,0],"ti":[0,0,0]},{"t":93}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392156863,0.537254901961,0.098039215686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":249,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactNativeAppDemo 2 | **ReactNativeAppDemo** 是一个以react-native+mobx为基础搭建的app案例,旨在让初学者了解基本的RNApp的搭建与应用。 3 | 4 | 教程包含基础框架搭建、路由封装、导航栏封装、service封装、全局报错处理封装、高阶组件封装、全局事件消息总线封装... 5 | 6 | ## 支持平台 7 | - [x] IOS 8 | - [x] Android 9 | 10 | ## 效果图 11 | ![img](https://github.com/niunai2016/ReactNativeAppDemo/blob/master/images-folder/app.gif) 12 | 13 | ## 基础环境搭建 14 | 按照[react-native中文官网](https://reactnative.cn/docs/getting-started/)搭建基础环境 15 | 16 | ``` 17 | $ react-native init ReactNativeAppDemo 18 | ``` 19 | 20 | **相关配置**:(如Eslint、git、Babel等) 21 | 22 | 1.Eslint配置: 在根目录新增.eslintrc 和 .eslintignore,具体配置看本文源码,也可查阅官方资料 23 | 24 | ``` 25 | $ yarn add eslint babel-eslint eslint-config-prettier eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-promise eslint-plugin-react -D 26 | ``` 27 | 2.git配置:根据官方自行配置,也可参考本文源码 28 | 29 | 30 | 31 | ## 路由功能 32 | 一般RN的路由通过官方推荐的[react-navigation](https://reactnavigation.org/docs/zh-Hans/getting-started.html)来实现,目前官方出到3.x 33 | 34 | 在你的 React Native 项目中安装`react-navigation`这个包 35 | ``` 36 | $ yarn add react-navigation 37 | ``` 38 | 然后,安装 react-native-gesture-handler。 39 | 40 | ``` 41 | $ yarn add react-native-gesture-handler 42 | ``` 43 | Link 所有的原生依赖 44 | 45 | ``` 46 | $ react-native link react-native-gesture-handler 47 | ``` 48 | 若想获取更多配置和用法,请移步至[react-navigation中文官网](https://reactnavigation.org/zh-Hans/) 49 | 50 | **路由组件封装** 51 | 52 | 1.根目录新建`src/pages/`目录,在`pages`下新建`home/home.js` 53 | 54 | a)`home.js`示例:(所用的historycolors在后文也有示例) 55 | 56 | ```js 57 | import React, { Component } from 'react' 58 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native' 59 | import {colors} from '../../assets/styles/colors-theme'; 60 | import history from '../../common/history'; 61 | 62 | export default class Home extends Component { 63 | render() { 64 | return ( 65 | 66 | Home 67 | history.push(this, '/list', {name: 'niunai'})}> 68 | 跳转到List 69 | 70 | 71 | ) 72 | } 73 | } 74 | 75 | const styles = StyleSheet.flatten({ 76 | button: { 77 | marginTop: 20, 78 | width: 100, 79 | height: 40, 80 | alignItems: 'center', 81 | justifyContent: 'center', 82 | backgroundColor: colors.statusBarColor 83 | }, 84 | buttonText: { 85 | color: '#fff' 86 | } 87 | }) 88 | ``` 89 | 90 | b)在`pages`下新建`page1/page1.js`、`page2/page2.js`、`page3/page3.js`、`list/list.js`、`detail/detail.js`,示例如下:(仅写一个示例,其他自行模仿) 91 | 92 | `page1`示例: 93 | 94 | ```js 95 | import React, { Component } from 'react' 96 | import { View, Text } from 'react-native' 97 | 98 | export default class Page1 extends Component { 99 | render() { 100 | return ( 101 | 102 | Page1 103 | 104 | ) 105 | } 106 | } 107 | ``` 108 | 109 | c)在`pages`下新建`router.js`,示例如下(`tab-nav`下文会讲到): 110 | 111 | ```js 112 | import {createStackNavigator, createAppContainer} from 'react-navigation' 113 | import List from './pages/list/list'; 114 | import Detail from './pages/detail/detail'; 115 | import {TabNav} from './common/tab-nav'; 116 | 117 | function generateRoute(path, screen) { 118 | return { 119 | path, 120 | screen 121 | } 122 | } 123 | 124 | 125 | const stackRouterMap = { 126 | list: generateRoute('/list', List), 127 | detail: generateRoute('/detail', Detail), 128 | main: TabNav 129 | } 130 | 131 | const stackNavigate = createStackNavigator(stackRouterMap, { 132 | initialRouteName: 'main', 133 | headerMode: 'none' 134 | }) 135 | 136 | const Router = createAppContainer(stackNavigate) 137 | 138 | export default Router 139 | ``` 140 | 141 | d)`tab-nav`的封装: `src`目录下新建`common/tab-nav.js`,示例如下(Icon 下文有介绍): 142 | 143 | ```js 144 | import React from 'react' 145 | import Icon from 'react-native-vector-icons/Ionicons' 146 | import {createBottomTabNavigator} from 'react-navigation'; 147 | 148 | import Home from '../pages/home/home'; 149 | import Page3 from '../pages/page3/page3'; 150 | import Page1 from '../pages/page1/page1'; 151 | import Page2 from '../pages/page2/page2'; 152 | import {colors} from '../assets/styles/colors-theme'; 153 | 154 | const TabRouterMap = { 155 | home: { 156 | screen: Home, 157 | navigationOptions: { 158 | tabBarLabel: 'Home', 159 | tabBarIcon:({focused}) => ( 160 | 165 | ) 166 | } 167 | }, 168 | page1: { 169 | screen: Page1, 170 | navigationOptions: { 171 | tabBarLabel: 'Page1', 172 | tabBarIcon:({focused}) => ( 173 | 178 | ) 179 | } 180 | }, 181 | page2: { 182 | screen: Page2, 183 | navigationOptions: { 184 | tabBarLabel: 'Page2', 185 | tabBarIcon:({focused}) => ( 186 | 191 | ) 192 | } 193 | }, 194 | page3: { 195 | screen: Page3, 196 | navigationOptions: { 197 | tabBarLabel: 'Page3', 198 | tabBarIcon:({focused}) => ( 199 | 204 | ) 205 | } 206 | } 207 | } 208 | 209 | export const TabNav = createBottomTabNavigator(TabRouterMap,{ 210 | initialRouteName: 'home', 211 | tabBarOptions: { 212 | //当前选中的tab bar的文本颜色和图标颜色 213 | activeTintColor: colors.statusBarColor, 214 | //当前未选中的tab bar的文本颜色和图标颜色 215 | inactiveTintColor: '#000', 216 | //是否显示tab bar的图标,默认是false 217 | showIcon: true, 218 | //showLabel - 是否显示tab bar的文本,默认是true 219 | showLabel: true, 220 | //是否将文本转换为大小,默认是true 221 | upperCaseLabel: false, 222 | //material design中的波纹颜色(仅支持Android >= 5.0) 223 | pressColor: 'red', 224 | //按下tab bar时的不透明度(仅支持iOS和Android < 5.0). 225 | pressOpacity: 0.8, 226 | //tab bar的样式 227 | // style: { 228 | // backgroundColor: '#fff', 229 | // paddingBottom: 1, 230 | // borderTopWidth: 0.2, 231 | // paddingTop:1, 232 | // borderTopColor: '#ccc', 233 | // }, 234 | //tab bar的文本样式 235 | labelStyle: { 236 | fontSize: 11, 237 | margin: 1 238 | }, 239 | //tab 页指示符的样式 (tab页下面的一条线). 240 | indicatorStyle: {height: 0}, 241 | }, 242 | //tab bar的位置, 可选值: 'top' or 'bottom' 243 | tabBarPosition: 'bottom', 244 | //是否允许滑动切换tab页 245 | swipeEnabled: true, 246 | //是否在切换tab页时使用动画 247 | animationEnabled: false, 248 | //是否懒加载 249 | lazy: true, 250 | //返回按钮是否会导致tab切换到初始tab页? 如果是,则设置为initialRoute,否则为none。 缺省为initialRoute。 251 | backBehavior: 'none' 252 | }) 253 | ``` 254 | 255 | e)在`App.js`中使用路由: 256 | 257 | ```js 258 | import React, {Component} from 'react'; 259 | import {StatusBar} from 'react-native'; 260 | import {SafeAreaView} from 'react-navigation' 261 | import Router from './src/router'; 262 | import {colors} from './src/assets/styles/colors-theme'; 263 | 264 | export default class App extends Component { 265 | render() { 266 | return ( 267 | 274 | 280 | 281 | 282 | ); 283 | } 284 | } 285 | ``` 286 | 287 | **2.history的基本封装** 288 | history是控制路由跳转的模块,一般封装出push、replace、goback、pop等,在`src`目录下新建`common/history.js`,示例如下: 289 | 290 | ```js 291 | const NAVIGATION_THROTTLE = 1000; // 1s内不准重复跳转 292 | const lastNavigationTimeStamps = {}; 293 | 294 | /** 295 | * 校验页面跳转参数 防止同一个path在很短的时间内被反复调用 296 | * @param path 297 | */ 298 | function validate(path) { 299 | const timestamp = new Date().valueOf(); 300 | if(lastNavigationTimeStamps[path] && (timestamp - lastNavigationTimeStamps[path]) < NAVIGATION_THROTTLE) { 301 | lastNavigationTimeStamps[path] = timestamp 302 | return false 303 | } else { 304 | lastNavigationTimeStamps[path] = timestamp 305 | } 306 | 307 | return true 308 | } 309 | 310 | /** 311 | * 处理路由跳转的状态 312 | * @param prevState 313 | * @param newState 314 | * @param action 315 | */ 316 | export function handleNavigationChange(prevState, newState, action) { 317 | console.log('@@@@@ prevState', prevState) 318 | console.log('@@@@@ newState', newState) 319 | console.log('@@@@@ action', action) 320 | } 321 | 322 | const history = { 323 | push: (instance, path, state) => { 324 | if(validate(path)) { 325 | const navigationController = instance.props.navigation; 326 | const nativePath = 327 | path.charAt(0) === '/' ? path.substring(1, path.length) : path; 328 | 329 | navigationController.push(nativePath, state) 330 | } 331 | }, 332 | replace: (instance, path, state) => { 333 | if(validate(path)) { 334 | const navigationController = instance.props.navigation; 335 | const nativePath = 336 | path.charAt(0) === '/' ? path.substring(1, path.length) : path; 337 | 338 | navigationController.replace(nativePath, state) 339 | } 340 | }, 341 | goBack: (instance) => { 342 | if(instance) { 343 | const navigationController = instance.props.navigation; 344 | navigationController.goBack() 345 | } 346 | }, 347 | pop: (instance, n) => { 348 | if(instance) { 349 | const navigationController = instance.props.navigation; 350 | navigationController.pop(-1 * n || -1) 351 | } 352 | } 353 | } 354 | 355 | export default history; 356 | ``` 357 | 358 | 修改`App.js`中的`Router` 359 | 360 | ```js 361 | 362 | 363 | 改为 364 | 365 | import {handleNavigationChange} from './src/common/history' 366 | 369 | ``` 370 | 371 | ## 字体图标库 372 | app中需要用到大量的小图标,本文选择[react-native-vector-icons](https://oblador.github.io/react-native-vector-icons/) 373 | 374 | ```js 375 | $ yarn add react-native-vector-icons 376 | $ react-native link react-native-vector-icons 377 | 378 | 使用方法: 379 | import Icon from 'react-native-vector-icons/Ionicons' 380 | 381 | 385 | 386 | ``` 387 | 388 | ## 其他配置 389 | **1.样式配置** 390 | `src`目录下新建`assets/styles/colors-theme.js`示例:全局控制整个APP所需的颜色 391 | 392 | ```js 393 | export const colors = { 394 | statusBarColor: '#23A2FF' 395 | } 396 | ``` 397 | 398 | **2.服务基础配置** 399 | `src/common`目录下新建`constants.js`, 用于配置全局所需的服务地址、设备号、设备类型、版本号、分页数量等等 400 | 401 | ```js 402 | (如果不需要设备号则无需下载) 403 | $ yarn add react-native-device-info 404 | $ react-native link react-native-device-info 405 | 406 | 407 | /** 408 | * 提供基础配置信息 409 | * constants.js 提供如服务器地址、分页数量、设备类型、设备号、版本号等配置 410 | */ 411 | import { Platform } from 'react-native' 412 | import DeviceInfo from 'react-native-device-info' 413 | 414 | export default { 415 | serverUrl: 'http://127.0.0.1:3600/portal', 416 | pageSize: 10, 417 | deviceType: Platform.OS.toUpperCase(), 418 | deviceNo: DeviceInfo.getUniqueID().replace('-').substr(0, 12), 419 | versionName: DeviceInfo.getVersion(), //也可写死如'1.0.0' 420 | } 421 | 422 | ``` 423 | 424 | **3.服务报错配置** 425 | `src/common`目录下新建`service-error.js`, 用于配置全局服务报错 426 | 427 | ```js 428 | /** 429 | * 服务报错处理 430 | */ 431 | 432 | export default class ServiceError extends Error{ 433 | constructor(code, message){ 434 | super(message); 435 | this.code = code; 436 | this.hash = Math.random() * 100000000000000000; 437 | this.signature = 'ServiceError'; 438 | } 439 | } 440 | ``` 441 | 442 | **4.code配置** 443 | `src/common`目录下新建`code.js`, 用于配置全局请求code 444 | 445 | ```js 446 | /** 447 | * code.js提供全局的请求服务字段处理 448 | */ 449 | export default { 450 | SUCCESS: 'SUCCESS', //请求成功 451 | REQUEST_FAILED: 'REQUEST_FAILED', //请求失败 452 | REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', //请求超时 453 | UN_KNOWN_ERROR: 'UN_KNOWN_ERROR', //未知错误 454 | TOKEN_INVALID: 'TOKEN_INVALID', //token失效 455 | SESSION_TIMEOUT: 'SESSION_TIMEOUT', //会话超时 456 | } 457 | ``` 458 | 459 | 460 | ## 封装顶部导航栏NaviBar 461 | 顶部导航栏用于显示当前页面的标题,操作路由的跳转,放置部分功能模块,如分享、弹框、设置等 462 | 463 | 新增`prop-types`,用于封装类型校验 464 | ``` 465 | $ yarn add prop-types 466 | ``` 467 | 468 | 在`src`下新建`components/navi-bar.js`,示例如下: 469 | ```js 470 | import React, { Component } from 'react' 471 | import { View, Text, StyleSheet, Dimensions, TouchableOpacity, Platform } from 'react-native' 472 | import PropTypes from 'prop-types' 473 | import {colors} from '../assets/styles/colors-theme'; 474 | import Icon from 'react-native-vector-icons/Ionicons' 475 | 476 | const { width } = Dimensions.get('window') 477 | 478 | export default class NaviBar extends Component { 479 | static propTypes = { 480 | style: PropTypes.object, 481 | leftItem: PropTypes.node, //原则上控制在宽度40的icon 482 | rightItem: PropTypes.node, //原则上控制在宽度40的icon 483 | title: PropTypes.string, 484 | titleColor: PropTypes.string, 485 | onBack: PropTypes.func, 486 | iconColor: PropTypes.string 487 | } 488 | 489 | render() { 490 | const props = this.props; 491 | 492 | return ( 493 | 494 | 495 | { 496 | props.leftItem ? props.leftItem : ( 497 | props.onBack ? ( 498 | 499 | 504 | 505 | ) : 506 | ) 507 | } 508 | 509 | {props.title} 510 | 511 | { 512 | props.rightItem ? props.rightItem : 513 | } 514 | 515 | 516 | ) 517 | } 518 | } 519 | 520 | const styles = StyleSheet.flatten({ 521 | naviBar: { 522 | width, 523 | height: Platform.OS === 'ios' ? 44 : 56, //ios原生导航高度是44,android是56 524 | backgroundColor: colors.statusBarColor, 525 | flexDirection: 'row', 526 | alignItems: 'center', 527 | justifyContent: 'space-between' 528 | } 529 | }) 530 | 531 | ``` 532 | 533 | 修改`list.js`,示例如下: 534 | 535 | ```js 536 | import React, { Component } from 'react' 537 | import {View, Text, TouchableOpacity, StyleSheet} from 'react-native' 538 | import NaviBar from '../../components/navi-bar'; 539 | import history from '../../common/history'; 540 | import {colors} from '../../assets/styles/colors-theme'; 541 | 542 | export default class List extends Component { 543 | render() { 544 | return ( 545 | 546 | 550 | 551 | List 552 | history.push(this, '/detail', {name: 'suannai'})}> 553 | 跳转到Detail 554 | 555 | 556 | 557 | ) 558 | } 559 | } 560 | 561 | const styles = StyleSheet.flatten({ 562 | button: { 563 | marginTop: 20, 564 | width: 100, 565 | height: 40, 566 | alignItems: 'center', 567 | justifyContent: 'center', 568 | backgroundColor: colors.statusBarColor 569 | }, 570 | buttonText: { 571 | color: '#fff' 572 | } 573 | }) 574 | ``` 575 | 576 | ## 基础服务请求返回封装 577 | 一般来说,大项目都需要统一封装一个基础服务组件,通过这个组件去全局处理request和返回response,处理全局的服务报错。 578 | 579 | 1.`fetch`拦截器的实现: 580 | ```js 581 | $ yarn add fetch-intercept 582 | 583 | 示例如下: 584 | import fetchIntercept from 'fetch-intercept' 585 | 586 | fetchIntercept.register({ 587 | request: function (url, config) { 588 | return [url, config]; 589 | }, 590 | requestError: function (error) { 591 | return Promise.reject(error); 592 | }, 593 | response: function (res) { 594 | return res; 595 | }, 596 | responseError: function (error) { 597 | return Promise.reject(error); 598 | } 599 | }); 600 | 601 | ``` 602 | 603 | 2.在`src`下新建`services/base-service.js`,封装`BaseService`(constantsServiceErrorcode的封装在上面) 604 | 605 | ```js 606 | /** 607 | * 基础服务类封装 608 | * BaseService 609 | */ 610 | import fetchIntercept from 'fetch-intercept' 611 | import constants from '../common/constants'; 612 | import ServiceError from '../common/service-error'; 613 | import code from '../common/code'; 614 | 615 | const fetchApi = fetch; // eslint-disable-line 616 | 617 | fetchIntercept.register({ 618 | request: function (url, config) { 619 | return [url, config]; 620 | }, 621 | requestError: function (error) { 622 | return Promise.reject(error); 623 | }, 624 | response: function (res) { 625 | return res; 626 | }, 627 | responseError: function (error) { 628 | return Promise.reject(error); 629 | } 630 | }); 631 | 632 | export default class BaseService { 633 | constructor(props){ 634 | if(props && props.showLoading){ 635 | this.showLoading = props.showLoading; 636 | } 637 | if(props && props.hideLoading){ 638 | this.hideLoading = props.hideLoading; 639 | } 640 | } 641 | 642 | async request(method, url, params, errorMsgIndex, showLoading = true, acceptType = 'application/json') { 643 | // 如果url不全,则自动补全 644 | if(url.indexOf('http://') < 0 && url.indexOf('https://') < 0){ 645 | url = constants.serverUrl + '/' + url; 646 | } 647 | 648 | if(showLoading && this.showLoading){ 649 | this.showLoading(); 650 | } 651 | 652 | let res = null 653 | let timer = null 654 | 655 | try { 656 | const options = { 657 | method: method, 658 | credentials: 'include', 659 | headers: { 660 | 'content-type': 'application/json', 661 | 'accept': acceptType, 662 | 'Cache-Control': 'no-cache' 663 | } 664 | } 665 | 666 | if(method === 'POST' || method === 'PUT') { 667 | params.DeviceType = constants.deviceType 668 | params.DeviceNo = constants.deviceNo 669 | 670 | options.body = JSON.stringify(params || {}) 671 | } 672 | 673 | res = await fetchApi(url, options) 674 | } catch (e) { 675 | if(this.hideLoading){ 676 | if(timer) { 677 | clearTimeout(timer) 678 | } 679 | timer = setTimeout(() => { 680 | this.hideLoading() 681 | }, 2000) 682 | } 683 | 684 | throw new ServiceError(code.REQUEST_FAILED, '网络请求失败') 685 | } 686 | 687 | if(res.status && res.status >= 200 && res.status < 300) { 688 | const contentType = res.headers.get('Content-Type') 689 | 690 | if(this.hideLoading){ 691 | this.hideLoading() 692 | } 693 | 694 | if(contentType.indexOf('text/plain') >= 0 || contentType.indexOf('text/html') >= 0){ 695 | return res.text() 696 | }else{ 697 | const responseJson = await res.json(); 698 | if (responseJson && !responseJson.jsonError) { 699 | return responseJson 700 | } else { 701 | throw new ServiceError(responseJson.jsonError[0]._exceptionMessageCode || code.REQUEST_FAILED, responseJson.jsonError[0]._exceptionMessage); 702 | } 703 | } 704 | } else { 705 | if(this.hideLoading){ 706 | if(timer) { 707 | clearTimeout(timer) 708 | } 709 | 710 | timer = setTimeout(() => { 711 | this.hideLoading() 712 | }, 2000) 713 | } 714 | 715 | if (res.status === 401) { 716 | throw new ServiceError(code.REQUEST_TIMEOUT, res.data.message); 717 | } else if (res.ok) { 718 | try { 719 | const responseJson = await res.json(); 720 | const { message } = responseJson; 721 | throw new ServiceError(code.REQUEST_FAILED, message); 722 | } catch (e) { 723 | throw new ServiceError(code.REQUEST_FAILED, '服务未知错误'); 724 | } 725 | } 726 | } 727 | } 728 | 729 | /** 730 | * GET 后台数据 731 | * @param url 732 | * @param errorMsg 报错消息 733 | * @returns {Promise<*>} 734 | */ 735 | async fetchJson(url, errorMsg, showLoading = true){ 736 | return await this.request('GET', url, null, errorMsg, showLoading) 737 | } 738 | 739 | /** 740 | * POST请求 741 | * @param url 742 | * @param params 743 | * @param errorMsg 报错消息 744 | * @returns {Promise.} 745 | */ 746 | async postJson(url, params, errorMsg, showLoading = true){ 747 | return await this.request('POST', url, params, errorMsg, showLoading) 748 | } 749 | 750 | } 751 | ``` 752 | 753 | **使用`BaseService`** 754 | 在`src/services`下新建`list-service.js` 755 | 756 | ```js 757 | /** 758 | * 列表页服务 759 | */ 760 | import BaseService from './base-service'; 761 | 762 | export default class ListService extends BaseService { 763 | /** 764 | * 获取列表 765 | * @return {Promise} 766 | */ 767 | async getList() { 768 | const res = await this.postJson('qryList.do', {}) 769 | 770 | return res; 771 | } 772 | } 773 | ``` 774 | 775 | 修改`list.js` 776 | 777 | ```js 778 | ... 779 | import ListService from '../../services/list-service'; 780 | 781 | export default class List extends Component { 782 | constructor(props) { 783 | super(props) 784 | ... 785 | this.listService = new ListService(props) 786 | } 787 | 788 | async componentDidMount() { 789 | const res = await this.listService.getList(); 790 | ... 791 | } 792 | 793 | ... 794 | } 795 | 796 | ``` 797 | 798 | ## 基础服务的使用 799 | 800 | **1.封装LoadingView** 801 | 封装`LoadingView`是给全局提供一个加载动画,服务器的加载需要时间,一般以加载动画来过渡。目前我选择国际上最火的[lottie](http://airbnb.io/lottie/#/react-native),动画所需`json`文件自行去[lottiefiles](https://lottiefiles.com/recent)下载 802 | 803 | ```js 804 | $ yarn add lottie-react-native 805 | $ react-native link lottie-react-native 806 | $ react-native link lottie-ios 807 | 808 | 针对IOS的XCode配置 809 | General > Embedded Binaries > add Lottie.framework 810 | ``` 811 | 812 | 在`src/common`下新建`loading.js`, 同时在`src/assets`下新建`animations/loading.json` 813 | 814 | ```js 815 | import React, { Component } from 'react' 816 | import {View, Dimensions, StyleSheet} from 'react-native'; 817 | import LottieView from 'lottie-react-native'; 818 | import LoadingAnimation from '../assets/animations/loading'; 819 | 820 | const { width, height } = Dimensions.get('window') 821 | 822 | export default class LoadingView extends Component { 823 | render(){ 824 | if(this.props.visible){ 825 | return ( 826 | 827 | 828 | 829 | 830 | 831 | ) 832 | }else{ 833 | return 834 | } 835 | } 836 | } 837 | 838 | const styles = StyleSheet.flatten({ 839 | wrapper: { 840 | position: 'absolute', 841 | top: 0, 842 | left: 0, 843 | width: width, 844 | height: height, 845 | backgroundColor: 'rgba(0, 0, 0, 0.2)' 846 | }, 847 | loading:{ 848 | position: 'absolute', 849 | top: height / 2 - 100, 850 | left: width / 2 - 70, 851 | width: 140, 852 | height: 140 853 | } 854 | }); 855 | ``` 856 | 857 | 在`App.js`中使用`LoadingView`,引入LoadingView放在Router下方就行 858 | 859 | ```js 860 | import LoadingView from './src/common/loading' 861 | 862 | ... 863 | 866 | 0} /> 867 | ... 868 | ``` 869 | 870 | **2.loading-hoc高阶组件封装** 871 | 872 | 在`src`下新建`hocs/loading-hoc.js`, loading-hoc是一个高阶组件,用于在页面外部以`@`修饰符引用`LoadingHoc`(查看Event事件消息总线封装) 873 | 874 | ```js 875 | import React, {Component} from 'react'; 876 | import {Dimensions, View} from 'react-native'; 877 | const { width, height } = Dimensions.get('window') 878 | import Event from '../common/event' 879 | 880 | export default function LoadingHoc(WrappedComponent) { 881 | return class ComposedComponent extends Component { 882 | showLoading(){ 883 | Event.emit('SHOW_LOADING') 884 | } 885 | 886 | hideLoading(){ 887 | Event.emit('HIDE_LOADING') 888 | } 889 | 890 | render() { 891 | const props = {...this.props, ...{ 892 | showLoading: this.showLoading.bind(this), 893 | hideLoading: this.hideLoading.bind(this) 894 | }}; 895 | return ( 896 | 897 | 898 | 899 | ); 900 | } 901 | }; 902 | } 903 | 904 | ``` 905 | 906 | 在`List`中引入`LoadingHoc` 907 | 908 | 909 | ```js 910 | ... 911 | @LoadingHoc 912 | export default class List extends Component { 913 | constructor(props) { 914 | super(props) 915 | ... 916 | this.listService = new ListService(props) 917 | } 918 | 919 | async componentDidMount() { 920 | const res = await this.listService.getList(); 921 | ... 922 | } 923 | } 924 | 925 | ``` 926 | 927 | **3.全局事件消息总线封装** 928 | 在`src/common`下新建`notification-center.js`和`event.js` 929 | 930 | 931 | notification-center.js示例: 932 | 933 | ```js 934 | const __notices = []; 935 | /** 936 | * addNotification 937 | * 注册通知对象方法 938 | * 939 | * 参数: 940 | * name: 注册名,一般let在公共类中 941 | * selector: 对应的通知方法,接受到通知后进行的动作 942 | * observer: 注册对象,指Page对象 943 | */ 944 | function addNotification(name, selector, observer) { 945 | if (name && selector) { 946 | if (!observer) { 947 | console.log( 948 | "addNotification Warning: no observer will can't remove notice" 949 | ); 950 | } 951 | const newNotice = { 952 | name: name, 953 | selector: selector, 954 | observer: observer 955 | }; 956 | 957 | addNotices(newNotice); 958 | } else { 959 | console.log('addNotification error: no selector or name'); 960 | } 961 | } 962 | 963 | /** 964 | * 仅添加一次监听 965 | * 966 | * 参数: 967 | * name: 注册名,一般let在公共类中 968 | * selector: 对应的通知方法,接受到通知后进行的动作 969 | * observer: 注册对象,指Page对象 970 | */ 971 | function addOnceNotification(name, selector, observer) { 972 | if (__notices.length > 0) { 973 | for (let i = 0; i < __notices.length; i++) { 974 | const notice = __notices[i]; 975 | if (notice.name === name) { 976 | if (notice.observer === observer) { 977 | return; 978 | } 979 | } 980 | } 981 | } 982 | this.addNotification(name, selector, observer); 983 | } 984 | 985 | function addNotices(newNotice) { 986 | // if (__notices.length > 0) { 987 | // for (var i = 0; i < __notices.length; i++) { 988 | // var hisNotice = __notices[i]; 989 | // //当名称一样时进行对比,如果不是同一个 则放入数组,否则跳出 990 | // if (newNotice.name === hisNotice.name) { 991 | // if (!cmp(hisNotice, newNotice)) { 992 | // __notices.push(newNotice); 993 | // } 994 | // return; 995 | // }else{ 996 | // __notices.push(newNotice); 997 | // } 998 | 999 | // } 1000 | // } else { 1001 | 1002 | // } 1003 | 1004 | __notices.push(newNotice); 1005 | } 1006 | 1007 | /** 1008 | * removeNotification 1009 | * 移除通知方法 1010 | * 1011 | * 参数: 1012 | * name: 已经注册了的通知 1013 | * observer: 移除的通知所在的Page对象 1014 | */ 1015 | 1016 | function removeNotification(name, observer) { 1017 | console.log('removeNotification:' + name); 1018 | for (let i = 0; i < __notices.length; i++) { 1019 | const notice = __notices[i]; 1020 | if (notice.name === name) { 1021 | if (notice.observer === observer) { 1022 | __notices.splice(i, 1); 1023 | return; 1024 | } 1025 | } 1026 | } 1027 | } 1028 | 1029 | /** 1030 | * postNotificationName 1031 | * 发送通知方法 1032 | * 1033 | * 参数: 1034 | * name: 已经注册了的通知 1035 | * info: 携带的参数 1036 | */ 1037 | 1038 | function postNotificationName(name, info) { 1039 | console.log('postNotificationName:' + name); 1040 | if (__notices.length === 0) { 1041 | console.log("postNotificationName error: u hadn't add any notice."); 1042 | return; 1043 | } 1044 | 1045 | for (let i = 0; i < __notices.length; i++) { 1046 | const notice = __notices[i]; 1047 | if (notice.name === name) { 1048 | notice.selector(info); 1049 | } 1050 | } 1051 | } 1052 | 1053 | // 用于对比两个对象是否相等 1054 | function cmp(x, y) { // eslint-disable-line 1055 | // If both x and y are null or undefined and exactly the same 1056 | if (x === y) { 1057 | return true; 1058 | } 1059 | 1060 | // If they are not strictly equal, they both need to be Objects 1061 | if (!(x instanceof Object) || !(y instanceof Object)) { 1062 | return false; 1063 | } 1064 | 1065 | // They must have the exact same prototype chain, the closest we can do is 1066 | // test the constructor. 1067 | if (x.constructor !== y.constructor) { 1068 | return false; 1069 | } 1070 | 1071 | for (const p in x) { 1072 | // Inherited properties were tested using x.constructor === y.constructor 1073 | if (x.hasOwnProperty(p)) { 1074 | // Allows comparing x[ p ] and y[ p ] when set to undefined 1075 | if (!y.hasOwnProperty(p)) { 1076 | return false; 1077 | } 1078 | 1079 | // If they have the same strict value or identity then they are equal 1080 | if (x[p] === y[p]) { 1081 | continue; 1082 | } 1083 | 1084 | // Numbers, Strings, Functions, Booleans must be strictly equal 1085 | if (typeof x[p] !== 'object') { 1086 | return false; 1087 | } 1088 | 1089 | // Objects and Arrays must be tested recursively 1090 | if (!Object.equals(x[p], y[p])) { 1091 | return false; 1092 | } 1093 | } 1094 | } 1095 | 1096 | for (const p in y) { 1097 | // allows x[ p ] to be set to undefined 1098 | if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) { 1099 | return false; 1100 | } 1101 | } 1102 | return true; 1103 | } 1104 | 1105 | module.exports = { 1106 | addNotification: addNotification, 1107 | removeNotification: removeNotification, 1108 | postNotificationName: postNotificationName, 1109 | addOnceNotification: addOnceNotification 1110 | }; 1111 | 1112 | ``` 1113 | 1114 | 1115 | event.js示例: 1116 | 1117 | ```js 1118 | /** 1119 | * 一个JavaScript 事件消息总线 1120 | */ 1121 | import NotificationCenter from './notification-center'; 1122 | 1123 | export default class Event { 1124 | static listen(eventName, callback, observer) { 1125 | NotificationCenter.addNotification(eventName, callback, observer); 1126 | } 1127 | static emit(eventName, params) { 1128 | NotificationCenter.postNotificationName(eventName, params); 1129 | } 1130 | static remove(eventName, observer) { 1131 | NotificationCenter.removeNotification(eventName, observer); 1132 | } 1133 | } 1134 | 1135 | ``` 1136 | 1137 | 1138 | **4.全局报错处理** 1139 | 在`src/common`下新建`global-error-handler.js` 1140 | 1141 | 1142 | global-error-handler.js示例: 1143 | 1144 | 1145 | ```js 1146 | import code from './code'; 1147 | import Event from './event' 1148 | 1149 | export function handleErrors(error){ 1150 | if(error && error.signature && error.signature === 'ServiceError') { 1151 | defaultServiceErrorHandler(error); 1152 | }else{ 1153 | defaultErrorHandler(error); 1154 | } 1155 | } 1156 | 1157 | function defaultServiceErrorHandler(error){ 1158 | if(error && error.code === code.SESSION_TIMEOUT){ 1159 | Event.emit('GLOBAL_ERROR', { 1160 | type: 'SESSION_TIMEOUT' 1161 | }) 1162 | }else if(error && error.message) { 1163 | Event.emit('GLOBAL_ERROR', { 1164 | type: 'SERVICE_ERROR', 1165 | message: error.message 1166 | }) 1167 | }else { 1168 | Event.emit('GLOBAL_ERROR', { 1169 | type: 'SERVICE_ERROR', 1170 | message: '服务出错,请稍后再试.' 1171 | }) 1172 | } 1173 | } 1174 | 1175 | function defaultErrorHandler(error){ 1176 | if(error && error.message) { 1177 | Event.emit('GLOBAL_ERROR', { 1178 | type: 'SERVICE_ERROR', 1179 | message: error.message 1180 | }) 1181 | }else { 1182 | Event.emit('GLOBAL_ERROR', { 1183 | type: 'SERVICE_ERROR', 1184 | message: '服务出错,请稍后再试.' 1185 | }) 1186 | } 1187 | } 1188 | 1189 | ``` 1190 | 1191 | 1192 | **5.全局监听** 1193 | 1194 | ```js 1195 | $ yarn add promise-polyfill 1196 | $ yarn add @ant-design/react-native 1197 | $ react-native link @ant-design/icons-react-native 1198 | ``` 1199 | 1200 | `App.js`添加`promise`的报错处理和使用`antd-mobileRN版本`来进行弹框报错。 1201 | 1202 | 注:**如果需要使用`Modal`以及`Toast`还需要在 `App` 的入口处加上`Provider`, 因`mobx`也需要使用`Provider`, 本文另定义为`ProviderAntd`, `mobx`的使用教程在下面可找到** 1203 | 1204 | ```js 1205 | import {Provider as ProviderAntd, Modal} from '@ant-design/react-native' 1206 | import LoadingView from './src/common/loading'; 1207 | import {handleErrors} from './src/common/global-error-handler'; 1208 | import Event from './src/common/event'; 1209 | 1210 | @observer 1211 | export default class App extends Component { 1212 | constructor(props) { 1213 | super(props) 1214 | this.timer = null 1215 | this.state = { 1216 | loadingCount: 0 1217 | } 1218 | 1219 | require('promise/setimmediate/rejection-tracking').enable({ 1220 | allRejections: true, 1221 | onUnhandled: (id, error) => { 1222 | handleErrors(error); 1223 | } 1224 | }) 1225 | 1226 | this._handleGlobalError = this.handleGlobalError.bind(this) 1227 | this._handleShowLoading = this.handleShowLoading.bind(this) 1228 | this._handleHideLoading = this.handleHideLoading.bind(this) 1229 | } 1230 | 1231 | componentDidMount() { 1232 | // 监听全局报错 1233 | Event.listen('GLOBAL_ERROR', this._handleGlobalError, this) 1234 | 1235 | // 显示加载动画 1236 | Event.listen('SHOW_LOADING', this._handleShowLoading, this) 1237 | 1238 | // 隐藏加载动画 1239 | Event.listen('HIDE_LOADING', this._handleHideLoading, this) 1240 | } 1241 | 1242 | //组件卸载之前移除监听 1243 | componentWillUnmount() { 1244 | Event.remove('GLOBAL_ERROR', this) 1245 | Event.remove('SHOW_LOADING', this) 1246 | Event.remove('HIDE_LOADING', this) 1247 | } 1248 | 1249 | render() { 1250 | return ( 1251 | 1252 | 1253 | 1254 | ... 1255 | 0} /> 1256 | 1257 | 1258 | 1259 | ); 1260 | } 1261 | 1262 | /** 1263 | * showLoading 1264 | */ 1265 | handleShowLoading() { 1266 | if(this.timer) { 1267 | clearTimeout(this.timer); 1268 | } 1269 | 1270 | this.timer = setTimeout(() => { 1271 | this.setState({ 1272 | loadingCount: this.state.loadingCount + 1 1273 | }) 1274 | }, 50) 1275 | } 1276 | 1277 | /** 1278 | * hideLoading 1279 | * @param bForece 1280 | */ 1281 | handleHideLoading(bForece){ 1282 | if(this.timer){ 1283 | clearTimeout(this.timer); 1284 | } 1285 | 1286 | this.timer = setTimeout(() => { 1287 | if(this.state.loadingCount > 0){ 1288 | this.setState({ 1289 | loadingCount: (bForece ? 0 : this.state.loadingCount - 1) 1290 | }); 1291 | } 1292 | }, 50) 1293 | } 1294 | 1295 | /** 1296 | * 全局报错处理 1297 | * @param event 1298 | */ 1299 | handleGlobalError(event) { 1300 | // 报错时,取消加载动画 1301 | if(this.state.loadingCount > 0){ 1302 | this.handleHideLoading(true) 1303 | } 1304 | 1305 | if(event && event.type){ 1306 | switch(event.type){ 1307 | case 'SESSION_TIMEOUT': 1308 | Modal.alert('会话超时', '您的会话已超时,请重新登录') 1309 | break; 1310 | case 'SERVICE_ERROR': 1311 | if(event.message) { 1312 | Modal.alert('出错了', event.message) 1313 | } 1314 | break; 1315 | default: 1316 | if(event.message) { 1317 | Modal.alert('温馨提示', '系统未知异常') 1318 | } 1319 | break; 1320 | } 1321 | } 1322 | } 1323 | } 1324 | ``` 1325 | 1326 | 1327 | ## mobx的使用 1328 | 1329 | ``` 1330 | $ yarn add mobx mobx-react 1331 | $ yarn add @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-object-rest-spread @babel/plugin-transform-classes @babel/plugin-transform-flow-strip-types @babel/plugin-transform-runtime @babel/polyfill @babel/preset-env @babel/preset-flow @babel/preset-react babel-loader babel-plugin-import babel-plugin-module-resolver babel-plugin-transform-runtime babel-polyfill babel-preset-es2015 babel-preset-react babel-preset-react-native babel-preset-react-native-stage-0 babel-preset-react-native-syntax -D 1332 | ``` 1333 | 1334 | `.babelrc`或`babel.config.js`添加如下代码: 1335 | 1336 | ```js 1337 | { 1338 | presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'], 1339 | plugins: [ 1340 | '@babel/transform-flow-strip-types', 1341 | [ 1342 | '@babel/plugin-proposal-decorators', { 'legacy' : true } 1343 | ], 1344 | [ 1345 | '@babel/plugin-proposal-class-properties', {'loose': true} 1346 | ], 1347 | [ 1348 | '@babel/plugin-transform-runtime', {} 1349 | ], 1350 | ['import', { 'libraryName': '@ant-design/react-native' }] 1351 | ] 1352 | } 1353 | ``` 1354 | 1355 | 在`src`下新建`stores/app-store.js`和`stores/index.js` 1356 | 1357 | `stores/app-store.js`示例: 1358 | 1359 | ```js 1360 | import {observable, action} from 'mobx' 1361 | 1362 | class AppStore { 1363 | @observable 1364 | list = [] 1365 | 1366 | @observable 1367 | timer = 0 1368 | 1369 | @action 1370 | setList(data){ 1371 | this.list = data 1372 | } 1373 | 1374 | @action 1375 | resetTimer() { 1376 | this.timer = 0 1377 | } 1378 | 1379 | @action 1380 | tick() { 1381 | this.timer += 1 1382 | } 1383 | } 1384 | 1385 | const appStore = new AppStore() 1386 | export {appStore} 1387 | 1388 | ``` 1389 | 1390 | `stores/index.js`示例: 1391 | 1392 | ``` 1393 | import {appStore} from './app-store' 1394 | 1395 | export {appStore} 1396 | 1397 | ``` 1398 | 1399 | 在`App.js`中新增`mobx`,具体使用参考[mobx中文文档](https://cn.mobx.js.org/) 1400 | 1401 | `App.js`示例: 1402 | 1403 | ```js 1404 | ... 1405 | import { Provider, observer } from 'mobx-react' 1406 | import * as stores from './src/stores/index'; 1407 | 1408 | @observer 1409 | export default class App extends Component { 1410 | ... 1411 | 1412 | render() { 1413 | return ( 1414 | 1415 | ... 1416 | 1417 | ); 1418 | } 1419 | } 1420 | 1421 | ``` 1422 | 1423 | 在`home.js`中引入`mobx`, 统计点击list的次数和给List页传值 1424 | 1425 | ```js 1426 | ... 1427 | import NaviBar from '../../components/navi-bar'; 1428 | import { inject, observer } from 'mobx-react'; 1429 | 1430 | @inject('rootStore') 1431 | @observer 1432 | export default class Home extends Component { 1433 | constructor(props) { 1434 | ... 1435 | this.store = props.rootStore.appStore 1436 | } 1437 | 1438 | render() { 1439 | return ( 1440 | 1441 | 1442 | 1443 | Home 1444 | { 1445 | //添加timer的次数 1446 | this.store.tick(); 1447 | const list = this.store.list; 1448 | 1449 | list.push({ 1450 | number: this.store.timer, 1451 | label: '第'+this.store.timer + '次点击' 1452 | }) 1453 | 1454 | this.store.setList(list) 1455 | history.push(this, '/list', {name: 'niunai'}) 1456 | }}> 1457 | 跳转到List 1458 | 1459 | 1460 | 统计跳转到List的次数: {this.store.timer} 1461 | 1462 | { 1463 | this.store.setList([]); 1464 | this.store.resetTimer(); 1465 | }}> 1466 | 重置List和timer 1467 | 1468 | 1469 | 1470 | ) 1471 | } 1472 | } 1473 | ``` 1474 | 1475 | 1476 | 在`list.js`中引入`mobx`, 统计点击list的次数和渲染`list` 1477 | 1478 | ```js 1479 | import React, { Component } from 'react' 1480 | import {View, Text, TouchableOpacity, StyleSheet, Dimensions} from 'react-native' 1481 | import NaviBar from '../../components/navi-bar'; 1482 | import history from '../../common/history'; 1483 | import {colors} from '../../assets/styles/colors-theme'; 1484 | import ListService from '../../services/list-service'; 1485 | import LoadingHoc from '../../hocs/loading-hoc'; 1486 | import {inject, observer} from 'mobx-react'; 1487 | 1488 | const { width } = Dimensions.get('window') 1489 | 1490 | @LoadingHoc 1491 | @inject('rootStore') 1492 | @observer 1493 | export default class List extends Component { 1494 | constructor(props) { 1495 | super(props) 1496 | this.state = { 1497 | list: [], 1498 | name: props.navigation.state.params.name 1499 | } 1500 | this.listService = new ListService(props) 1501 | this.store = props.rootStore.appStore 1502 | } 1503 | 1504 | async componentDidMount() { 1505 | const res = await this.listService.getList(); 1506 | 1507 | if(res) { 1508 | this.setState({ 1509 | list: res 1510 | }) 1511 | } 1512 | } 1513 | 1514 | render() { 1515 | return ( 1516 | 1517 | 1521 | 1522 | 1523 | Home页传过来的name:{this.state.name} 1524 | 1525 | 1526 | 统计到{this.store.timer}次跳转到List 1527 | 1528 | 1529 | 1530 | 次数 1531 | 1532 | 1533 | 描述 1534 | 1535 | 1536 | { 1537 | this.store.list.map((item, index) => { 1538 | return ( 1539 | 1540 | 1541 | {item.number} 1542 | 1543 | 1544 | {item.label} 1545 | 1546 | 1547 | ) 1548 | }) 1549 | } 1550 | history.push(this, '/detail', {name: 'suannai'})}> 1551 | 跳转到Detail 1552 | 1553 | 1554 | 1555 | ) 1556 | } 1557 | } 1558 | 1559 | const styles = StyleSheet.flatten({ 1560 | button: { 1561 | marginTop: 20, 1562 | width: 100, 1563 | height: 40, 1564 | alignItems: 'center', 1565 | justifyContent: 'center', 1566 | backgroundColor: colors.statusBarColor 1567 | }, 1568 | buttonText: { 1569 | color: '#fff' 1570 | }, 1571 | number: { 1572 | width: 0.3 * width, 1573 | height: 40, 1574 | alignItems: 'center', 1575 | justifyContent: 'center' 1576 | }, 1577 | label: { 1578 | width: 0.7 * width, 1579 | height: 40, 1580 | alignItems: 'center', 1581 | justifyContent: 'center' 1582 | } 1583 | }) 1584 | 1585 | ``` 1586 | 1587 | **注意:本例中主要是通过在`App.js`中建立一个全局的`store`,命名为`rootStore`来控制所有页面的状态, 1588 | 在`home.js`页去触发`action`增加`timer`和`setList`以及重置`timer`和`list`, 1589 | 在`list.js`去渲染`timer`和`list`** 1590 | 1591 | 1592 | ## Demo总结 1593 | 至此,本文主要描述了如何构架一个react-native的APP,从基础搭建、全局的路由、路由`history`控制、全局`event`事件消息总线封装、全局`error`报错处理和监听、服务配置、基础`service`搭建到使用`mobx`进行全局状态监控就基本上讲完了,其中还包含了如何封装顶部导航栏`NaviBar`、引用字体图标库`react-native-vector-icons`、引入蚂蚁金服的`@ant-design/react-native`, 同时涉及到高阶组件`loading-hoc`的搭建使用和`lottie`动画的使用。 1594 | 1595 | --------------------------------------------------------------------------------