分享一个打造react-native壳,react肉的跨平台三端通吃的APP的记录

3,398 阅读5分钟
原文链接: www.jianshu.com

沉寂了大半年许久,终于准备写下这个篇纯技术干货,结合项目,由于是公司项目,只能用demo作为讲解

# 项目预览

这个性能效果接近原生

image image image image image

# 项目搭建

FB提供的脚手架各创建项目一套:


create-react-native-app //rn app 脚手架

create-react-app //web app 脚手架

# 知识点

## RN:

native app 只需要加载一个全屏的webview即可,牵扯到的坑也只有native和js的通讯了,等会项目会有介绍,这里暴露一下package.json

image

## React:

web app其实页面不多,技能要求也算中规中矩吧

  • RSA MD5 加密

  • 文件的上传

  • 二维码识别

  • socket通讯

  • indexedDB操作

  • js 和native 相互调用

这里也暴露一下package.json

image

# web app 开发

## 路由配置****:


class App extends Component {

 render() {

 return (

 <div className="App">

 <BrowserRouter>

 <div>

 <Route path="/" exact component={Page2} />

 <Route path="/index" component={Index} />

 </div>

 </BrowserRouter>

 </div>

 );

 }

}

如果需要重定向指定页面把Route换成Protected包起来


const Protected = ({component: _comp, ...rest}) => {

 //判断条件  重定向指定路由

let isLogin = true

return <Route { ...rest} render = {props => isLogin ? < _comp /> : <Redirect to ="index"/>}/>

}

## 跨域配置

最简单的代理,在package.json增加:


"proxy": {

 "/yours": {

 "target": "http://your api server/",

 "changeOrigin": true

 }

}

## 封装个****axios****请求基类

axios很强大,简单配置下


import axios from 'axios'

const AUTH_TOKEN = ""

//全局配置

// axios.defaults.baseURL = 'https://api.example.com';

axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;

axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

// http request 请求拦截器,有token值则配置上token值

axios.interceptors.request.use(

config => {

let token = ""

if (token) { // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了

config.headers.Authorization = token;

}

return config;

},

err => {

return Promise.reject(err);

});

// http response 服务器响应拦截器,这里拦截401错误,并重新跳入登页重新获取token

axios.interceptors.response.use(

response => {

return response;

},

error => {

if (error.response) {

switch (error.response.status) {

case 401:

}

}

return Promise.reject(error.response.data)

});

export default axios;

## RSA md 示例

把请求的params传过来,加签后返回string(ps:我们项目里只做加签)


export function encryptData(params) {

 if (_.isEmpty(params)) {

 return ""

 }

//TODO: 数组转json字符

 if (_.get(params,'fixqrs')){

 params = {

 ...params,

 fixqrs:JSON.stringify(params.fixqrs)

 }

 }

 let keyArray = []

 for (let key in params) {

 keyArray.push(key);

 }

 keyArray.sort();

 let sb = "";

 for (let i = 0; i < keyArray.length; i++) {

 sb += keyArray[i] + "=" + params[keyArray[i]] + "&";

 }

let pkcs8key = "your key";

let rsaPrivateKey = new jsrsasign.RSAKey();

/*由于java后台生成的key格式是pkcs8格式 而前端js插件是pkcs1格式解析,故使用KEYUTIL.getKey(pkcs8key)获取私钥*/

rsaPrivateKey = jsrsasign.KEYUTIL.getKey(pkcs8key);

let sigval = rsaPrivateKey.sign(sb.substring(0, sb.length - 1), "sha1");

 return sigval

}

用户名密码比较隐私,md5+salt


export function mdPassword(pw) {

return md5(pw+'your salt')

}

## 文件上传


/**

* 上传文件

 * @param url

 * @param params

 * @param file

 * @returns {AxiosPromise<any>}

 */

export function n_uploadFile(url,params,file) {

//header-sign-params

let param = new FormData()

_.forEach(params,(value,key) => {

param.append(key, value)

})

_.forEach(file,(value,key) => {

param.append(key, value)

})

let config = {

headers: {

'Content-Type': 'multipart/form-data',

}

}

return axios.post(url, param, config)

}

## 二维码识别

获取的file需要转一下url,调用tool里getObjectURL()方法


qrcode.qrcode.decode(getObjectURL(file))

qrcode.qrcode.callback = (d, status) =>{

console.log('',d)

}

## socket****通讯

傻瓜式操作


connect = () => {

let url = `your socket server ip`

this.ws = new Sockette(url, {

timeout: 2e3,

maxAttempts: 10,

onopen: e => console.log('Connected!', e),

onmessage: e => console.log('onmessage!', e),

});

}

close = () => {

if (this.ws){

this.ws.close()

}

}

## indexedDB****操作

关于indexedDB,这个不多说,取代sqlite,高性能储存查询,鉴于js db操作全是异步,各种回调有点恶心,不过dexie利用promise,大大解决了回调地狱,下面看看,如何用这个利器吧,


