用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 實戰教程 查看內容

07年湖人vs火箭:京喜首頁(微信購物入口)跨端開發與優化實踐

湖人vs爵士季后赛 www.nbgeh.club Rolan 2019-12-4 00:45

隨著今年的雙十一落下帷幕,京喜(原京東拼購)也迎來了首捷。雙十一前夕微信購物一級入口切換為京喜小程序,項目順利通過近億級的流量考驗,在此與大家分享一點自己參與的工作。在接手項目前,京喜業務已在線上穩定 ...

隨著今年的雙十一落下帷幕,京喜(原京東拼購)也迎來了首捷。雙十一前夕微信購物一級入口切換為京喜湖人vs爵士季后赛,項目順利通過近億級的流量考驗,在此與大家分享一點自己參與的工作。

在接手項目前,京喜業務已在線上穩定運行較長時間。但經過一段時間迭代維護后,發現首頁存在以下問題:

  1. H5 版本首頁針對不同渠道開發了多套頁面,對開發者維護和內容運營來說存在較大挑戰,需投入大量人力成本;
  2. 項目技術棧不統一,分別有傳統 H5 開發、原生小程序開發、wqVue 框架開發,嚴重影響項目復雜度,迭代過程苦不堪言;
  3. H5、小程序以及 RN 三端存在各自構建和發布流程,涉及較多工具及復雜系統流程,影響業務交付效率。

綜上所述,京喜迎來一次改版契機。

改版目標

從前端角度來看,本次改版要實現以下目標:

  • 升級并統一項目技術棧,解決項目技術棧混亂的現狀;
  • 使用一套代碼,適配微信入口、手 Q 入口、微信小程序、京東 APP、京喜 APP、M 站六大業務場景,減少多套頁面的維護成本,提升交付效率;
  • 通過讓 RN 技術在業務上的落地,完善團隊在 App 端的技術儲備;
  • 優化頁面性能及體驗,為下沉市場用戶提供優質的產品體驗;

技術選型

京喜業務擁有非常豐富的產品形態,涵蓋了 H5、微信小程序以及獨立 APP 三種不同的端,對支持多端的開發框架有著天然的需求。

在技術選型上,我們選擇團隊自研的 Taro 多端統一開發解決方案。

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 來進行繁瑣的目錄文件處理,比如我們的小程序頁面和組件都需要繼承主購小程序的 JDPage 和 JDComponent 基類,所以在進行文件復制之前需要進行代碼替換,代碼如下:

