微信购物入口「京喜」首页跨端开发与优化实践

Taro 是一套遵循 React 语法规范的多端开发解决方案。

现如今市面上端的形态多种多样,Web、React-Native、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。

使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动/QQ 小程序、快应用、H5、React-Native 等)运行的代码。

选它有两个原因,一来是 Taro 已经成熟,内部和外部都有大量实践,内部有京东 7FRESH、京东到家等,外部有淘票票、猫眼试用等多个案例,可以放心投入到业务开发;二来团队成员都拥有使用 Taro 来开发内部组件库的经验,对业务快速完成有保障。

开发实录

由于首页改版的开发排期并不充裕,因此充分地复用已有基础能力(比如像请求、上报、跳转等必不可少的公共类库),能大量减少我们重复的工作量。话虽如此,但在三端统一开发过程中,我们仍遇到不少问题同时也带来解决方案,以下我们一一阐述。

H5 篇

我们所有的页面都依赖现有业务的全局公共头尾及搜索栏等组件,这就不可避免的需要将 Taro 开发流程融入到现有开发和发布流程中去。同时公共组件都是通过 SSI的方式引入和维护的,为了能在运行 npm run dev:h5时预览到完整的页面效果,需要对 index.html模版中的 SSI 语法进行解析,index.html模版文件代码结构大致如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  <title>京喜</title>
  <!--#include virtual="/sinclude/common/head_inc.shtml"-->
</head>
<body>
  <div id="m_common_header" style="display:none;"></div>
  <!--S 搜索框-->
  <div id="search_block" class="search_block"></div>
  <div id="smartboxBlock" style="display:none;"></div>
  <!--E 搜索框-->
  <div id="app" class="wx_wrap"></div>
  <!--#include virtual="/sinclude/common/foot.shtml"-->
</body>
</html>

可以看到模版中存在很多类似 <!--#include virtual="..." -->格式的代码,这些就是通过 SSI 方式引入的 H5 公共组件,它的 virtual属性指向的文件不存在于本地而是存在于服务器上的,所以我们遇到的第一个问题就是在本地解析这些文件,确保能预览到完整的页面效果,不然开发调试起来就非常的低效。好在 Taro 有暴露出 webpack 的配置,我们可以通过引入自定义加载器(这里就叫 ssi-loader)来解析这些代码的路径,然后请求服务器上的文件内容并进行替换即可,要实现这个功能只需在项目的 config/dev.js中加入如下代码即可:

module.exports = {
  h5: {
    webpackChain(chain, webpack) {
      chain.merge({
        module: {
          rule: {
            ssiLoader: {
              test: /\.html/,
              use: [
                {
                  loader: 'html-loader'
                },
                {
                  loader: 'ssi-loader',
                  options: {
                    locations: {
                      include: 'https://wqs.jd.com'
                    }
                  }
                }
              ]
            }
          }
        }
      })
    }
  }
}

这样就解决了本地开发调试难点,然后开开心心的进行页面开发。

当页面开发完成之后,接下来遇到的问题就是如何将前端资源部署到测试和生产环境。由于现有开发和发布流程都是基于内部已有的平台,我们临时定制一套也不太现实,所以需要将它融入到 Taro 的流程中去,这里我们引入了 gulp来整合各种构建和发布等操作,只要构建出符合发布平台规范的目录即可利用它的静态资源构建、版本控制及服务器发布等能力,这样我们就打通了整个开发和发布流程。

这套拼凑起来的流程还存在不少的问题,对于新接手的同学有一点小繁琐,有着不少改善的空间,这也是接下来的重点工作方向。另外 Taro 的 H5 端之前是基于 SPA 模式,对于有着多页开发需求的项目来说不太友好,当时反馈给 Taro 团队负责 H5 的同学,很快得到了响应,目前 Taro 已支持 H5 多页开发模式,支持非常迅速。

小程序篇

由于开发完 H5 版之后,对应的业务逻辑就已经处理完了,接下来只需要处理小程序下的一些特殊逻辑(比如分享、前端测速上报等)即可,差异比较大的就是开发和发布流程。

这里讲一下如何在一个原生小程序项目中使用 Taro 进行开发,因为我们的 Taro 项目跟已有的原生小程序项目是独立的两个项目,所以需要将 Taro 项目的小程序代码编译到已有的原生小程序项目目录下,第一步要做的就是调整 Taro 配置 config/index.js,指定编译输出目录以及禁用 app 文件输出防止覆盖已有文件。

const config = {
  // 自定义输出根目录
  outputRoot: process.argv[3] === 'weapp' ? '../.temp' : 'dist',
  // 不输出 app.js 和 app.json 文件
  weapp: {
    appOutput: false
  }
}

由于京喜以前是主购小程序的一个栏目,后面独立成了独立的小程序,但是核心购物流程还是复用的主购小程序,所以这让情况变得更加复杂。这里还是通过 gulp来进行繁琐的目录文件处理,比如我们的小程序页面和组件都需要继承主购小程序的 JDPageJDComponent基类,所以在进行文件复制之前需要进行代码替换,代码如下:

// WEAPP
const basePath = `../.temp`
const destPaths = [`${basePath}/pages/index/`, `${basePath}/pages/components/`]
const destFiles = destPaths.map(item => `${item}**/*.js`)

/*
 * 基类替换
 */
