京東購物小程式 | Taro3 專案分包實踐

凹凸實驗室發表於2021-08-05

背景

京東購物小程式作為京東小程式業務流量的主要入口,承載著許多的活動和頁面,而很多的活動在小程式開展的同時,也會在京東 APP 端進行同步的 H5 端頁面的投放。這時候,一個相同的活動,需要同時開發原生小程式頁面和H5頁面的難題又擺在了前端程式設計師的面前。
幸運的是,我們有 Taro,一個開放式跨端跨框架解決方案。可以幫助我們很好地解決這種跨端開發的問題。但不幸的是,Taro 並沒有提供一套完整的將專案作為獨立分包執行在小程式中的解決方案。因此,本篇文章將介紹如何通過一套合適的混合開發實踐方案,解決 Taro 專案作為獨立分包後出現的一些問題

目錄

  • 背景
  • 整體流程
  • 應用過程
    • 準備合適的開發環境
    • 將 Taro 專案作為獨立分包進行編譯打包
    • 引入 @tarojs/plugin-indie 外掛,保證 Taro 前置邏輯優先執行
    • 引入 @tarojs/plugin-mv 外掛,自動化挪動打包後的檔案
    • 引入公共方法、公共基類和公共元件
      • 引入公共方法
      • 引入公共元件
      • 引入頁面公共基類
  • 存在問題
  • 後續

整體流程

總的來說,若要使用 Taro 3 將專案作為獨立分包執行在京東購物小程式,我們需要完成以下四個步驟:

  1. 準備開發環境,下載正確的 Taro 版本
  2. 安裝 Taro 混合編譯外掛,解決獨立分包的執行時邏輯問題
  3. 呼叫 Taro 提供的混合編譯命令,對 Taro 專案進行打包
  4. 挪動打包後 Taro 檔案到主購小程式目錄下

那麼接下來,我們將對每個步驟進行詳細的說明,告訴大家怎麼做,以及為什麼要這樣做。

應用過程

準備合適的開發環境

首先我們需要全域性安裝 Taro 3,並保證全域性和專案下的 Taro 的版本高於3.1.4,這裡我們以新建的Taro 3.2.6專案為例:

yarn global add @tarojs/cli@3.2.6

taro init

之後我們在專案中用React語法寫入簡單的 hello word 程式碼,並在程式碼中留出一個Button元件來為將來呼叫京東購物小程式的公共跳轉方法做準備。

// src/pages/index/index.jsx

import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'

import './index.scss'

export default class Index extends Component {
  handleButtonClick () {
    // 呼叫京東購物小程式的公共跳轉方法
    console.log('trigger click')
  }

  render () {
    return (
      <View className='index'>
        <Text>Hello world!</Text>
        <Button onClick={this.handleButtonClick.bind(this)} >點選跳轉到主購首頁</Button>
      </View>
    )
  }
}

俗話說得好,有竟者事竟成,在開始編碼前,我們來簡單地定幾個小目標:

  • 成功地將 Taro 專案 Hello world 在京東購物小程式的分包路由下跑通
  • 引入京東購物小程式的公共元件 nav-bar 並能正常使用
  • 引入公共方法 navigator.goto 並能正常使用
  • 引入公共基類 JDPage 並能正常使用

將 Taro 專案作為獨立分包進行編譯打包

在將 Taro 專案打包進主購小程式時,我們很快就遇到了第一個難題:Taro 專案下預設的命令打包出來的檔案是一整個小程式,如何打包成一個單獨的分包?

幸運的是,在3.1.4版本後的 Taro,提供了混合開發的功能,意思為可以讓原生專案和 Taro 打包出來的檔案混合使用,只需要在打包時加入 --blended 命令即可。

cross-env NODE_ENV=production taro build --type weapp --blended

blended 中文翻譯是混合的意思,在加入了這個命令後,Taro 會在構建出來的 app.js 檔案中匯出 taroApp,我們可以通過引入這個變數來在原生專案下的 app.js 呼叫 Taro 專案 app 的 onShow、onHide 等生命週期。

// 必須引用 Taro 專案的入口檔案
const taroApp = require('./taro/app.js').taroApp