// 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(
              `:full_moon_with_face: 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 針對我們常見的場景提供了一些最佳實踐,可以作為布局參考:

Image
flex
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 && <Text></Text> 的返回值是空字符串,RN 嘗試把字符串添加到 View 的 children 時在安卓環境下會報錯:

    Error: Cannot add a child that doesn't have a YogaNode
    復制代碼
  • border-radius 導致背景色異常,單獨給某個角設置圓角時,沒有設置圓角的邊會出現一塊與背景色顏色相同,但半透明的色塊。

    transform:tanslate()
    
  • 透明 View 無法點擊的問題,給設置了 onClick 的元素添加透明背景色即可:

style={{ backgroundColor: "transparent" }}
復制代碼

不可以用 scss 寫,只有寫在 JSX 上的才有效,Taro 編譯時可能把透明背景色忽略了。

  • 一像素縫隙問題

    可能是 RN 布局引擎的問題,或單位轉換以及瀏覽器渲染中的精度損失問題??梢緣髡趁娼峁估慈乒?。 或者簡單粗暴一點,設置負 margin 值蓋住縫隙。

跨平臺開發

JS 文件

1、文件拆分的方式

要"完美"的編譯出三端代碼,首先要解決的是公共類庫的適配問題,好在兄弟業務團隊已經沉淀有完成度較高的三端公共類庫,利用 Taro 提供的跨平臺開發能力,抹平三端方法名和參數不統一的情況,即可很好的解決公共類庫的適配問題,如下所示:

.
├── goto.h5.js
├── goto.rn.js
├── goto.weapp.js
├── request.h5.js
├── request.rn.js
├── request.weapp.js
└── ...
復制代碼

以 request 公共組件為例,三端代碼如下:

request.h5.js

import request from '@legos/request'
export { request }
復制代碼

request.rn.js

import request from '@wqvue/jdreact-request'
export { request }
復制代碼

request.weapp.js(由于小程序的公共組件沒有發布至 npm,這里引用的本地項目源文件)

import { request } from '../../../common/request/request.js'
export { request }
復制代碼

如遇到需要適配的方法參數不一致或者增加額外處理的情況,可進行再包裝確保最終輸出的接口一致,如下:

goto.rn.js

import jump from '@wqvue/jdreact-jump'

function goto(url, params = {}, options = {}) {
  jump(url, options.des || 'm', options.source || 'JDPingou', params)
}

export default goto
復制代碼

文件引入的時候我們正常使用就好,Taro 在編譯的時候為我們編譯對應的平臺的文件

import goto from './goto.js'
復制代碼

2、條件編譯的方式

解決了公共類庫適配之后,接下來就可以專注于業務代碼開發了,同樣業務代碼在三端也可能存差異的情況,可以用 Taro 提供的環境變量來達到目的,示例代碼如下:

if (process.env.TARO_ENV === 'h5') {
  this.speedReport(8) // [測速上報] 首屏渲染完成
} else if (process.env.TARO_ENV === 'weapp') {
  speed.mark(6).report() // [測速上報] 首屏渲染完成
} else if (process.env.TARO_ENV === 'rn') {
  speed.mark(7).report() // [測速上報] 首屏渲染完成
}
復制代碼

CSS 文件

以上是 js 的代碼處理方式,對于 css 文件及代碼,同樣也有類似的處理。

1、文件拆分的方式

比如 RN 相對于 H5 和小程序的樣式就存在比較大的差異,RN 支持的樣式是 CSS 的子集,所以很多看起來很常見的樣式是不支持的,可以通過以下方式進行差異化處理:

├── index.base.scss
├── index.rn.scss
├── index.scss
復制代碼

這里以 index.base.scss 作為三端都能兼容的公共樣式(名字可以任取,不一定為 xxx.base.scss), index.rn.scss 則為 RN 端獨特的樣式, index.scss 則為 H5 和小程序獨特的樣式,因為 H5 和小程序樣式基本上沒有什么差異,這里合為一個文件處理。

2、條件編譯的方式

Taro 也支持樣式文件內的條件編譯,語法如下:

/* #ifdef %PLATFORM% */
// 指定平臺保留
/* #endif */

/* #ifndef %PLATFORM% */
// 指定平臺剔除
/* #endif */
復制代碼

%PLATFORM% 的取值請參考 Taro 內置環境變量

以下為示例代碼:

.selector {
  color: #fff;
  /* #ifndef RN */
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .1);
  /* #endif */
}
復制代碼

編譯為 H5 和小程序的樣式為:

.selector {
  color: #fff;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .1);
}
復制代碼

RN 的樣式為:

.selector {
  color: #fff;
}
復制代碼

兩種方式選其一即可,這樣就能開開心心的編寫業務代碼了。

有些許遺憾的是產品經理對這次新版首頁有著明確的上線優先級:先 H5 版,再微信小程序版,最后是 RN 版,這就為后續 RN 版本跟 H5 和 小程序版本分道揚鑣埋下了伏筆,條件允許的話建議優先以 RN 版本為基準進行開發,以免開發完成 H5 和小程序之后發現對結構和樣式進行大的調整,因為 RN 對樣式確實會弱一些。

性能優化

圖片優化

電商性質的網站,會存在大量的素材或商品圖片, 往往這些會對頁面造成較大的性能影響。得益于京東圖床服務,提供強大的圖片定制功能,讓我們在圖片優化方面省去大量工作。以引入商品圖片 "https://img10.360buyimg.com/mobilecms/s355x355_jfs/t1/55430/24/116/143859/5cd27c99E71cc323f/0e8da8810fb49796.jpg!q70.dpg.webp" 為樣本,我們對圖片應用做了部分優化:

  • 根據容器大小適當裁剪圖片尺寸:s355x355_jfs
  • 根據網絡環境設置圖片品質參數:0e8da8810fb49796.jpg!q70
  • 根據瀏覽器環境合理選擇圖片類型:0e8da8810fb49796.jpg!q70.dpg.webp

為 Image 標簽設置 lazyload 屬性,這樣可以在 H5 和小程序下獲得懶加載功能。

接口聚合直出

起初京喜首頁的首屏數據涉及的后端接口多達 20 余個,導致整體數據返回時間較長;為了解決此項痛點,我們聯合后端團隊,獨立開發首屏專用的 聚合直出接口 。一方面,將眾多接口請求合并成一個,減少接口聯動請求帶來的性能損耗;另一方面,將復雜的業務邏輯挪到后端處理,前端只負責視圖渲染和交互即可,減少前端代碼復雜度;通過此項優化,頁面性能和體驗得到極大改善。

緩存優先策略

由于京喜業務主要圍繞下沉市場,其用戶群體的網絡環境會更加復雜,要保障頁面的性能,減少網絡延時是一項重要措施。

為了提升用戶二次訪問的加載性能,我們決定采用 緩存優先策略 。即用戶每次訪問頁面時所請求的主接口數據寫入本地緩存,同時用戶每次訪問都優先加載緩存數據,形成一套規范的數據讀取機制。通過優先讀取本地緩存數據,可讓頁面內容在極短時間內完成渲染;另外,本地緩存數據亦可作為頁面兜底數據,在用戶網絡超時或故障時使用,可避免頁面空窗的情景出現。

高性能瀑布流長列表

首頁緊接著首屏區域的是一個支持下滑加載的瀑布流長列表,每次滑到底部都會異步拉取 20 條數據,總計會拉取將近 500 條數據,這在 iOS 下交互體驗還比較正常。但是在配置較低的安卓機型下,當滑動到 2 到 3 屏之后就開始出現嚴重卡頓,甚至會閃退。

針對這種場景也嘗試過用 FlatList 和 SectionList 組件來優化,但是它們都要求規則等高的列表條目,于是不得不自己來實現不規則的瀑布流無限滾動加載。其核心思路是通過判斷列表的條目是否在視窗內來決定圖片是否渲染,要優化得更徹底些得話,甚至可以移除條目內所有內容只保留容器,以達到減少內容節點以及內存占用,不過在快速進行滑動時比較容易出現一片白框,算是為了性能損失一些體驗,整體上來說是可以接受得。

由于 RN 下在獲取元素坐標偏移等數據相對 H5 和小程序要麻煩得到,具體的實現細節可以查看抽離出來的簡單實現 Taro 高性能瀑布流組件(for RN) 。

寫在最后

這篇文章從技術選型、開發實錄再到性能優化三個維度對京喜首頁改版做了簡單總結。整個項目實踐下來,證實 Taro 開發框架 已完全具備投入大型商業項目的條件。雖在多端開發適配上耗費了一些時間,但仍比各端獨立開發維護工作量要少;在前端資源匱乏的今天,選擇成熟的開發工具來控制成本、提升效率,已是各團隊的首要工作目標。 同時,京喜作為京東戰略級業務,擁有千萬級別的流量入口,我們對頁面的體驗優化和性能改進遠不止于此,希望每一次微小的改動能為用戶帶來愉悅的感受,始終為用戶提供優質的產品體驗。

歡迎關注凹凸實驗室博客: aotu.io

分享至 : QQ空間
收藏
原作者: 凹凸實驗室 來自: 掘金