Taro竟然适配鸿蒙了!

本人很荣幸成为Taro适配鸿蒙开发的共建者,下面也是我在开发时一些心得体会。也是遇到了不少坑爬过来的。

Taro 对于鸿蒙的适配原理

HarmonyOS是一款面向万物互联时代的、全新的分布式操作系统。

Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ 小程序 / H5 / RN 等应用

HarmonyOS开发有三类:Java、Js、Native。鸿蒙Js的开发跟Taro的开发相似。可以按照我们Taro的代码解析成鸿蒙的代码。主要原理如下:

原理

整体原理

graph LR
A(taro cli) --> B(根据不同的type解析不同的插件)
B --> C(利用webpack去编译各端的代码)
C --> D(再利用各端自己的插件配置磨平各端的代码)
D --> E(生成harmony代码)

编译

整体原理

graph
A(taro代码) --> B(通过tarojs/cli开始编译)
B --> C(tarojs/service判断不同端使用的包)
C --> D(使用tarojs/mini-runner进行打包)
D --> E(通过plugin-platform-harmony插件抹平鸿蒙的代码)
E --> F(生成harmony的代码)

@tarojs/mini-runner的原理

graph LR
A(taro代码) --> B(AST解析)
B --> C(解析Render代码)
C --> D(利用脚本转换)
D --> E(生成代码)

编译鸿蒙时,CLI 会调用 @tarojs/mini-runner 包,这个包主要是对代码进行编译。mini-runner 主要做了这些事情:

  1. 负责根据开发者的编译配置调整 Webpack 配置
  2. 注入自定义的 PostCSS 插件。(如 postcss-pxtransform)
  3. 注入自定义的 Webpack 插件。
  4. 注入自定义的 Webpack Loaders。(Loaders 位于 @tarojs/taro-loader 包中)
  5. 调用 Webpack 开启编译。
  6. 修改 Webpack 的编译产物,调整最终的编译结果。

再根据@tarojs/plugin-platform-harmony进行调整,对于组件、API、路由会在这个插件进行磨平

mini-runner进行分析

├── src
|   ├── config                      项目配置文件
|   ├── dependencies                项目依赖文件
|   ├── loader                      自定义loader
|   |   ├── quickappStyleLoader     快应用                      
|   |   └── miniTemplateLoader      小程序模版解析器
|   ├── plugins                     自定义插件
|   ├── prerender                   自定义的代码结构
|   ├── template                    自定义模版
|   |   ├── comp                    组件模版
|   |   ├── component               自定义组件模版
|   |   └── custom-wrapper          自定义小程序插件
|   ├── utils                       公共配置
|   ├── webpack                     webpack配置
|   |   ├── base.conf               基础配置
|   |   ├── build.conf              打包配置
|   |   ├── chain                   插件和解析器
|   |   └── postcss.conf            对css转换的配置
|   └── index                       webpack的启动
└── index                           入口文件
复制代码

注:babel的使用插件

babel使用 说明
@babel/core 解析代码获取 AST
@babel/types AST 节点定义,生成节点
@babel/traverse 递归遍历操作 AST
@babel/generator AST 生成源码

源代码 --> 端代码

<View className="flex items-center h-44 pb-40">
  <SlPrice price={ price } />
  {
    tags.length !== 0 &&
    tags.map(tag =>
      <View className="px-20 py-6 mx-12 bg-red-light text-yellow text-22 rounded-20" key={ tag }>{tag}</View>
    )
  }
</View>
复制代码
(_tarojs_components__WEBPACK_IMPORTED_MODULE_1__[/* View */ "d"], {
  className: "flex items-center h-44 pb-40",
  children: [/*#__PURE__*/Object(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_6__["jsx"])(_jd_selling_c_ui__WEBPACK_IMPORTED_MODULE_2__[/* SlPrice */ "e"], {
    price: price
  }), tags.length !== 0 && tags.map(function (tag) {
    return /*#__PURE__*/Object(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_6__["jsx"])(_tarojs_components__WEBPACK_IMPORTED_MODULE_1__[/* View */ "d"], {
      className: "px-20 py-6 mx-12 bg-red-light text-yellow text-22 rounded-20",
      children: tag
    }, tag);
  })]
})
复制代码

模版处理

运行

为了让 React、Vue 等框架直接运行在鸿蒙端,我们需要在小程序的逻辑层模拟浏览器环境,包括实现 DOM、BOM API 等。

@tarojs/runtime 是 Taro 的运行时适配器核心,它实现了精简的 DOM、BOM API、事件系统、Web 框架和小程序框架的桥接层等。此包主要是对小程序和h5运行进行适配,因为 ReactDOM 体积较大,且包含很多兼容性代码。因此 Taro 借助 react-reconciler 实现了一个自定义渲染器用于代替 ReactDOM。渲染器位于 @tarojs/react 包中。

