小程式開發之影分身術

BakerJQ發表於2019-03-01

小程式開發之影分身術

前言

影分身術,看過火影的都知道,一個本體,多個分身。

大家肯定要問了,那小程式開發跟影分身術也能扯上關係?沒錯,那自然就是:一套程式碼,多個小程式啦。

各位先別翻白眼,且聽我細細說來。。。

如今小程式發展如日中天,再加上微信的力推,很多公司的業務也都慢慢的轉向小程式,這讓我這個安卓開發,也不得不開始了小程式開發之旅。

然而隨著公司的發展,客戶越來越多,核心功能相同的小程式,需要上架多個小程式分別給不同的客戶使用,每個小程式之間又存在這一小部分的定製化,比如介面展示的不同、小功能的差異等等。

這可讓我這個剛接觸小程式開發的前端菜鳥抓狂了,每個小程式複製一份程式碼出來,然後做定製化的修改?這豈不是如果哪天核心業務有改動,我得對每套程式碼分別改動一次?不行,即使是菜鳥,對這種弄出多套重複程式碼的行為也是無法容忍的!!

於是,就有了針對這種場景下的一個解決方案:給小程式開發來個影分身術。

Github地址:https://github.com/BakerJQ/WeAppBunXin

該專案基於Taro框架,由凹凸實驗室開源,非常感謝他們的努力付出。

之所以選用Taro,主要是因為它採用React語法標準,而本人之前有過ReactNative開發經驗。

由於本人接觸前端開發時間不長,文中若出現了錯誤或者有更好的方案,歡迎各位包容和指正,萬分感謝。

影分身之基礎配置

影分身的能力,主要來源於Taro所提供的編譯能力,所以需要對Taro的編譯配置編譯配置詳情有所瞭解。

我們先來看看配置的相關檔案目錄:

小程式開發之影分身術

config目錄為Taro初始化後的預設配置目錄,圖中藍色框框內的三個檔案(dev、index、prod)為預設生成的配置檔案,剩下的檔案,則為分身所需的配置。圖中配置了三個分身,我們以channel1為例,config是該分身的一些配置,project.config.json就是該分身小程式的基本配置,如:

{
    "miniprogramRoot": "./",
    "projectname": "channel1",
    "description": "channel1",
    "appid": "wx8888888888888",
    ...
}
複製程式碼

channel.js檔案,則是用來指定,當前需要編譯哪個小程式,如:

module.exports = {
  channel: 'channel1'
}
複製程式碼

在預設的編譯配置入口檔案index.js中,我們需要配置小程式的輸出目錄,配置如下:

const channelInfo = require('./channel')
const config = {
    ...
    //輸入目錄為dist_channel1
    outputRoot: 'dist_' + channelInfo.channel,
    ...
    //講config/channel1/project.config.json檔案拷貝到dist_channel1下
    copy: {
    patterns: [
      {
        from: 'config/' + channelInfo.channel + '/project.config.json',
        to: 'dist_' + channelInfo.channel + '/project.config.json'
      }
    ],
    ...
    }
    ...
}
複製程式碼

執行Taro的小程式編譯命令後,將會生成該分身對應的小程式程式碼資料夾dist_channel1,直接使用小程式開發者工具開啟該目錄,就可以進行channel1小程式的預覽了。

通過這些配置,我們就可以通過同一套程式碼,生成多個不同的小程式啦!當然,這些小程式的內容是完全一樣的,頂多就是project.config.json中配置的名字、appid有不同而已。

那麼下面,我們就開始看看如何實現生成多個有差異化的小程式。

在具體實現之前,我們需要知道Taro兩個重要的配置:全域性變數"defineConstants"別名"alias"

影分身之樣式分身

首先,我們來看看最常見的一種需求,那就是不同小程式之間,樣式上的差別。我們先來看兩張圖。

小程式A 小程式B
小程式開發之影分身術
小程式開發之影分身術

在樣式上,這兩個小程式目前的區別有:

  • 主色調不同
  • 對應圖片資源不同
  • 排列樣式不同

建立分身目錄

第一步,在src下為每個分身小程式建立一個目錄,名字最好與channel.js中的配置一樣,如下圖:

小程式開發之影分身術

放置樣式差異

以之前的“小程式A”來舉例:

其中assets資料夾就是該小程式的資原始檔,即各種藍色的圖示。

app.less為全域性的樣式檔案,內容如下:

@main_color: #1296db;
.main_color_txt {
  color: @main_color
}
複製程式碼

ChannelStyle.ts檔案則為可能在程式碼中需要用到的樣式:

const ChannelStyle = {
  mainColor: '#1296db'
}
export default ChannelStyle
複製程式碼

配置別名

在放置好各類樣式差異後,就可以進行全域性變數和別名的配置了,在專案的config下的index.js中做如下配置

