京东购物小程序 | Taro3 项目分包实践
背景
京東購物小程序作為京東小程序業務流量的主要入口,承載著許多的活動和頁面,而很多的活動在小程序開展的同時,也會在京東 APP 端進行同步的 H5 端頁面的投放。這時候,一個相同的活動,需要同時開發原生小程序頁面和H5頁面的難題又擺在了前端程序員的面前。 幸運的是,我們有 Taro,一個開放式跨端跨框架解決方案。可以幫助我們很好地解決這種跨端開發的問題。但不幸的是,Taro 并沒有提供一套完整的將項目作為獨立分包運行在小程序中的解決方案。因此,本篇文章將介紹如何通過一套合適的混合開發實踐方案,解決 Taro 項目作為獨立分包后出現的一些問題。
目錄
- 背景
- 整體流程
- 應用過程
- 準備合適的開發環境
- 將 Taro 項目作為獨立分包進行編譯打包
- 引入 @tarojs/plugin-indie 插件,保證 Taro 前置邏輯優先執行
- 引入 @tarojs/plugin-mv 插件,自動化挪動打包后的文件
- 引入公共方法、公共基類和公共組件
- 引入公共方法
- 引入公共組件
- 引入頁面公共基類
- 存在問題
- 后續
整體流程
總的來說,若要使用 Taro 3 將項目作為獨立分包運行在京東購物小程序,我們需要完成以下四個步驟:
那么接下來,我們將對每個步驟進行詳細的說明,告訴大家怎么做,以及為什么要這樣做。
應用過程
準備合適的開發環境
首先我們需要全局安裝 Taro 3,并保證全局和項目下的 Taro 的版本高于3.1.4,這里我們以新建的Taro 3.2.6項目為例:
yarn global add @tarojs/cli@3.2.6taro init之后我們在項目中用React語法寫入簡單的 hello word 代碼,并在代碼中留出一個Button組件來為將來調用京東購物小程序的公共跳轉方法做準備。
// src/pages/index/index.jsximport { 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 --blendedblended 中文翻譯是混合的意思,在加入了這個命令后,Taro 會在構建出來的 app.js 文件中導出 taroApp,我們可以通過引入這個變量來在原生項目下的 app.js 調用 Taro 項目 app 的 onShow、onHide 等生命周期。
// 必須引用 Taro 項目的入口文件 const taroApp = require('./taro/app.js').taroAppApp({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.blendedif (!blended) returnconsole.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.jsximport { 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.jsximport { 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-barnavBarData={{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) returnconst 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) returnchild._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 : childhandleAppJsReplace(value)})})} else {handleAppJsReplace(assets[filename]._value)}}} }) }后續
本篇文章主要是講述了 Taro 項目在京東購物小程序端的應用方式和開發方式,暫無涉及 H5 部分的內容。之后計劃輸出一份 Taro 項目在 H5 端的開發指南,并講述 Taro 在多端開發中的性能優化方式。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:
總結
以上是生活随笔為你收集整理的京东购物小程序 | Taro3 项目分包实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “互联网+”时代 母婴行业像叫外卖一样“
- 下一篇: 老板该如何选着程序员帮我做开发?