小团队实践ReactNative两年了,总结一下。
B端APP,实践相关都是围绕一个omni的框架展开的。
Omni的组成
| 组件 | 简介 |
|---|---|
| @omni/cli | 提供dev build命令行工具 |
| @omni/ui | ios、android、web三端组件库 |
| @omni/core | 核心封装,提供app Native功能 |
| @omni/create-omni-app | 脚手架支持 |
| @omni/icon | icon统一封装、构建 |
| @omni/native | 原生集成构建相关 |
接下来具体介绍这几部分。
@omni/create-omni-app
这部分主要作用提供脚手架的支持。(没啥说的)
yarn create @omni/omni-app
# or
npx @omni/create-omni-app@latest
@omni/core
主要封装了一些跟Native交互的功能,如下:
import { useUser, } from '@omni/core'
const Demo = () => {
const user = useUser()
console.log(user.employeeID) // 工号
...
}
@omni/ui
主要做了一套跨端组件库(web\ios\android),目前40+,下面介绍几个自己封装的有特点ReactNative组件。
TablePro
支持冻结行列,Table骨架,自定义样式等。
Form表单
主要特点不带数据域管理,超级灵活。 React表单的数据管理的库已经很多了,不想集成在组件里面,想完全跟UI分离。
自己的设计如下:
export default () => {
const form = useFormik({
initialValues: {
name: '',
gender: '',
},
validate(values) {
let errors: any = {}
if (!values.name) {
errors.name = 'name 为必填项'
}
return errors
},
onSubmit: (values) => {
console.log('values', values)
},
})
return (
<>
<Form
attachProps2FormItem={itemProps => {}}
attachProps2FormItemChildren={(itemProps) => {}}>
<Form.Header><Box h={10}></Box></Form.Header>
<Form.Item label='姓名' name='name' required>
<Input placeholder='请输入姓名' align='right' clearable />
</Form.Item>
<Form.Item name='gender' label='性别'>
<Radio.Group>
<Radio value='1'>{'男 '}</Radio>
<Radio value='2'>女</Radio>
</Radio.Group>
</Form.Item>
</Form>
<Button
onPress={() => {
form.handleSubmit()
}}>提交</Button>
</>
}
主要Form组件上提供attachProps2FormItem,attachProps2FormItemChildren这两个方法实现数据与UI的粘合。比如我要用Input组件与Formik实现UI与数据的绑定。
const form = useFormik({
initialValues: {
name: '',
},
})
<Form
attachProps2FormItem={itemProps => {}}
attachProps2FormItemChildren={(itemProps) => {
return {
value: form.values[name],
[itemProps.trigger || 'onChange']: (text: string) => {
if (itemProps.normalize) {
text = itemProps.normalize(text, form.values[name], form.values)
}
form.setFieldValue(name, text)
},
}
}}>
<Form.Item label='姓名' name='name' required>
<Input placeholder='请输入姓名' align='right' clearable />
</Form.Item>
</Form>
当然根据这套语法,可以实现更多的功能,来减少模板的书写。
@omni/icon
没啥说的,就是封装了个Icon组件。
import Icon from '@omni/icon'
<Icon name="copy" />
name如何有type提示?
这里,自己写了个根据svg文件名生成type的工具。
就有提示啦!!!
@omni/native
这个库主要就是一些三方的依赖。统一做了个库管理。什么都没做。
{
...
"react": "17.0.2",
"react-native": "0.64.3",
"react-native-web": "0.17.6",
...
}
@omni/cli
命令行工具主要封装了以下的功能:
- 打包RN代码(分包,拆包)
- 打包web端代码
- 提供开发调试
Metrojs分包拆包
- 把三方库common依赖拆出来
- 把业务代码拆出来
metro.common.config.js,moduleId用的依赖的路径,有点简单,缺点打包出来的资源大小稍微大一点。这块可以用自增ID。
const fs = require('fs');
const path = require('path')
const pathSep = path.sep;
const projectRootPath = path.resolve(process.cwd(), '');
function createModuleId(modulePath) {
let moduleId = modulePath.substr(projectRootPath.length + 1);
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
moduleId = moduleId.replace(regExp, '__');
return moduleId;
}
module.exports = {
projectRoot: projectRootPath,
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return function (modulePath) {
const moduleId = createModuleId(modulePath)
fs.appendFileSync(path.resolve(projectRootPath, '.omni/moduleIds_android.txt'), `${moduleId}\n`);
return moduleId;
};
},
},
};
业务包metro.bus.config.js,主要是过滤common包的模块,生成业务的bundle。
const fs = require('fs');
const path = require('path')
const pathSep = path.sep;
const projectRootPath = path.resolve(process.cwd(), '');
const moduleIdsFilePath = path.resolve(projectRootPath, '.omni/moduleIds_android.txt')
const moduleIds = fs.readFileSync(moduleIdsFilePath, 'utf8').toString().split('\n');
function createModuleId(modulePath) {
let moduleId = modulePath.substr(projectRootPath.length + 1);
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
moduleId = moduleId.replace(regExp, '__');
return moduleId;
}
module.exports = {
projectRoot: projectRootPath,
resolver: {
resolveRequest: (context, realModuleName, platform, moduleName) => {
if (moduleName.startsWith('omniKeys')) {
return {
filePath: path.resolve(projectRootPath, '.omni/omniKeys.js'),
type: 'sourceFile',
};
}
const { resolveRequest: removed, ...restContext } = context;
return require("metro-resolver").resolve(restContext, moduleName, platform);
},
sourceExts: ['jsx', 'js', 'ts', 'tsx', 'cjs', 'mjs'],
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return createModuleId;
},
processModuleFilter: function (modules) {
const mouduleId = createModuleId(modules.path);
if (modules.path == '__prelude__') {
return false
}
if (mouduleId == 'node_modules__metro-runtime__src__polyfills__require.js') {
return false
}
if (moduleIds.indexOf(mouduleId) < 0) {
return true;
}
return false;
},
getPolyfills: function() {
return []
}
},
};
webpack打包web端代码
这块比较简单了,利用react-native-web来实现web端的跨端渲染。
webpack提供web页面调试
使用webpack来实现RN-web页面的调试,可以大大提高rn的开发效率,优先进行web页面调试,后期真机调试。ios、android、web全部调试完毕,就可以提测了。
优先web的一个主要原因也是因为我们的业务可能嵌入到别app或者网页中,让三方集成rn显然不现实。
CLI用法设计
# 调试
omni dev --web -e sit
omni dev --native -e sit
# 打包
omni build --web -e sit
omni build --native -e sit
目录结构设计
├── src
│ └── modules
│ ├── moduleA
│ ├── moduleB
命令行会遍历modules目录,打包生成生成不同模块的代码。
生成web端代码如下
├── dist
│ └── js
│ └── images
│ ├── moduleA.html
│ ├── moduleB.html
生成RN代码如下
├── bundles
│ └── ios
│ ├── moduleA.ios.jsbundle
│ ├── moduleB.ios.jsbundle
│ └── android
│ ├── moduleA.android.jsbundle
│ ├── moduleB.android.jsbundle
跟原生开发协商语法,加载不同的模块就可以了。
配置项
主要是项目的一下打包、调试、代理、虚拟模块等配置项。
差不多就这些了,整体来说,React Native得开发体验不如web,有时候也得解决一些兼容问题,其实最麻烦的还是RN的升级问题,现在还停留在0.64.3,想升级到0.7+获得更好的性能,估计得下不少功夫。