const config = {
  ...
  alias: {
    '@/channel': path.resolve(__dirname, '..', 'src/channel/' + channelInfo.channel),
    '@/assets': path.resolve(__dirname, '..', 'src/channel/' + channelInfo.channel + '/assets'),
    '@/app_style': path.resolve(__dirname, '..', 'src/channel/' + channelInfo.channel + '/app.less'),
  }
  ...
}
複製程式碼

這樣,在程式碼中就可以通過別名進行引用了

//程式碼中需要用到ChannelStyle中的樣式
import ChannelStyle from '@/channel/ChannelStyle'
//app.tsx入口檔案引用全域性樣式
import '@/app_style'
//引用資源圖片
<Image src={require('@/assets/icon.png')} />
複製程式碼

另外請注意,由於目前Taro還未在.less等樣式檔案中支援別名,所以無法通過類似@import ‘@/app_style’的方式進行引用,所以目前需要在每個分身包下放置全量的差異樣式

配置全域性變數

由於對於TabBar的配置,是純字串的形式,無法通過別名配置,所以需要使用另一種配置方式,也就是全域性變數,在index.js的配置方式如下:

const config = {
  defineConstants: {
    ASSETS_PATH: 'channel/'+channelInfo.channel+'/assets'
  }
}
複製程式碼

但是主色調每個分身都不一樣,所以需要在分身的配置檔案中配置,就是基礎配置中,分身資料夾下的config.js,在其中加入全域性變數的配置:

module.exports = {
  ...
  defineConstants: {
    MAIN_COLOR: '#1296db'
  },
  ...
}
複製程式碼

全域性變數在程式碼中可以直接使用,如app.tsx中TabBar的配置:

config: Config = {
    ...
    tabBar: {
      ...
      selectedColor: MAIN_COLOR,
      list: [
        {
          pagePath: 'pages/index/index',
          text: '首頁',
          iconPath: ASSETS_PATH + '/home_u.png',
          selectedIconPath: ASSETS_PATH + '/home_s.png'
        },
        ...
      ]
    }
  }
複製程式碼

配置合併

在配置完成之後,在index.js檔案最後的合併程式碼中,加上我們定義的分身配置:

module.exports = function (merge) {
  ...
  //預設的原始程式碼為return merge({}, config, envConfig)
  return merge({}, config, envConfig, require('./' + channelInfo.channel + "/config"))
}
複製程式碼

樣式分身小結

如此,根據“小程式B”的資原始檔和主題色配置之後,通過修改channel.js中的編譯分身名,就可以生成這兩個小程式了。

我們可能還發現,“小程式A”和“小程式B”的樣式差異,除了資源圖片和主題色之外,“開發”頁面的佈局方式也有差異,這該怎麼處理呢?沒錯,還是通過別名指定less檔案的方式,為各頁面指定對應的樣式檔案。

如果說在實際業務中,不同的小程式存在明顯的主題樣式風格差異的話,建議可以建立主題包,然後為不同的小程式分身配置不同的主題包,如:

小程式開發之影分身術

//分身配置
module.exports = {
  ...
  alias: {
    '@/theme': path.resolve(__dirname, '..', '../src/theme/theme1'),
    ...
  }
  ...
}
//檔案引用
import '@/theme/dev.less'
複製程式碼

影分身之功能分身

除了樣式差異之外,有定製化屬性的小程式一定也會存在一定的功能性差異。

細心的小夥伴可能發現了,“小程式A”和“小程式B”開發頁面的條目數是不一樣的。

“小程式A”並沒有FireWall這一項,而且,這兩個小程式的前兩個條目Java和JSX的順序是不一樣的。不僅如此,如果執行小程式,點選各項的話你會發現,點選C++這一項,“小程式B”是跳轉到條目詳情頁面,而“小程式A”則是跳轉到“管理”Tab頁。

類似這種功能性的差異,我們該如何處理呢?

定義頁面配置

我所想到的思路是,給具有差異化的頁面,提供差異化的配置項,然後通過合併的方式,合併具有差異的分身配置。

我們先來看定義完成後的配置目錄,該目錄在src下:

小程式開發之影分身術

以“開發“頁面為例,在DevConfig.ts中,我定義瞭如下的配置:

import Taro from "@tarojs/taro";
//頁面配置
export default {
  dev: {
    items: {//條目
      item1: {//條目1
        img: require('@/assets/jsx.png'),//圖片
        txt: 'JSX',//文字
        onItemClick: () => {//點選跳轉事件
          toPage('JSX', require('@/assets/jsx.png'))
        }
      },
      item2: {...},
      ...
    }
  }
}
//頁面跳轉
function toPage(title, img){
  Taro.navigateTo({url: '/pages/dev/DevInfo?title='+title+'&img='+img})
}
複製程式碼

定義差異合併

同時,diff包下的ChannelConfigDiff.ts檔案,作為差異配置檔案,其內容如下:

