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

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


[TOC]

项目预览

这个性能效果接近原生
image
image
image
image
image

项目搭建

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

1
2
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 开发

路由配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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包起来

1
2
3
4
5
6
const Protected = ({component: _comp, ...rest}) => {
//判断条件 重定向指定路由
let isLogin = true
return <Route { ...rest} render = {props => isLogin ? < _comp /> : <Redirect to ="index"/>}/>
}

跨域配置

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

1
2
3
4
5
6
"proxy": {
"/yours": {
"target": "http://your api server/",
"changeOrigin": true
}
}

封装个axios请求基类

axios很强大,简单配置下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:我们项目里只做加签)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

1
2
3
export function mdPassword(pw) {
return md5(pw+'your salt')
}

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 上传文件
* @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()方法

1
2
3
4
qrcode.qrcode.decode(getObjectURL(file))
qrcode.qrcode.callback = (d, status) =>{
console.log('',d)
}

socket通讯

傻瓜式操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,大大解决了回调地狱,下面看看,如何用这个利器吧,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
//react-native-webview-android
this.refs.WEBVIEW_REF.postMessage(data);
//react-native-wkwebview
this.refs.WK_WebView.evaluateJavaScript(`receivedMessageFromReactNative(${data})`)

//js

1
2
3
4
5
6
7
8
9
10
//android
window.onload = () => {
document.addEventListener('message', (e) => {
this.handleMessage(e.data)
});
}
//ios
window.receivedMessageFromReactNative && window.receivedMessageFromReactNative(data => {
this.handleMessage(data)
})

// js ==> native

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
//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:这里我各自封装了两个库)

1
2
3
4
5
6
7
8
9
10
11
12
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设备有个返回虚拟键,所有拦截一下事件,稍作处理

1
2
3
4
5
6
7
8
9
10
11
12
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 声明一下变量,

1
2
3
4
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 引用

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
#!/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

1
2
3
#!/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项目里, 顺便改一下代码:

1
2
3
4
//这个是本地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

坚持原创技术分享,您的支持将鼓励我继续创作!