多重影分身:一套代码如何生成多个小程序?
影分身之基础配置
影分身的能力,主要来源于 Taro 所提供的编译能力,所以需要对 Taro 的编译配置和编译配置详情有所了解。
我们先来看看配置的相关文件目录:
imageconfig 目录为 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 两个重要的配置:
影分身之样式分身
首先,我们来看看最常见的一种需求,那就是不同小程序之间,样式上的差别。我们先来看两张图。
小程序 A:
image小程序 B:
image在样式上,这两个小程序目前的区别有:
-
主色调不同
-
对应图片资源不同
-
排列样式不同
建立分身目录
第一步,在 src 下为每个分身小程序建立一个目录,名字最好与 channel.js 中的配置一样,如下图:
image放置样式差异
以之前的“小程序 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
// 分身配置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以“开发“页面为例,在 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在 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:
image小程序 B:
image编写新页面
我们假设“小程序 B”的“管理”页很难通过配置的方式去做差异化,那么这时,我们只有专门写一个新页面,目录如下:
image其中 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 独有的页面了。
总结
本文所提供的,只是我能够想到的一种解决“多个核心功能类似的小程序需要维护多套代码”这种窘境的方法,如果有更好的方法,希望各位能够告诉我,非常感谢。
最后,给大家推荐一个前端学习进阶内推交流群685910553(前端资料分享),不管你在地球哪个方位,
不管你参加工作几年都欢迎你的入驻!(群内会定期免费提供一些群主收藏的免费学习书籍资料以及整理好的面试题和答案文档!)
如果您对这个文章有任何异议,那么请在文章评论处写上你的评论。
如果您觉得这个文章有意思,那么请分享并转发,或者也可以关注一下表示您对我们文章的认可与鼓励。
愿大家都能在编程这条路,越走越远。