export default (config, merge)=>{
  return merge([{}, config])
}
複製程式碼

可以看出,這其實就是把傳入的config原封不動的返回了,因為對於專案主體來說,config是不需要改變的,具體的用途,會在下面說明。

而MultiChannelConfig.ts則為最終的各頁面配置,內容如下:

import merge from 'deepmerge'
import ChannelConfigDiff from '@/diff/ChannelConfigDiff'
//開發頁面配置
import DevConfig from './pages/DevConfig'
//合併基本頁面配置
const baseConfig = Object.assign({}, DevConfig)
//合併差異頁面配置
const config = ChannelConfigDiff(baseConfig, merge.all)
//開發頁面最終配置
export const devConfig = config.dev
複製程式碼

定義差異配置

在上面的定義中,我們發現ChannelConfigDiff是根據別名引用的,現在大家應該明白ChannelConfigDiff.ts檔案的作用了吧?沒錯,就是通過在各分身中加入這個檔案,並編寫配置。

以“小程式A”為例,diff目錄如下:

小程式開發之影分身術

在channel2的ChannelConfigDiff.ts中,只需要配置具體的差異項即可,未配置的則採用預設的配置:

const dev = {
  dev: {
    items: {
      item1: {//定義第一個item為java內容
        img: require('@/assets/java.png'),
        txt: 'Java',
        onItemClick: () => {
          toPage('Java', require('@/assets/java.png'))
        }
      },
      item2: {...},//第二個item為jsx內容
      item5: null,//第五個item(FireWall)為空
      item8: {
        onItemClick: () => {//最後一個item(C++)點選後跳轉TAB
          Taro.switchTab({url: '/pages/index/Manage'})
        }
      }
    }
  }
}
//將dev配置合併到原始整體配置
export default (config, merge) => {
  return merge([{}, config, dev])
}
複製程式碼

可以看到,該配置中,將item1(原jsx)和item2(原java)的內容對調,將item5(原FireWall)置空,將item8(原C++)點選事件改變。通過這些配置,以達到實現“小程式A”中的功能差異。

最後,別忘了別名的定義,在index.js中,別名配置為:

    '@/diff': path.resolve(__dirname, '..', 'src/config/diff'),
複製程式碼

在channel2的config.js中,別名配置為:

    '@/diff': path.resolve(__dirname, '..', '../src/channel/channel2/diff'),
複製程式碼

功能分身小結

如果有了其他的頁面差異的話,通過類似的增加配置,來進行差異化處理,檔案的目錄格式並無要求,只需要保證配置檔名一致、別名配置正確就可以了。

這時,編譯過後,生成的“小程式A”就擁有樣式和功能差異化的“開發”頁面了。

通過這種方式進行差異化配置,就要求對業務有較好的理解和對元件的合理拆分,並且定義出合理的配置項。

影分身之大差異分身

即便使用了樣式分身和功能分身,依然可能出現一些巨大差異的定製化需求,這些巨大的差異導致樣式分身和功能分身的配置成本過大,那這種情況下,該如何是好呢?

如果真的出現這種情況,那也只好斷臂求生了 —— 那就是整體頁面的替換。

我們來看看“小程式A”和“小程式B”的“管理頁面”:

小程式A 小程式B
小程式開發之影分身術
小程式開發之影分身術

編寫新頁面

我們假設“小程式B”的“管理”頁很難通過配置的方式去做差異化,那麼這時,我們只有專門寫一個新頁面,目錄如下:

小程式開發之影分身術

其中pages下的就是專屬於channel3的頁面

頁面替換

替換頁面的方式,其實也是通過全域性變數。

index.js:

defineConstants: {
  PAGE_MANAGE: 'pages/index/Manage',
}
複製程式碼

channel3的config.js:

defineConstants: {
  PAGE_MANAGE: 'channel/channel3/pages/index/Manage'
},
複製程式碼

app.tsx的頁面配置:

  config: Config = {
    pages: [
      ...
      PAGE_MANAGE
    ],
    ...
    tabBar: {
      ...
      list: [
        ...
        {
          pagePath: PAGE_MANAGE,
          ...
        }
      ]
    }
  }
複製程式碼

如此,編譯後,channel3生成“小程式B”的“管理”頁面,就是channel3獨有的頁面了。

總結

本文所提供的,只是我能夠想到的一種解決“多個核心功能類似的小程式需要維護多套程式碼”這種窘境的方法,如果有更好的方法,希望各位能夠告訴我,非常感謝。

由於本人只是一個剛接觸前端不久的安卓開發,還有許多需要學習的地方,如果文中有誤,歡迎指正批評。

具體的程式碼可以到Github查閱,也歡迎各位Star和提Issue。

最後,再次貼一下Github地址:https://github.com/BakerJQ/WeAppBunXin

相關文章