App({
  onShow () {
    // 可選,呼叫 Taro 專案 app 的 onShow 生命週期
    taroApp.onShow()
  },

  onHide () {
    // 可選,呼叫 Taro 專案 app 的 onHide 生命週期
    taroApp.onHide()
  }
})

如果單純地使用 blended 命令,即使我們不需要呼叫 onShow、onHide 這些生命週期,我們也需要在原生專案下的 app.js 裡引入Taro專案的入口檔案,因為在執行我們的小程式頁面時,我們需要提前初始化一些執行時的邏輯,因此要保證 Taro 專案下的 app.js 檔案裡的邏輯能優先執行。

理想很豐滿,現實很骨感,由於我們需要將 Taro 專案作為單獨的分包打包到主購專案中,因此這種直接在原生專案的 app.js 中引入的方式只適用於主包內的頁面,而不適用於分包。

引入 @tarojs/plugin-indie 外掛,保證 Taro 前置邏輯優先執行

要解決混合開發在分包模式下不適用的問題,我們需要引入另外一個 Taro 外掛 @tarojs/plugin-indie

首先我們先在 Taro 專案中對該外掛進行安裝

yarn add --dev @tarojs/plugin-indie

之後我們在 Taro 的配置項檔案中對該外掛進行引入

// config/index.js
const config = {
  // ...
  plugins: [
    '@tarojs/plugin-indie'
  ] 
  // ...
}

檢視該外掛的原始碼,我們可以發現該外掛處理的邏輯非常簡單,就是在編譯程式碼時,對每個頁面下的 js chunk 檔案內容進行調整,在這些 js 檔案的開頭加上 require("../../app"),並增加對應 modulesourceMap 對映。在進行了這樣的處理後,便能保證每次進入 Taro 專案下的小程式頁面時,都能優先執行 Taro 打包出來的執行時檔案了。

引入 @tarojs/plugin-mv 外掛,自動化挪動打包後的檔案

到目前為止,我們已經可以成功打包出能獨立分包的 Taro 小程式檔案了,接下來,我們需要將打包出來的 dist 目錄下的檔案挪到主購專案中。

手動挪動?no,一個優秀的程式設計師應該想盡辦法在開發過程中“偷懶”。
因此我們會自定義一個 Taro 外掛,在 Taro 打包完成的時候,自動地將打包後的檔案移動到主購專案中。

// plugin-mv/index.js
const fs = require('fs-extra')
const path = require('path')

export default (ctx, options) => {
  ctx.onBuildFinish(() => {
    const blended = ctx.runOpts.blended || ctx.runOpts.options.blended
    
    if (!blended) return

    console.log('編譯結束!')

    const rootPath = path.resolve(__dirname, '../..')
    const miniappPath = path.join(rootPath, 'wxapp')
    const outputPath = path.resolve(__dirname, '../dist')

    // testMini是你在京東購物小程式專案下的路由資料夾
    const destPath = path.join(miniappPath, `./pages/testMini`)

    if (fs.existsSync(destPath)) {
      fs.removeSync(destPath)
    }
    fs.copySync(outputPath, destPath)

    console.log('拷貝結束!')
  })
}

在配置檔案中加入這個自定義外掛:

// config/index.js
const path = require('path')

const config = {
  // ...
  plugins: [
    '@tarojs/plugin-indie',
    path.join(process.cwd(), '/plugin-mv/index.js')
  ] 
  // ...
}

重新執行cross-env NODE_ENV=production taro build --type weapp --blended打包命令,即可將 Taro 專案打包並拷貝到京東購物小程式專案對應的路由資料夾中。

至此,我們便可在開發者工具開啟主購小程式專案,在 app.json 上新增對應的頁面路由,並條件編譯該路由,即可順利地在開發者工具上看到 Hello World 字樣。

效果圖

引入公共方法、公共基類和公共元件

在日常的主購專案開發中,我們經常需要用到主購原生專案下封裝的一些公共模組和方法,那麼,通過混合編譯打包過來的 Taro 專案是否也能通過某種辦法順利引用這些方法和模組呢?

答案是可以的。

引入公共方法

先簡單說一下思路,更改 webpack 的配置項,通過 externals 配置處理公共方法和公共模組的引入,保留這些引入的語句,並將引入方式設定成 commonjs 相對路徑的方式,詳細程式碼如下所示:

const config = {
  // ...
  mini: {
    // ...
    webpackChain (chain) {
      chain.merge({
        externals: [
          (context, request, callback) => {
            const externalDirs = ['@common', '@api', '@libs']
            const externalDir = externalDirs.find(dir => request.startsWith(dir))

            if (process.env.NODE_ENV === 'production' && externalDir) {
              const res = request.replace(externalDir, `../../../../${externalDir.substr(1)}`)

              return callback(null, `commonjs ${res}`)
            }

            callback()
          },
        ],
      })
    }
    // ...
  }
  // ...
}

通過這樣的處理之後,我們就可以順利地在程式碼中通過 @common/*@api/*@libs/* 來引入原生專案下的 common/*api/*libs/* 了。

// src/pages/index/index.jsx

import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'

import * as navigator from '@common/navigator.js'

import './index.scss'

export default class Index extends Component {
  handleButtonClick () {
    // 呼叫京東購物小程式的公共跳轉方法
    console.log('trigger click')
    // 利用公共方法跳轉京東購物小程式首頁
    navigator.goto('/pages/index/index')
  }

  render () {
    return (
      <View className='index'>
        <Text>Hello world!</Text>
        <Button onClick={this.handleButtonClick.bind(this)} >點選跳轉到主購首頁</Button>
      </View>
    )
  }
}

能看到引入的公共方法在打包後的小程式頁面中也能順利跑通了

跳轉動畫

引入公共元件

公共元件的引入更加簡單,Taro 預設有提供引入公共元件的功能,但是如果是在混合開發模式下打包後,會發現公共元件的引用路徑無法對應上,打包後頁面配置的 json 檔案引用的是以 Taro 打包出來的 dist 資料夾為小程式根目錄,所以引入的路徑也是以這個根目錄為基礎進行引用的,因此我們需要利用 Taro 的 alias 配置項來對路徑進行一定的調整:

// pages/index/index.config.js
export default {
  navigationBarTitleText: '首頁',
  navigationStyle: 'custom',
  usingComponents: {
    'nav-bar': '@components/nav-bar/nav-bar',
  }
}
// config/index.js
const path = require('path')

const config = {
  // ...
  alias: {
    '@components': path.resolve(__dirname, '../../../components'),
  }
  // ...
}

接著我們在程式碼中直接對公共元件進行使用,並且無需引入:

// src/pages/index/index.jsx

import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'

import * as navigator from '@common/navigator.js'

import './index.scss'

export default class Index extends Component {
  handleButtonClick () {
    // 呼叫京東購物小程式的公共跳轉方法
    console.log('trigger click')
    // 利用公共方法跳轉京東購物小程式首頁
    navigator.goto('/pages/index/index')
  }

  render () {
    return (
      <View className='index'>
        {/* 公共元件直接引入,無需引用 */}
        <nav-bar
          navBarData={{
            title: '測試公共元件導航欄',
            capsuleType: 'miniReturn',
            backgroundValue: 'rgba(0, 255, 0, 1)'
          }}
        />
        <Text>Hello world!</Text>
        <Button onClick={this.handleButtonClick.bind(this)} >點選跳轉到主購首頁</Button>
      </View>
    )
  }
}

這樣打包出來的 index.json 檔案中 usingComponents 裡的路徑就能完美匹配原生小程式下的公共元件檔案了,我們也由此能看到公共導航欄元件 nav-bar 在專案中的正常使用和執行了:

導航欄使用效果圖

引入頁面公共基類

在京東購物小程式,每一個原生頁面在初始化的時候,基本都會引入一個 JDPage 基類,並用這個基類來修飾原本的 Page 例項,會給 Page 例項上原本的生命週期裡新增一些埋點上報和引數傳遞等方法。

而我們在使用 Taro 進行混合編譯開發時,再去單獨地實現一遍這些方法顯然是一種很愚蠢的做法,所以我們需要想辦法在 Taro 專案裡進行類似的操作,去引入 JDPage 這個基類。

首先第一步,我們需要在編譯後的 JS 檔案裡,找到 Page 例項的定義位置,這裡我們會使用正則匹配,去匹配這個 Page 例項在程式碼中定義的位置:

const pageRegx = /(Page)(\(Object.*createPageConfig.*?\{\}\)\))/