Web 框架就可以使用 Taro 模拟的 API 渲染出一颗 Taro DOM 树,但是这一切都运行在小程序的逻辑层。而小程序的 xml 模板需要提前写死,Taro 选择了利用小程序 <template> 可以引用其它 <template> 的特性,把 Taro DOM 树的每个 DOM 节点对应地渲染为一个个 <template>。这时只需要把 Taro DOM 树的序列化数据进行 setData,就能触发 <template> 的相互引用,从而渲染出最终的 UI。

而鸿蒙这边没有<template>的概念,我们用自定义组件element代替。

运行时我们会将上述说的Taro DOM 树以一个整体变量 root 去运行代码。再根据root.cn下的数组去遍历和递归我们自定义组件

页面内容

<element name="container" src="../../container/index.hml"></element>
<element name="navbar" src="../../container/components-harmony/navbar/index.hml"></element>
<element name="tabbar" src="../../container/components-harmony/tabbar/index.hml"></element>

<div class="container">
  <navbar title="{{taroNavBar.title}}" background="{{taroNavBar.background}}" text-style="{{taroNavBar.textStyle}}" st="{{taroNavBar.style}}"></navbar>
  <div class="body" style="padding-top: 44px;padding-bottom: {{isShowTaroTabBar ? '56px' : '0'}}">
    <refresh if="{{enablePullDownRefresh}}" type="pulldown" refreshing="{{isRefreshing}}" onrefresh="onPullDownRefresh">
      <container root="{{root}}"></container>
    </refresh>
    <container else root="{{root}}"></container>
  </div>
  <tabbar if="{{isShowTaroTabBar}}" data="{{taroTabBar}}" selected="{{selected}}"></tabbar>
</div>
复制代码

root数据

{
  "root": {
    "cn": [
      {
        "cl": "layout bg-gray-lightbg",
        "cn": [
          {
            "cn": [
              {
                "nn": "#text",
                "v": "订单"
              }
            ],
            "nn": "view",
            "uid": "_n_885"
          },
          {
            "cl": "layout-area-slot w-full h-210 area-slot",
            "cn": [],
            "nn": "pure-view",
            "uid": "_n_888"
          }
        ],
        "nn": "pure-view",
        "uid": "_n_886"
      }
    ],
    "uid": "pages/tab-bar/order/index?$taroTimestamp=1636613920366"
  },
}
复制代码

container的内容

<element name="container" src="./index.hml"></element>
<element name="taro-textarea" src="./components-harmony/textarea/index.hml"></element>


<block for="{{i in root.cn}}">

<block if="{{i.nn == 'view'}}">
  <div hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}" hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}" hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}" hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}" @touchstart="{{eh}}" @touchmove="{{eh}}" @touchend="{{eh}}" @touchcancel="{{eh}}" @longtap="{{eh}}" animation="{{i.animation}}" @animationstart="{{eh}}" @animationiteration="{{eh}}" @animationend="{{eh}}" @transitionend="{{eh}}" style="{{i.st}}" class="{{i.cl}}" @click="{{eh}}"  id="{{i.uid}}">
    <container root="{{i}}"></container>
  </div>
</block>

<block if="{{i.nn == 'textarea'}}">
  <taro-textarea value="{{i.value}}" placeholder="{{i.placeholder}}" placeholder-style="{{i.placeholderStyle}}" placeholder-class="{{i.placeholderClass===undefined?'textarea-placeholder':i.placeholderClass}}" disabled="{{i.disabled}}" maxlength="{{i.maxlength===undefined?140:i.maxlength}}" auto-focus="{{i.autoFocus===undefined?false:i.autoFocus}}" focus="{{i.focus===undefined?false:i.focus}}" auto-height="{{i.autoHeight===undefined?false:i.autoHeight}}" fixed="{{i.fixed===undefined?false:i.fixed}}" cursor-spacing="{{i.cursorSpacing===undefined?0:i.cursorSpacing}}" cursor="{{i.cursor===undefined?-1:i.cursor}}" selection-start="{{i.selectionStart===undefined?-1:i.selectionStart}}" selection-end="{{i.selectionEnd===undefined?-1:i.selectionEnd}}" @focus="{{eh}}" @blur="{{eh}}" @linechange="{{eh}}" @input="{{eh}}" @confirm="{{eh}}" name="{{i.name}}" headericon="{{i.headericon}}" showcounter="{{i.showcounter===undefined?false:i.showcounter}}" menuoptions="{{i.menuoptions===undefined?[]:i.menuoptions}}" softkeyboardenabled="{{i.softkeyboardenabled===undefined?true:i.softkeyboardenabled}}" @translate="{{eh}}" @share="{{eh}}" @search="{{eh}}" @optionselect="{{eh}}" @selectchange="{{eh}}" style="{{i.st}}" class="{{i.cl}}" @click="{{eh}}"  id="{{i.uid}}">
    
  </taro-textarea>
