分享下这几个月的开发经历以及痛点,希望能对大家有所帮助,能够让大家少踩坑...
框架版本选型
- 开发之初对于 flutter 和 react-native 做了以上对比,再者项目时间紧,业务相当复杂,属于偏业务型APP,最终选择了采用生态较为完善的
react-native,版本也相对保守采用较旧的版本0.59.1; - 一共
2个人(中途加入了1个人)前后大概5个月的开发时间(中途还完成了官网、小程序、运营后台的开发上线)完成了页面168张、组件110+, 最终 App 顺利上线,进入迭代期,并帮助公司拿下了pre-a阶段的融资; - 事实其实也证明了当初的框架选型的正确,不然还真的来不及做完;
下面来抛一下中途遇到的坑,从0到1真真是遇到了不少的坑,尤其本身对原生完全不熟悉... | 因素 | 语言基础 | 语言特性 | 平台支持 | 生态 | 学习成本 | | ----- | ------ | ----- | ----- | ----- | ----- | | flutter | Dart | google新推出的强类型语言 | web/ios/android | 不完善,Google目前主推,生态库不完善 | 新语法特性,从0开始,不过面向对象的语言特性只要接触过一些后端语言的其实也相对容易上手 | | react-native | React | facebook js渐进式框架 | ios/android | 发展时间较长,生态库相对完善,坑点相对较少 | 属性react语法的上手很快,熟悉js的话上手也会快很多,主要是对react库的熟悉 |
相关依赖库
基础库
基础路由导航采用了 react-navigation,相对功能比较全,状态管理采用 mobx,与 react 结合开发个人认为比 redux 方便多了,至于UI库,这个主要看业务相关组件权重了;
工具
这个库本身自带了很多没啥用的字体,最好
fork了去除冗余代码和字体包,并引入 iconfont 自定义字体包,然后发布私有包,可优化性能之一,目前我这边也已完成优化:
- 去除了冗余代码;
- 去除了无用字体库;
- 引入
iconfont字体库;- 修改
android的fonts.gradle相关依赖配置;- 修改
ios的RNVectorIcons.podspec相关依赖配置;
通讯录这块获取数据需要注意下缓存,因为第一次获取可能会比较慢,然后一般都需要做到根据中文拼音去排序,这里我
fork了 pinyinjs 这个库,进行了改写,主要原因是该库中有相关的字典库,都比较大,所以必然得去优化,不然打包会很大,对性能影响比较大
- react-native-image-picker 图片选择器
- react-native-image-crop-picker 图片截取选择器
- react-native-popup-dialog 弹出层
- react-native-fs 文件读写
- react-native-picker 级联下拉
- react-native-qrcode-svg 二维码生成工具
- react-native-qrcode-scanner 二维码扫码
- react-native-image-zoom-viewer 图片预览
- react-native-shake-event 摇一摇
微信相关
目前并未支持微信最新的sdk,需要
fork并更新最新lib库,并更新相关原生bridge映射方法,再发布到私有库进行使用;
地图相关
- react-native-amap3d 高德地图组件
- react-native-amap-geolocation 高德地图组件
- react-native-maps 谷歌地图
- react-native-baidumap-sdk - 百度地图
以上就场景功能而言最终选用了高德地图,再结合高德
web api可以完成很多复杂场景,比如轨迹、路线等;
热更新配置
这玩意亲测,不靠谱,贼不稳定......时灵时不灵的,有资源的话建议公司自建
应用商店准备
- 提前准备软著,各大应用市场开账户需要,软著找黄牛需要2周+的;
- 安卓各大应用市场账户提前申请,别等开发完了再去整,加起来前后审核至少需要一周的
- ios 开发者账户注册 需要注册企业开发者账户,一年600多好像,ios市场统一这一点还是很好的,发布啥的都还挺方便的;
安卓打包
android签名
# 跟着命令输入即可,切记一定要记录好 name 还有最后输入的秘钥
# 签名后生成的文件注意保密存储
$ keytool -genkeypair -v -keystore release.keystore --alias "name" -keyalg RSA -keysize 2048 -validity 100000
android查看签名
$ keytool -v -list -keystore /Users/**/android/keystores/release.keystore
android环境微信开放平台签名设置为apk签名 = "keystoreMD5".replace(/:/ig, '').toLowerCase();此签名用作微信sdk对接配置时用到
- 配置
gradle.properties签名路径,用作生产环境打包
android/app/build.gradle中增加 release 配置,设置读取签名文件
android {
...
signingConfigs {
// 设置读取生产环境配置
release {
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
// include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
// 打包apk涵盖内核,一般以下两种即可,这个比较影响打包大小,实际可根据平台情况自选即可
include "armeabi-v7a", "x86"
}
}
buildTypes {
// 开发环境配置代码混淆
release {
// 是否代码混淆
minifyEnabled enableProguardInReleaseBuilds
// 代码混淆配置文件
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
// def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
// 定义内核版本
def versionCodes = ["armeabi-v7a": 1, "x86": 2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
ios 打包
- 首选需要打包
rn bundle代码
$ react-native bundle --entry-file index.tsx --platform ios --dev false --bundle-output ./ios/bundle/index.ios.jsbundle --assets-dest ./ios/bundle
- 相关权限申请文案配置,这个影响审核结果,需要描述清楚申请这个权限是干啥的,以下仅供参考
- 注意
Bundle identifier这个为包名,发布后即不可变更; - 注意需改打包版本号,相同版本号打包后是不能上传
appStore的; - 选择事先申请好的开发者账号,以及相关证书;
ios打包最常见的问题是关于证书文件的,这个需要事先在开发者后台申请好生产环境的证书文件,,build时候需要选好bundle打包文件,具体流程网上有不少,这里就不啰嗦啦...- 打包好ipa包后,即可上传
appStore,后续即可在appStore后台进行文案编辑操作和发布了;
相关问题解决
ios在xcode中会碰到环境资源文件找不到的问题,这里需要设置下,File > Project/Workspace Settings中的Share Project/Workspace Settings里build system选项将New Build System(Default)切换成Legacy build system;- 安卓注意相关应用权限每次需要验证申请,不然会导致某个功能不可用,甚至会导致
crash
const permissionKeys = {
camera: PermissionsAndroid.PERMISSIONS.CAMERA,
location: PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
contacts: PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
photos: PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
};
const permissionCheck = (permission, title, message, cb = null) => {
if (!isAndroid || !cb) {
cb && cb();
return;
}
PermissionsAndroid.check(permission).then(isAllow => {
isAllow ? cb && cb() : PermissionsAndroid.request(permission, {
title: title,
message: message,
buttonNegative: '取消',
buttonPositive: '允许'
}).then(() => cb && cb()).catch(() => {
console.error('获取权限失败@', permission);
});
});
};
/**
* 监测定位权限
*/
export const checkLocationPermission = (cb = null) => {
const permission = permissionKeys.location;
permissionCheck(permission, "是否允许访问当前定位?", "以便于精确定位您当前的位置,在地图、车辆列表中准确显示。", cb);
};
/**
* 相机权限
*/
export const checkCameraPermission = (cb = null) => {
const permission = permissionKeys.camera;
permissionCheck(permission, "是否允许访问相机?", "以便于使用相机进行拍摄,并在头像、扫码、上传单据、留言等功能使用所拍摄的照片。", cb);
};
/**
* 通讯录权限
*/
export const checkContactPermission = (cb = null) => {
const permission = permissionKeys.contacts;
permissionCheck(permission, "是否允许访问通讯录?", "以便于快速选择通讯信息保存为系统常用联系人。", cb);
};
/**
* 保存图片到相册权限
*/
export const checkLibraryPermission = (cb = null) => {
const permission = permissionKeys.photos;
permissionCheck(permission, "是否允许访问相册?", "以便于你可以在应用中上传、发送相机胶卷中的内容,或保存照片到相机胶卷中。", cb);
};
- 安卓返回按键监听,很多安卓机会有一个返回键,有些业务场景需要进行监听处理
/**
* 安卓返回键监听
*
* @type {{bind: (cb?: any) => void; unbind: () => void}}
*/
let listener = null;
export const keyboardBackListener = {
bind: (cb = null, isStop = true) => {
if (!isAndroid) return;
listener && listener.remove();
listener = BackHandler.addEventListener(keyboardEnum.back, () => {
cb && cb();
return isStop;
});
},
unbind: () => {
if (!isAndroid) return;
listener && listener.remove();
}
};
- 监听设备原生事件,即原生桥接rn的事件响应监听
const _deviceListener = {};
/**
* 监听设备原生事件
*
* @type {{bind: (type: string, cb?: any) => void; unbind: (type: string) => void}}
*/
export const deviceListener = {
bind: (type: string, cb = null) => {
_deviceListener[type] && _deviceListener[type].remove();
_deviceListener[type] = DeviceEventEmitter.addListener(type, (data) => {
const params = data ? data.toString().split(',') : null;
cb && cb(params);
});
},
unbind: (type: string) => {
_deviceListener[type] && _deviceListener[type].remove();
}
};
- 监听键盘呼出、隐藏
let _keyboardShowListener = null;
/**
* 键盘呼出事件监听
*/
export const keyboardShowListener = {
bind: (cb = null) => {
_keyboardShowListener && _keyboardShowListener.remove();
_keyboardShowListener = Keyboard.addListener("keyboardDidShow", e => {
const {width, height} = e.endCoordinates;
cb && cb({width, height});
});
},
unbind() {
_keyboardShowListener && _keyboardShowListener.remove();
}
};
let _keyboardHideListener = null;
/**
* 键盘隐藏事件监听
*/
export const keyboardHideListener = {
bind: (cb = null) => {
_keyboardHideListener && _keyboardHideListener.remove();
_keyboardHideListener = Keyboard.addListener("keyboardDidHide", e => {
cb && cb();
});
},
unbind() {
_keyboardHideListener && _keyboardHideListener.remove();
}
};
- 监听应用唤醒
componentWillMount() {
AppState.addEventListener('change', this._handleAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
}
/**
* app 唤醒监听
* @param nextAppState active 唤醒 background 后台运行
* @private
*/
_handleAppStateChange = (nextAppState) => {
// 唤醒
nextAppState === 'active'
// TODO...
};
......
总结
以上记录了一部分开发经验,希望对大家有用,同时欢迎大家帮忙指正;
其实中间还碰到了很多坑,一下子想不起来了,如若需要,可以私下联系沟通;