找到 Page 例項中,將 Page 例項轉換成我們需要的 JDPage 基類,這些步驟我們都可以將他們寫在我們之前自制 Taro 外掛 plugin-mv 中去完成:

const isWeapp = process.env.TARO_ENV === 'weapp'
const jsReg = /pages\/(.*)\/index\.js$/
const pageRegx = /(Page)(\(Object.*createPageConfig.*?\{\}\)\))/

export default (ctx, options) => {
  ctx.modifyBuildAssets(({ assets }) => {
    Object.keys(assets).forEach(filename => {
      const isPageJs = jsReg.test(filename)

      if (!isWeapp || !isPageJs) return

      const replaceFn = (match, p1, p2) => {
        return `new (require('../../../../../bases/page.js').JDPage)${p2}`
      }

      if (
        !assets[filename]._value &&
        assets[filename].children
      ) {
        assets[filename].children.forEach(child => {
          const isContentValid = pageRegx.test(child._value)

          if (!isContentValid) return

          child._value = child._value.replace(pageRegx, replaceFn)
        })
      } else {
        assets[filename]._value = assets[filename]._value.replace(pageRegx, replaceFn)
      }
    })
  })
}

經過外掛處理之後,打包出來的頁面 JS 裡的 Page 都會被替換成 JDPage,也就擁有了基類的一些基礎能力了。

至此,我們的 Taro 專案就基本已經打通了京東購物小程式的混合開發流程了。在能使用 Taro 無痛地開發京東購物小程式原生頁面之餘,還為之後的雙端甚至多端執行打下了結實的基礎。

存在問題

在使用 Taro 進行京東購物小程式原生頁面的混合開發時,會發現 Taro 在一些公共樣式和公共方法的處理上面,存在著以下一些相容問題:

  1. Taro 會將多個頁面的公共樣式進行提取,放置於 common.wxss 檔案中,但打包後的 app.wxss 檔案卻沒有對這些公共樣式進行引入,因此會導致頁面的公共樣式丟失。解決辦法也很簡單,只要在外掛對 app.wxss 檔案進行調整,新增對 common.wxss 的引入即可:
const wxssReg = /pages\/(.*)\/index\.wxss$/
function insertContentIntoFile (assets, filename, content) {
  const { children, _value } = assets[filename]
  if (children) {
    children.unshift(content)
  } else {
    assets[filename]._value = `${content}${_value}`
  }
}
export default (ctx, options) => {
  ctx.modifyBuildAssets(({ assets }) => {
    Object.keys(assets).forEach(filename => {
      const isPageWxss = wxssReg.test(filename)

      // ...

      if (isPageWxss) {
        insertContentIntoFile(assets, filename, "@import '../../common.wxss';\n")
      }
    }
  })
}
  1. 使用 Taro 打包後的 app.js 檔案裡會存在部分對京東購物小程式公共方法的引用,該部分內容使用的是和頁面 JS 同一個相對路徑進行引用的,因此會存在引用路徑錯誤的問題,解決辦法也很簡單,對 app.js 裡的引用路徑進行調整即可:
const appReg = /app\.js$/
const replaceList = ['common', 'api', 'libs']
export default (ctx, options) => {
  ctx.modifyBuildAssets(({ assets }) => {
    Object.keys(assets).forEach(filename => {
      const isAppJS = appReg.test(filename)
      const handleAppJsReplace = (item) => {
        replaceList.forEach(name => {
          item = item.replace(new RegExp(`../../../../../${name}`, 'g'), `'../../../${name}`)
        })
      }
      if (isAppJS) {
        if (
          !assets[filename]._value &&
          assets[filename].children
        ) {
          assets[filename].children.forEach(child => {
            replaceList.forEach(name => {
              const value = child._value ? child._value : child

              handleAppJsReplace(value)
            })
          })
        } else {
          handleAppJsReplace(assets[filename]._value)
        }
      }
    }
  })
}

後續

本篇文章主要是講述了 Taro 專案在京東購物小程式端的應用方式和開發方式,暫無涉及 H5 部分的內容。之後計劃輸出一份 Taro 專案在 H5 端的開發指南,並講述 Taro 在多端開發中的效能優化方式。

歡迎關注凹凸實驗室部落格:aotu.io

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

歡迎關注凹凸實驗室公眾號

相關文章