影分身术,看过火影的都知道,一个本体,多个分身。html
你们确定要问了,那小程序开发跟影分身术也能扯上关系?没错,那天然就是:一套代码,多个小程序啦。前端
各位先别翻白眼,且听我细细说来。。。java
现在小程序发展如日中天,再加上微信的力推,不少公司的业务也都慢慢的转向小程序,这让我这个安卓开发,也不得不开始了小程序开发之旅。git
然而随着公司的发展,客户愈来愈多,核心功能相同的小程序,须要上架多个小程序分别给不一样的客户使用,每一个小程序之间又存在这一小部分的定制化,好比界面展现的不一样、小功能的差别等等。github
这可以让我这个刚接触小程序开发的前端菜鸟抓狂了,每一个小程序复制一份代码出来,而后作定制化的修改?这岂不是若是哪天核心业务有改动,我得对每套代码分别改动一次?不行,即便是菜鸟,对这种弄出多套重复代码的行为也是没法容忍的!!json
因而,就有了针对这种场景下的一个解决方案:给小程序开发来个影分身术。小程序
Github地址:https://github.com/BakerJQ/We...微信
该项目基于Taro框架,由凹凸实验室开源,很是感谢他们的努力付出。app
之因此选用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/We...