美文网首页
Taro跨三端平台的应用与实践

Taro跨三端平台的应用与实践

作者: LaxusJ | 来源:发表于2020-02-28 15:12 被阅读0次

    背景

    Taro 是一套遵循 React 语法规范的 多端开发 解决方案。现如今市面上端的形态多种多样,Web、React-Native、微信小程序等各种端大行其道,当业务同时在不同的端都要求有所表现的时候,针对不同的端编写多套代码的成本显然非常高,这时只编写一套代码就能适配到多端的能力就显得极为重要。
    使用 Taro,我们只需书写一套代码,再通过 Taro 的编译工具,即可将源代码分别编译出在不同端(微信/百度/支付宝/字节跳动小程序、快应用、H5、React-Native 等)运行的代码。
    58租房-创新找房业务有上线三端并高度一致的需求,为了节约开发人力和创新,我们在该项目中探索性的尝试使用Taro进行开发,下面将从 Taro框架编译原理、源码改造、依赖改造、业务开发实践过程几个角度来分别阐述一下。

    Taro框架编译原理

    因为H5和React-Native都可以遵循React语法规范开发,在 React 中,是使用 JSX 来作为组件的模板的,而小程序则与 Vue 一样,是使用字符串模板的。这样两者之间就有着巨大的差异了。
    JSX

    render () {
      return (
        <View className='index'>
          {this.state.list.map((item, idx) => (
            <View key={idx}>{item}</View>
          ))}
          <Button onClick={this.jump}>跳转</Button>
        </View>
      )
    }
    

    小程序模板

    <view class="index">
      <view wx:key={idx} wx:for="{{list}}" wx:for-item="item" wx:for-index="idx">{{item}}</view>
      <view bindtap="jump">跳转</view>
    </view>
    

    那么这个时候我们就想,要是能够将 JSX 编译成小程序模板就好了。

    事实上在我们平时的开发中,这种编译的操作到处可见,babel 就是我们最常用的 JS 代码编译器,一般浏览器是不能支持一些非常新的语法特性的,但我们又想使用它们,这个时候就可以借助 babel 来将我们的高版本的 ES 代码,编译成浏览器可以运行的 ES 代码。而我们像要将 JSX编译成小程序模板,也是同样的道理。我们首先来了解一下 Babel 的运行机制。

    Babel 作为一个 代码编译器 ,能够将 ES6/7/8 的代码编译成 ES5 的代码,其核心利用的就是计算中非常基础的编译原理知识,将输入语言代码,通过编译器执行,输出目标语言的代码。编译原理的一般过程就是,输入源程序,经过词法分析、语法分析,构造出语法树,再经过语义分析,理解程序正确与否,再对语法树做出需要的操作与优化,最终生成目标代码。

    image.png

    将 JSX 编译成小程序模板,非常幸运的是 babel 的核心编译器 babylon 是支持对 JSX 语法的解析的,我们可以直接利用它来帮我们构造 AST,而我们需要专注的核心就是如何对 AST 进行转换操作,得出我们需要的新 AST,再将新 AST 进行递归遍历,生成小程序的模板。

    源码改造

    我们是基于Taro1.3.10版本开发的,为了使Taro兼容58RN工程我们对源码做了以下改造:
    taro脚手架taro-cli

    1. 升级内部react-native版本"react-native": "0.57.8"
    2. 固定注册包名AppRegistry.registerComponentwuba

    taro依赖库taro-rn
    1.删除Expo相关依赖的api,保证依赖纯净

    taro依赖库taro-router-rn
    修改initRouter中基于react-navigation的初始化参数,使其保证和58RN跳转方式统一

    为了便于版本管理,我们将taro关键字转换为fangchan-taro,并修改其依赖树,fangchan-taro托管于npm,后续陆续改造了taro-cli、taro-rn、taro-rn-component等依赖,我们把Taro下的依赖统一进行管理,置于同一个组织,标记为公开,形如:
    @fangchan/taro-rn、@fangchan/taro-cli

    image.png

    图上是Taro开发RN端的工作流程,通过执行taro build --type rn --watch指令将Taro编译成RN代码,并开启metro server将rn_temp下的js文件打包成js bundle,通过npm start开启服务后就可以在58rn测试载体页上愉快的访问了。

    依赖改造

    Taro提供的组件和Api相对比较基础,通用性更强,但是58移动端并没有对应的sdk支持,所以我们暂时去掉了不支持的api,并在有必要的情况下进行重写。
    例如taro-rn中的请求Request是必要的API,我们对其改造,让它内部调用房产SDK的请求,保证请求内部流程一致。

    import HMS from 'house-middleware-sdk'
    function request (options) {
      options = options || {}
      let url = options.url
      let data = options.data || {}
      if (typeof options === 'string') {
        options = {
          url: options
        }
      }
      let method = options.method || 'GET'
      method = method.toUpperCase()
      if (method === 'GET') {
        return HMS.get(url, data)
      }
      if (method === 'POST') {
        const formData = new FormData()
        Object.keys(data).forEach(key => {
          formData.append(key, data[key])
        })
        return HMS.post(url, formData)
      }
    }
    export default {
      request
    }
    

    房产业务中使用到的业务组件如筛选、底部栏等,不同端提供的Api如登录、认证等需要我们自行开发,我们把开发分为两个阶段:

    • 第一阶段为了业务快速迁移上线,尽量复用各端已有组件,使用条件编译完成各端打包
    • 第二阶段对于通用性较强的组件、基于Taro封装各端API和组件库提高开发效率、降低维护成本
      目前我们处于第一阶段,使用Taro提供的条件编译来处理不同端的差异
    if (process.env.TARO_ENV === 'weapp') {
        // 微信小程序端执行逻辑
    } else if (process.env.TARO_ENV === 'h5') {
        // h5 端执行逻辑
    } else if (process.env.TARO_ENV === 'rn') {
        // react-native 端执行逻辑
    }
    

    业务开发实现

    1.项目初始化

    我们通过改造的cli来快速构建项目fangchan-taro init TaroDemo,构建好后项目目录结构如下

    ├── dist                   编译结果目录
    ├── config                 配置目录
    |   ├── dev.js             开发时配置
    |   ├── index.js           默认配置
    |   └── prod.js            打包时配置
    ├── src                    源码目录
    |   ├── pages              页面文件目录
    |   |   ├── index          index 页面目录
    |   |   |   ├── index.js   index 页面逻辑
    |   |   |   └── index.css  index 页面样式
    |   ├── app.css            项目总通用样式
    |   └── app.js             项目入口文件
    └── package.json
    

    我们首先需要修改入口文件,在构造中初始化全局参数,在全局配置config中定义页面以及设置小程序导航栏和设置rn特殊返回键处理等操作。

    class App extends Component {
    
      constructor(props) {
        super(props);
        initGlobalConst();
      }
    
      config = {
        pages: [
          'pages/index/index',
          'pages/listpage/index',
          'pages/filterpage/index',
          'pages/guidepage/index',
          'pages/demandpage/index',
        ],
        window: {
          backgroundTextStyle: 'light',
          navigationBarBackgroundColor: '#fff',
          navigationBarTitleText: 'WeChat',
          navigationBarTextStyle: 'black',
        },
        backStackConfig: {
          ctrlFun: process.env.TARO_ENV === 'rn' ? 
          require('./utils/initRN').ctrlBackStackFun : false
        }
      };
    
      render() {
        return (
          <Index/>
        )
      }
    }
    
    Taro.render(<App/>, document.getElementById('app'));
    

    然后在'src/index/index.js'中的componentWillMount生命周期处理多端的初始化操作

    componentWillMount() {
        process.env.TARO_ENV === 'rn' && initRN((goal) => {
          this.renderPage();
        });
        process.env.TARO_ENV === 'h5' && initH5((goal) => {
          this.renderPage();
        });
        process.env.TARO_ENV === 'weapp' && initH5((goal) => {
          this.renderPage();
        });
      }
    

    以RN端为例,需要在initRN方法中从native端获取header信息、跳转协议等,所以这一步是必不可少的。

    function initRN(callback) {
      HMS.initPackage(
        () => HMS.initNativeParams(
          () => {
            const cacheFlag = global.jumpParams.content.params.useCache || true;
            HMS.CacheUtil.setUseCache(cacheFlag);
            upDateGlobalConst('PACKAGE', global.CURRENT_PACKAGE);
            upDateGlobalConst('FULL_PATH', global.jumpParams.content.params.full_path);
            handleTargetPageType(callback);
          }
        )
      );
    }
    

    2. 页面间跳转

    我们只需要在入口文件的 config 配置中指定好 pages,然后就可以在代码中通过 Taro 提供的 API 来跳转到目的页面,例如:

    // 跳转到目的页面,打开新页面
    Taro.navigateTo({
      url: '/pages/page/path/name'
    })
    
    // 传入参数 id=2&type=test
    Taro.navigateTo({
      url: '/pages/page/path/name?id=2&type=test'
    })
    

    3. 状态管理

    为了减少学习成本,我们沿用RN端使用的状态管理机制mobx进行状态管理,在Taro端使用方式和RN端完全一致。这样也方便已有的RN项目后面能快速迁移到Taro。

    4.样式管理

    样式管理是多端开发的一大挑战,因为 React Native 与一般 Web 样式支持度差异较大。样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制。
    不过这也正巧适用于我们团队,因为我们对RN样式控制比较熟,所以我们并没有采用scss的方式,而是沿用编写RN样式的方式,完全使用style来编写Taro样式。

    <View
            style={{
              display: 'flex',
              position:'relative',
              width: Taro.pxTransform((global.WINDOW_WIDTH_VALUE - 30) * 2),
              height: Taro.pxTransform(130)
              flexDirection: 'column',
              backgroundColor: '#EAEAEA'
            }}
    />
    

    其中有几点特殊需要注意:
    1.因为RN一般需通过Dimensions 获取宽高再进行换算,Taro提供的 pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,
    所有数字必须在style中用Taro.pxTransform()包装,且单位是px。

    1. 在开发中必须声明flex布局和排版,因为RN和小程序H5默认横纵不一样。
    2. position: 'absolute'需要在外层父view设置position: 'relative'。
    3. 覆盖组件样式可以通过style传递,但style不支持数组。

    最终看一下实现的效果还是能保证高度统一的。


    image.png

    开发中遇到的问题

    1.语法问题
    由于微信小程序端的限制,有一些jsx用法不能得到很好地支持,比如不能使用 Array#map 之外的方法操作 JSX 数组、暂不支持在 render() 之外的方法定义 JSX、不能在 JSX 参数中使用对象展开符、不支持无状态组件等。
    2.房产组件库依赖问题
    因为组件库中使用了es6语法,且部分组件有mobx的引用,直接依赖会有不兼容的问题,所以将其统一用babel转成es5格式打包到lib中引用。
    3.跨域问题
    RN不存在跨域问题,小程序网络请求需要在小程序设置中配置,H5存在跨域的问题,这个可通过 devServer.proxy 解决,以及编译打包的静态资源是固定文件名,建议改成带 hash 值方便缓存管理,这些配置在项目里的 src/config 中都能找到。

    总结

    本文从一个RN开发者角度来进行Taro跨平台项目实践,其中在状态管理及样式管理上都采用了和RN一样的机制,一是为了组内同学可以快速上手开发,二是为了将已有rn项目能快速平移到Taro。但是对H5和小程序两端掌握的能力有限,需要实践积累。
    目前由于组件库、sdk积累较少,前期开发业务的同时要同时进行业务开发、底层封装、脚手架调整,业务开发速度因此受到影响,粗略的估算是RN同等业务开发时间的2倍左右。预计未来理想情况下,基于完整的组件库、sdk、以及标准化的协议可以抹平系统、平台、端之间的大多数差异,开发效率趋近于目前RN情况。
    小程序各端统一技术栈到Taro后,两方理论上可以做到无缝衔接,底层资源共享、复用,这将显著提升开发效率,后续我们在推动四端逻辑、协议统一的同时,会加强组间沟通,与FE同学共同促进通用层优化,使业务开发者可以专注于业务本身。

    相关文章

      网友评论

          本文标题:Taro跨三端平台的应用与实践

          本文链接:https://www.haomeiwen.com/subject/jokcfhtx.html