京喜首頁(微信購物入口)跨端開發與優化實踐

背景介紹

隨着今年的雙十一落下帷幕,京喜(原京東拼購)也迎來了首捷。雙十一前夕微信購物一級入口切換爲京喜小程序,項目順利經過近億級的流量考驗,在此與你們分享一點本身參與的工做。css

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

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

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

改版目標

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

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

技術選型

京喜業務擁有很是豐富的產品形態,涵蓋了 H五、微信小程序以及獨立 APP 三種不一樣的端,對支持多端的開發框架有着自然的需求。react

京喜豐富的產品形態

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

Taro 是一套遵循 React 語法規範的多端開發解決方案。webpack

現現在市面上端的形態多種多樣,Web、React-Native、微信小程序等各類端大行其道,當業務要求同時在不一樣的端都要求有所表現的時候,針對不一樣的端去編寫多套代碼的成本顯然很是高,這時候只編寫一套代碼就可以適配到多端的能力就顯得極爲須要。git

使用 Taro,咱們能夠只書寫一套代碼,再經過 Taro 的編譯工具,將源代碼分別編譯出能夠在不一樣端(微信/百度/支付寶/字節跳動/QQ 小程序、快應用、H五、React-Native 等)運行的代碼。github

選它有兩個緣由,一來是 Taro 已經成熟,內部和外部都有大量實踐,內部有京東 7FRESH、京東到家等,外部有淘票票、貓眼試用等多個案例,能夠放心投入到業務開發;二來團隊成員都擁有使用 Taro 來開發內部組件庫的經驗,對業務快速完成有保障。web

組件庫

開發實錄

因爲首頁改版的開發排期並不充裕,所以充分地複用已有基礎能力(好比像請求、上報、跳轉等必不可少的公共類庫),能大量減小咱們重複的工做量。話雖如此,但在三端統一開發過程當中,咱們仍遇到很多問題同時也帶來解決方案,如下咱們一一闡述。

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 級別的封裝,提供了統1、易於開發的 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 和小程序的調試體驗。

React Native Debugger 界面

這樣咱們就擁有了一個正常的開發調試環境,接下來就能夠進行高效的開發了,因爲咱們前面在 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 && <Text></Text> 的返回值是空字符串,RN 嘗試把字符串添加到 View 的 children 時在安卓環境下會報錯:

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

    1. 添加外層容器設置圓角與超出隱藏
    2. 所有角都設置圓角而後使用 transform:tanslate() 藏起不想要的圓角
  • 透明 View 沒法點擊的問題,給設置了 onClick 的元素添加透明背景色便可:

style={{ backgroundColor: "transparent" }}
複製代碼

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

  • 一像素縫隙問題

    多是 RN 佈局引擎的問題,或單位轉換以及瀏覽器渲染中的精度損失問題。能夠調整頁面結構來繞過。 或者簡單粗暴一點,設置負 margin 值蓋住縫隙。

跨平臺開發

JS 文件

一、文件拆分的方式

要"完美"的編譯出三端代碼,首先要解決的是公共類庫的適配問題,好在兄弟業務團隊已經沉澱有完成度較高的三端公共類庫,利用 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'
複製代碼
二、條件編譯的方式

解決了公共類庫適配以後,接下來就能夠專一於業務代碼開發了,一樣業務代碼在三端也可能存差別的狀況,能夠用 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 文件及代碼,一樣也有相似的處理。

一、文件拆分的方式

好比 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 和小程序樣式基本上沒有什麼差別,這裏合爲一個文件處理。

二、條件編譯的方式

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

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章:

相關文章
相關標籤/搜索