import Dexie from 'dexie';

const db = new Dexie("Cache_Image_DB");

db.version(1).stores({ file: '++id' });

export const findFiles = async(file) => {

return db.file

.filter(item => item.name === file.name)

.toArray()

}

export const addFile = async(file) => {

return db.file

.add(file)

}

export const deleteFile = async(id) => {

return db.file

.delete(id)

}

## 比较坑的****js native****通讯

先说Android吧,在RN官网的组件WebView,如果你h5里有用到选取相册,那WebView绝对的不支持,因为在Android设备需要你自己去实现,iOS则正常,发消息都是window.postMessage(),收消息也是在onMessage回调里,

再说iOS,官网的WebView组件是基于UIWebView,性能烂的一比,通讯也只能js注入,无法达到WKWebView的高性能,上下文注入的快捷,

综上,rn app就装了两个库

react-native-webview-android

react-native-wkwebview-reborn

所以呢,js && native 相互通讯的方式也有点差异,

//native ==> js


//react-native-webview-android

this.refs.WEBVIEW_REF.postMessage(data);

//react-native-wkwebview

this.refs.WK_WebView.evaluateJavaScript(`receivedMessageFromReactNative(${data})`)

//js


 //android

window.onload = () => {

document.addEventListener('message', (e) => {

this.handleMessage(e.data)

});

}

//ios

window.receivedMessageFromReactNative && window.receivedMessageFromReactNative(data => {

this.handleMessage(data)

})

// js ==> native


export const postMessage = (message) => {

//react-native-webview-android

window.webView&&window.webView.postMessage(message)

//react-native-wkwebview

!_.isEmpty(window,'webkit.messageHandlers.reactNative')&&window.webkit.messageHandlers.reactNative.postMessage(message);

}

//native


//react-native-webview-android

onMessage =(event) => {

console.log('android_webview on message',e)

}

//react-native-wkwebview

onMessage = (e) => {

console.log('wk_webview on message',e.nativeEvent)

}

# RN App****开发

## 按设备加载****webview****组件

只需要判断一下设备,如果是Android就加载react-native-webview-android这个库,iOS就加载react-native-wkwebview,(ps:这里我各自封装了两个库)


render() {

return (

 <View style={styles.container}>

 {

 this.platform === 'ios'?

 <WebViewIOS ref="web_ios" url={SITE_URL}/>

 :

 <WebViewANDROID ref="web_android" url={SITE_URL}/>

 }

 </View>

);

}

## android****设备的返回虚拟键返回处理

android设备有个返回虚拟键,所有拦截一下事件,稍作处理


componentWillMount() {

BackHandler.addEventListener('hardwareBackPress', this.handleBack)

AppState.addEventListener('change', this.handleAppStateChange);

}

componentWillUnmount() {

BackHandler.removeEventListener('hardwareBackPress', this.handleBack)

}

handleBack = () => {

this.refs.web_android.goBack();

return true;

}

# 打包发布

## rn Android 打包部署

1.Android studio 创建keystore

image

2.your project/android/gradle.properties 声明一下变量,


MYAPP_RELEASE_STORE_FILE=your-keystore.keystore

MYAPP_RELEASE_KEY_ALIAS=your alias name

MYAPP_RELEASE_STORE_PASSWORD=your keystore password

MYAPP_RELEASE_KEY_PASSWORD=your key password

在your project/android/app/build.gradle 引用


signingConfigs {

 release {

 keyAlias MYAPP_RELEASE_KEY_ALIAS

 keyPassword MYAPP_RELEASE_KEY_PASSWORD

 storeFile file(MYAPP_RELEASE_STORE_FILE)

 storePassword MYAPP_RELEASE_STORE_PASSWORD

 }

}

3.写个shell 自动打包出apk


#!/bin/sh

cd ~/your rn project name/

curl "localhost:8081/index.android.bundle?platform=android&dev=false&minify=true" -o "android/app/src/main/assets/index.android.bundle"

cd android && ./gradlew assembleRelease

cd app/build/outputs/apk/release

open .

## rn iOS 打包部署

ios打包就相对简单了,前提需要在developer.apple.com 配好provisioning profile 和开发生产证书,这里我就不多说了

同样,写个shell 创建一个生产环境的jsboundle


#!/bin/sh

cd ~/your rn project name/

react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

把release_ios/main.jsbundle,拖到ios项目里, 顺便改一下代码:


//这个是本地jsboundle

// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];

//生产环境的jsboundle,名字要对应哦

 jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

然后你就可以一劳永逸的直接achieve,然后直接上传appstore,或者打出ADHOC包内测一下,

#****中间遇到的错

关于中间的遇到的报错,大概Stack Overflow或者某度都可以,如果你有遇到什么错,也可以在下面评论一下

# demo

rn-webapp-demo