小程序开发之影分身术

image

前言

影分身术,看过火影的都知道,一个本体,多个分身。

大家肯定要问了,那小程序开发跟影分身术也能扯上关系?没错,那自然就是:一套代码,多个小程序啦。

各位先别翻白眼,且听我细细说来。。。

如今小程序发展如日中天,再加上微信的力推,很多公司的业务也都慢慢的转向小程序,这让我这个安卓开发,也不得不开始了小程序开发之旅。

然而随着公司的发展,客户越来越多,核心功能相同的小程序,需要上架多个小程序分别给不同的客户使用,每个小程序之间又存在这一小部分的定制化,比如界面展示的不同、小功能的差异等等。

这可让我这个刚接触小程序开发的前端菜鸟抓狂了,每个小程序复制一份代码出来,然后做定制化的修改?这岂不是如果哪天核心业务有改动,我得对每套代码分别改动一次?不行,即使是菜鸟,对这种弄出多套重复代码的行为也是无法容忍的!!

于是,就有了针对这种场景下的一个解决方案:给小程序开发来个影分身术。

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

该项目基于Taro框架,由凹凸实验室开源,非常感谢他们的努力付出。

之所以选用Taro,主要是因为它采用React语法标准,而本人之前有过ReactNative开发经验。

由于本人接触前端开发时间不长,文中若出现了错误或者有更好的方案,欢迎各位包容和指正,万分感谢。

影分身之基础配置

影分身的能力,主要来源于Taro所提供的编译能力,所以需要对Taro的编译配置编译配置详情有所了解。

我们先来看看配置的相关文件目录:

[图片上传失败...(image-46a1b0-1551408031294)]

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
[图片上传失败...(image-8c19b9-1551408031294)] [图片上传失败...(image-a1b33d-1551408031294)]

在样式上,这两个小程序目前的区别有:

  • 主色调不同
  • 对应图片资源不同
  • 排列样式不同

建立分身目录

第一步,在src下为每个分身小程序建立一个目录,名字最好与channel.js中的配置一样,如下图:

[图片上传失败...(image-eb1f25-1551408031294)]

放置样式差异

以之前的“小程序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文件的方式,为各页面指定对应的样式文件。

如果说在实际业务中,不同的小程序存在明显的主题样式风格差异的话,建议可以建立主题包,然后为不同的小程序分身配置不同的主题包,如:

[图片上传失败...(image-e020f6-1551408031294)]

//分身配置
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下:

[图片上传失败...(image-35eb34-1551408031294)]

以“开发“页面为例,在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目录如下:

[图片上传失败...(image-95221c-1551408031294)]

在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
[图片上传失败...(image-656f3f-1551408031294)] [图片上传失败...(image-1cdeff-1551408031294)]

编写新页面

我们假设“小程序B”的“管理”页很难通过配置的方式去做差异化,那么这时,我们只有专门写一个新页面,目录如下:

[图片上传失败...(image-5d35a3-1551408031294)]

其中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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容

  • 露腚斗蛐蛐, 光丫不履屐。 管他风与雨, 童趣可充饥。 《长相思》 蝶恋花,蝶恋花, 歇翅芳丛若自家。 风姿娇俏佳...
    明月清泉_e47b阅读 223评论 4 3
  • 我在上一篇文章《说真的,到最后,很有可能你还是那个loser》中,感谢了你们的耐心,让这边文章能够阅读量破百,这对...
    菜头和安子阅读 210评论 0 1
  • (小说) 一日,某男百无聊赖,拿起手机,打开微信“附近的人”,一个叫“勇敢一点”的女孩吸引了他,该女孩资料显示山西...
    兰桂腾芳阅读 1,241评论 0 0
  • 还记得昨天 那个夏天 微风吹过的一瞬间 那个被风吹过的夏天 图片人物 微博@林南生林菇凉
    潘卡西阅读 307评论 0 1