</block>

...

</block>
复制代码

组件

Taro 在 鸿蒙 端实现了遵循微信小程序规范的基础组件库。

默认会使用 @tarojs/components 提供的 Web Components 组件库。

目前需要将Taro的组件库跟鸿蒙自带的组件库对接,将组建内属性和事件一一对齐。对于鸿蒙没有的组件,开发自定义组件对齐。因为鸿蒙的标签跟web的类似,目前需要对所有标签进行重新封装。主要有两类情况:

  1. 是将基本组件转换成我们目标组件

基本组件就是将Taro的基本组件直接适配到鸿蒙的基础组件库,如下代码所示:

buildStandardComponentTemplate (comp) {
  let nodeName = ''
  switch (comp.nodeName) {
    case 'slot':
    case 'slot-view':
    case 'cover-view':
    case 'view':
    case 'swiper-item':
      nodeName = 'div'
      break
    case 'static-text':
      nodeName = 'text'
      break
    case 'static-image':
      nodeName = 'image'
      break
    default:
      nodeName = comp.nodeName
      break
  }
  return this.generateComponentTemplateSrc(comp, nodeName)
}

generateComponentTemplateSrc (comp, nodeName?): string {
  const children = this.voidElements.has(comp.nodeName)
    ? ''
    : `<block for="{{i.cn}}">
    <container i="{{$item}}"></container>
  </block>`
  if (!nodeName) {
    nodeName = comp.nodeName
  }
  if (this.nativeComps.includes(nodeName)) {
    nodeName = `taro-${nodeName}`
  }

  const res = `
<block if="{{i.nn == '${comp.nodeName}'}}">
<${nodeName} ${this.buildAttrs(comp.attributes, comp.nodeName)} id="{{i.uid}}">
  ${children}
</${nodeName}>
</block>
`
  return res
}
复制代码
  1. 鸿蒙和Taro组建相同,但是属性不一样。对于这种情况我们将组件重新写成自定义组件,自定义组件中会把组件的属性和鸿蒙的对齐,如果鸿蒙实在没有相应属性,可以舍弃。对于鸿蒙的组件中有的属性在Taro中没有,在组件配置文件/components/index.ts中加入相应属性。
// program.ts

platform = 'harmony'
globalObject = 'globalThis'
runtimePath = `${PACKAGE_NAME}/dist/runtime`
taroComponentsPath = `${PACKAGE_NAME}/dist/components/components-react`
fileType = {
  templ: '.hml',
  style: '.css',
  config: '.json',
  script: '.js'
}

// components-harmony文件夹下
├── components-harmony
    ├──image
    ├──input
    └──...
复制代码

API

开发者从 @tarojs/taro 中引用 Taro 对象并使用它提供的 API。

在 鸿蒙 环境,@tarojs/taro 会从 @tarojs/api 取与平台无关的 API,从 @tarojs/plugin-platform-harmony 中取遵循鸿蒙规范实现的 API,最终集合成一个 Taro 对象暴露给开发者。

对于鸿蒙系统的开发,API跟Taro的API功能差不多,但是名称,属性都不一样。通过调用initNativeApi方法我们将taro内部的的API方法直接替换掉,使用自定义的API方法去实现鸿蒙API的方法。

// /apis/index.ts
import { noop, current } from './utils'
import * as apis from './apis'

export function initNativeApi (taro) {
  current.taro = taro
  taro.initPxTransform = noop
  Object.assign(taro, apis)
}

// apis文件夹下
├── apis
    ├── interactive
    ├── navbar
    ├── ...
    └── index.ts
复制代码

路由

依据app.config里面pages进行页面划分,目前生成的代码是在default。需要手动将鸿蒙项目的config.json修改

插件说明

公共

路径 说明
@tarojs/cli CLI 工具(init/build)
@tarojs/taro-loader Webpack loaders
@tarojs/helper 工具库,主要供 CLI、编译时使用
@tarojs/runner-utils 工具库,主要供小程序、H5 的编译工具使用
@tarojs/shared 工具库,主要供运行时使用
@tarojs/taro 暴露各端所需要的 Taro 对象
@tarojs/api 和各端无关的 Taro API
babel-preset-taro Babel preset
eslint-config-taro ESLint 规则
postcss-pxtransform PostCSS 插件,转换 px 为各端的自适应尺寸单位

鸿蒙

路径 说明
@tarojs/plugin-platform-harmony 鸿蒙的编译适配
@tarojs/mini-runner 小程序编译

