背景
京東購物小程序作爲京東小程序業務流量的主要入口,承載着許多的活動和頁面,而很多的活動在小程序開展的同時,也會在京東 APP 端進行同步的 H5 端頁面的投放。這時候,一個相同的活動,需要同時開發原生小程序頁面和H5頁面的難題又擺在了前端程序員的面前。 幸運的是,我們有 Taro,一個開放式跨端跨框架解決方案。可以幫助我們很好地解決這種跨端開發的問題。但不幸的是,Taro 並沒有提供一套完整的將項目作爲獨立分包運行在小程序中的解決方案。因此,本篇文章將介紹如何通過一套合適的混合開發實踐方案,解決 Taro 項目作爲獨立分包後出現的一些問題。
目錄
- 背景
- 整體流程
- 應用過程
- 準備合適的開發環境
- 將 Taro 項目作爲獨立分包進行編譯打包
- 引入 @tarojs/plugin-indie 插件,保證 Taro 前置邏輯優先執行
- 引入 @tarojs/plugin-mv 插件,自動化挪動打包後的文件
- 引入公共方法、公共基類和公共組件
- 引入公共方法
- 引入公共組件
- 引入頁面公共基類
- 存在問題
- 後續
整體流程
總的來說,若要使用 Taro 3 將項目作爲獨立分包運行在京東購物小程序,我們需要完成以下四個步驟:
- 準備開發環境,下載正確的 Taro 版本
- 安裝 Taro 混合編譯插件,解決獨立分包的運行時邏輯問題
- 調用 Taro 提供的混合編譯命令,對 Taro 項目進行打包
- 挪動打包後 Taro 文件到主購小程序目錄下
那麼接下來,我們將對每個步驟進行詳細的說明,告訴大家怎麼做,以及爲什麼要這樣做。
應用過程
準備合適的開發環境
首先我們需要全局安裝 Taro 3,並保證全局和項目下的 Taro 的版本高於3.1.4
,這裏我們以新建的Taro 3.2.6
項目爲例:
yarn global add @tarojs/[email protected]
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")
,並增加對應 module
的 sourceMap
映射。在進行了這樣的處理後,便能保證每次進入 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 在一些公共樣式和公共方法的處理上面,存在着以下一些兼容問題:
- 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")
}
}
})
}
- 使用 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),不定時推送文章。