京東購物小程序做爲京東小程序業務流量的主要入口,承載着許多的活動和頁面,而不少的活動在小程序開展的同時,也會在京東 APP 端進行同步的 H5 端頁面的投放。這時候,一個相同的活動,須要同時開發原生小程序頁面和H5頁面的難題又擺在了前端程序員的面前。
幸運的是,咱們有 Taro,一個開放式跨端跨框架解決方案。能夠幫助咱們很好地解決這種跨端開發的問題。但不幸的是,Taro 並無提供一套完整的將項目做爲獨立分包運行在小程序中的解決方案。所以,本篇文章將介紹如何經過一套合適的混合開發實踐方案,解決 Taro 項目做爲獨立分包後出現的一些問題。javascript
應用過程css
引入公共方法、公共基類和公共組件前端
總的來講,若要使用 Taro 3 將項目做爲獨立分包運行在京東購物小程序,咱們須要完成如下四個步驟:java
那麼接下來,咱們將對每一個步驟進行詳細的說明,告訴你們怎麼作,以及爲何要這樣作。react
首先咱們須要全局安裝 Taro 3,並保證全局和項目下的 Taro 的版本高於3.1.4
,這裏咱們以新建的Taro 3.2.6
項目爲例:webpack
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> ) } }
俗話說得好,有竟者事竟成,在開始編碼前,咱們來簡單地定幾個小目標:web
在將 Taro 項目打包進主購小程序時,咱們很快就遇到了第一個難題:Taro 項目下默認的命令打包出來的文件是一整個小程序,如何打包成一個單獨的分包?shell
幸運的是,在3.1.4
版本後的 Taro,提供了混合開發的功能,意思爲可讓原生項目和 Taro 打包出來的文件混合使用,只須要在打包時加入 --blended
命令便可。json
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 中引入的方式只適用於主包內的頁面,而不適用於分包。
要解決混合開發在分包模式下不適用的問題,咱們須要引入另一個 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 打包出來的運行時文件了。
到目前爲止,咱們已經能夠成功打包出能獨立分包的 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 在多端開發中的性能優化方式。