开发

Taro 对鸿蒙的适配工作正在开发中,目前可以通过拉取 feat/harmony 分支的代码并编译后进行使用。

1. 下载 Taro 源码并编译

需要对鸿蒙的适配进行link

# 下载源码
$ git clone [email protected]:NervJS/taro.git
$ git checkout feat/harmony

# 安装依赖
$ yarn

# 编译
$ yarn build

# yarn link (harmony 基于 Taro v3.4,但 v3.4 暂时还未发布,所以需要把 3.4 改动的包都进行软链)
$ cd packages/taro-cli
$ yarn link
$ cd ../taro-runtime
$ yarn link
$ cd ../taro-harmony
$ yarn link
$ cd ../taro-plugin-react
$ yarn link
$ cd ../taro-plugin-vue2
$ yarn link
$ cd ../taro-plugin-vue3
$ yarn link
$ cd ../taro-mini-runner
$ yarn link
$ cd ../taro
$ yarn link
$ cd ../taro-api
$ yarn link
$ cd ../shared
$ yarn link
$ cd ../taro-loader
$ yarn link
复制代码

2. 安装 @tarojs/cli,创建 Taro 项目,建立软链

安装 @tarojs/cli(如本地已安装过,可跳过这步)

# 使用 npm 安装 CLI
$ npm install -g @tarojs/cli

# OR 使用 yarn 安装 CLI
$ yarn global add @tarojs/cli

# OR 安装了 cnpm,使用 cnpm 安装 CLI
$ cnpm install -g @tarojs/cli
复制代码

创建 Taro 项目,建立软链

taro init myApp
cd myApp
yarn link @tarojs/runtime
yarn link @tarojs/plugin-platform-harmony
yarn link @tarojs/plugin-framework-react
yarn link @tarojs/plugin-framework-vue2
yarn link @tarojs/plugin-framework-vue3
yarn link @tarojs/mini-runner
yarn link @tarojs/taro
yarn link @tarojs/api
yarn link @tarojs/shared
yarn link @tarojs/taro-loader

# OR
yarn link @tarojs/runtime @tarojs/plugin-platform-harmony @tarojs/plugin-framework-react @tarojs/plugin-framework-vue2 @tarojs/plugin-framework-vue3 @tarojs/mini-runner @tarojs/taro @tarojs/api @tarojs/shared @tarojs/taro-loader
复制代码

3. 编译配置

// config/index.js
config = {
  // 配置输出路径,输出到鸿蒙 JS FA 的对应目录下。(支持绝对路径、相对路径)
  // 例如鸿蒙应用名称为 MyApplication,JS FA 名称为默认的 default,那么 outputRoot 需要设置为:
  // 'MyApplication/entry/src/main/js/default'
  outputRoot: '...',
  // 配置使用插件
  plugins: ['@tarojs/plugin-platform-harmony'],
  mini: {
    // 如果使用开发者工具的预览器(previewer)进行预览的话,需要使用 development 版本的 react-reconciler。
    // 因为 previewer 对长串的压缩文本解析有问题。(真机/远程真机没有此问题)
    debugReact: true,
    // 如果需要真机断点调试,需要关闭 sourcemap 的生成
    enableSourceMap: false
  }
}
复制代码

4. 鸿蒙项目配置

鸿蒙配置文件的详细文档:配置文件的元素

4.1 配置路由

假设有鸿蒙项目 MyApplication,如果需要配置路由,可以修改 MyApplication/entry/src/main/config.jsonmodules.js 配置。

4.2 关闭系统自带的顶部导航栏

修改 MyApplication/entry/src/main/config.jsonmodules 配置,增加以下内容:

"metaData": {
  "customizeData":[
    {
      "name": "hwc-theme",
      "value": "androidhwext:style/Theme.Emui.NoTitleBar"
    }
  ]
}
复制代码

5. 编译

taro build --type harmony --watch
复制代码

6. 运行

6.1 创建一个鸿蒙 JS 项目,如 MyApplication

华为开发者工具:developer.harmonyos.com/cn/develop/… 鸿蒙开发文档:developer.harmonyos.com/cn/document…

6.2 使用鸿蒙开发者工具的 previewer、真机、远程真机进行预览

previewer文档:developer.harmonyos.com/cn/docs/doc…

sudo launchctl unload /Library/LaunchDaemons/com.terminal.plist 
关掉公司电脑自动删除 adb 服务的启动项

安装 adb 工具:
brew install android-platform-tools 
复制代码

demo

鸿蒙Taro项目:内容

在此感谢Taro的作者们对我的帮助和悉心教导。Taro适配鸿蒙的开发版本会在这个月底开放出来,希望大家多多指正。

Guess you like

Origin juejin.im/post/7031826512126935076