React Native实践这两年

348 阅读4分钟

小团队实践ReactNative两年了,总结一下。

B端APP,实践相关都是围绕一个omni的框架展开的。

image.png

Omni的组成

组件简介
@omni/cli提供dev build命令行工具
@omni/uiios、android、web三端组件库
@omni/core核心封装,提供app Native功能
@omni/create-omni-app脚手架支持
@omni/iconicon统一封装、构建
@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

image.png

支持冻结行列,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>

当然根据这套语法,可以实现更多的功能,来减少模板的书写。

image.png

@omni/icon

没啥说的,就是封装了个Icon组件。

import Icon from '@omni/icon'

<Icon name="copy" />

name如何有type提示?

image.png 这里,自己写了个根据svg文件名生成type的工具。

image.png 就有提示啦!!!

@omni/native

这个库主要就是一些三方的依赖。统一做了个库管理。什么都没做。

{
  ...
  "react": "17.0.2",
  "react-native": "0.64.3",
  "react-native-web": "0.17.6",
  ...
}

@omni/cli

命令行工具主要封装了以下的功能:

  1. 打包RN代码(分包,拆包)
  2. 打包web端代码
  3. 提供开发调试

Metrojs分包拆包

  1. 把三方库common依赖拆出来
  2. 把业务代码拆出来

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

跟原生开发协商语法,加载不同的模块就可以了。

配置项

主要是项目的一下打包、调试、代理、虚拟模块等配置项。

image.png

差不多就这些了,整体来说,React Native得开发体验不如web,有时候也得解决一些兼容问题,其实最麻烦的还是RN的升级问题,现在还停留在0.64.3,想升级到0.7+获得更好的性能,估计得下不少功夫。

最后,大家新年快乐。