function replaceBaseComponent (files) {
  return (
    gulp
      .src(files || destFiles, { base: basePath })
      .pipe(
        replace(
          /\b(Page|Component)(\(require\(['"](.*? "'"")\/npm\/)(.*)(createComponent.*)/,
          function(match, p1, p2, p3, p4, p5) {
            const type =
              (p5 || '').indexOf('true') != -1 ||
              (p5 || '').indexOf('!0') != -1
                ? 'Page'
                : 'Component'
            if (type == 'Page') p5 = p5.replace('))', '), true)') // 新:page.js基类要多传一个参数
            const reservedParts = p2 + p4 + p5
            // const type = p1
            // const reservedParts = p2
            const rootPath = p3

            const clsName = type == 'Page' ? 'JDPage' : 'JDComponent'
            const baseFile = type == 'Page' ? 'page.taro.js' : 'component.js'

            console.log(
              `🌝 Replace with \`${clsName}\` successfully: ${this.file.path.replace(
                /.*?wxapp\//,
                'wxapp/'
              )}`
            )
            return `new (require("${rootPath}/bases/${baseFile}").${clsName})${reservedParts}`
          }
        )
      )
      .pipe(gulp.dest(basePath))
  )
}

// 基类替换
gulp.task('replace-base-component', () => replaceBaseComponent())

还有很多类似这样的骚操作,虽然比较麻烦,但是只需要处理一次,后续也很少改动。

RN 篇

对于 RN 开发,也是第一次将它落地到实际的业务项目中,所以大部分时候都是伴随着各种未知的坑不断前行,所以这里也友情提示一下,对于从未使用过的技术,还是需要一些耐心的,遇到问题勤查勤问。

由于京喜 APP 是复用京东技术中台的基础框架和 JDReact 引擎,所以整个的开发和部署都是遵循 JDReact 已有的流程,画了一张大致的流程图如下:

JDReact 平台是在 Facebook ReactNative 开源框架基础上,进行了深度二次开发和功能扩展。不仅打通了 Android/iOS/Web 三端平台,而且对京东移动端基础业务能力进行了 SDK 级别的封装,提供了统一、易于开发的 API。业务开发者可以通过 JDReact SDK 平台进行快速京东业务开发,并且不依赖发版就能无缝集成到客户端(android/iOS)或者转换成 Web 页面进行线上部署,真正实现了一次开发,快速部署三端。

由于京喜 APP 的 JDReact 模块都是独立的 git 仓库,所以需要调整我们 Taro 项目配置 config/index.js的编译输出路径如下:

rn: {
  outPath: '../jdreact-jsbundle-jdreactpingouindex'
}

这样,当我们运行 yarn run dev:rn进行本地开发时,文件自动编译到了 JDReact 项目,接下来我们就可以用模拟器或者真机来进行预览调试了。当我们在进行本地开发调试的时候,最高效的方式还是推荐用 Taro 官方提供的 taro-native-shell原生 React Native 壳子来启动我们的项目,详细的配置参照该项目的 README 进行配置即可。

由于 React Native 官方提供的 Remote Debugger功能非常弱,推荐使用 React Native Debugger来进行本地 RN 调试,提供了更为丰富的功能,基本接近 H5 和小程序的调试体验。

这样我们就拥有了一个正常的开发调试环境,接下来就可以进行高效的开发了,由于我们前面在 H5 和小程序版本阶段已经完成了绝大部分的业务逻辑开发,所以针对 RN 版本的主要工作集中在 iOS 和安卓不同机型的样式和交互适配上。

在样式适配这块,不得不提下 Taro 针对我们常见的场景提供了一些最佳实践,可以作为布局参考:

  • 固定尺寸(按钮、文字大小、间距):写 PX / Px / pX
  • 保持宽高比(比如 banner 图片):Image组件处理
  • 间距固定,内容自适应(比如产品卡片宽度):使用 flex布局
  • 按屏幕等比缩放:使用 px 单位,编译时处理(scalePx2dp动态计算)

Taro RN 最佳实践集锦

在实际开发过程中也遇到不少兼容性问题,这里整理出来以供大家参考:

  • 文本要用 <Text>标签包起来,因为 RN 没有 textNode的概念;

  • 使用 Swiper 时在外面包一个 View,否则设置 margin后会导致安卓下高度异常;

  • Cannot read property 'x' of undefined,Swiper 底层使用的 react-native-swiper 导致的问题,Disable Remote JS Debug 就不会出现。

  • 图片默认尺寸不对,RN 不会自动帮助设置图片尺寸,而是交给开发者自己处理,故意这样设计的;

  • Image 组件上不可以设置 onClick

  • 实现基线对齐:vertical-align: baseline,用 <Text>把需要基线对齐的组件包住即可。

    <Text>
      <Text style={{ fontSize: 20 }}>abc</Text>
      <Text style={{ fontSize: 40 }}>123</Text>
    </Text>
  • 尽量避免使用 line-height,在安卓和 iOS 下表现不一致,而且即使设置为与 fontSize相同也会导致裁剪;

  • android 调试生产环境的 bundle,摇手机,选 Dev Setting,取消勾选第一项 Dev 即可;

  • iOS 调试生产环境的 bundle,AppDelegate.m中增加一行语句关闭 dev 即可:

    [[RCTBundleURLProvider sharedSettings] setEnableDev:false];
      // 找到这行,并在它的上面增加上面这行
      jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  • <Text><View>支持的 style 属性不相同。

    > [Text Style Props](https://facebook.github.io/react-native/docs/text-style-props "Text Style Props") & [View Style Props](https://facebook.github.io/
    

    react-native/docs/view-style-props)

  • render 方法中不要返回空字符串

    下面的代码在 android 下会报错(empty_string 内容为空字符串)

    <View>
      {empty_string && <Text></Text>}
    </View>

    因为 `empty_string &&

原文链接:segmentfault.com

上一篇:前端API层架构,也许你做得还不够
下一篇:你可能不知道的15个 Git 命令

相关推荐

官方社区

扫码加入 JavaScript 社区