- 一套代码发布多个微信小程序的实践
之前接手了公司的一个微信小程序项目,上线了一段时间之后,需要以这个项目为基础再发布多个小程序。这些小程序的内容基本上都是一样的,只不过它们有不同的名称、主题、图标等等;或者,某几个小程序需要加一些定制化页面,功能等。本文主要记录下我从纯手工复制项目进化到使用命令行工具复制项目的实践过程。这个简单的命令行工具简单粗暴地叫做 quickcopy,文档在这里。 我是使用 Taro 2.2.13 开发小程序的,所以这个工具目前也是在这个环境下开发的。除了 Taro 插件功能 以外,2.x 都可以使用这个工具。 开发工具前 defineConstants 一开始,因为项目也不多,所以我就直接纯手工操作了。比如,我们已经有了一个小程序叫做 小程序A,现在我们需要以这个小程序为基础复制出一个新的小程序,并且在打包之后实现以下 3 个简单的需求: 设置 [代码]config.window.navigationBarTitleText[代码],在 navigation bar 显示各自的小程序名称; 设置 [代码]config.tabBar.selectedColor[代码],在 tabBar 选中时显示不同的颜色; 为新的小程序定制 [代码]config.tabBar[代码] 图标,小程序A 则继续使用原来的图标。 首先,我们使用全局常量来改造 app.jsx 中的 [代码]config[代码]: [代码]// app.jsx config = { tabBar: { // 改造前: selectedColor: '#000' selectedColor: __MAIN_COLOR, list: [ { // 改造前: 'assets/icons/tabbar-home-s.png' selectedIconPath: 'assets/' + __ICON_DIR + '/tabbar-home-s.png' } ] }, window: { // 改造前: navigaionBarTitleText: '小程序A' navigationBarTitleText: __APP_NAME } } [代码] 然后,在 config 目录下分别为这两个小程序创建 Taro 编译配置文件 build-configA.js 和 build-configB.js,写入 [代码]defineConstants[代码]: [代码]// build-configA.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#000'), __ICON_DIR: JSON.stringify('icons') } } // build-configB.js module.exports = { defineConstants: { __APP_NAME: JSON.stringify('小程序B'), __MAIN_COLOR: JSON.stringify('#111'), __ICON_DIR: JSON.stringify('icons-b') } } [代码] 最后,编译打包 小程序A 的时候,我需要在 config/index.js 的最后将 build-configA.js 与基础的编译配置 [代码]merge[代码]。当编译打包 小程序B 的时候也是一样。 [代码]module.exports = function(merge) { return merge({}, config, require('./dev'), require('./build-configA.js')) } [代码] 运行这两个小程序,我们就可以看到它们会显示各自的名称与主题色,小程序B 还会显示定制化的 tabBar 图标。 sass.resource 既然在上面的全局常量中我们已经定义了一个主题色 [代码]__MAIN_COLOR[代码],那么,我们肯定也需要为不同的小程序编写不同的主题样式。 首先,在 src/style/themes 目录下分别为两个小程序创建主题样式文件。然后在 build-configA.js 以及 build-configB.js 中进行全局注入: [代码]// build-configA.js sass: { resource: [ 'src/style/themes/themeA.scss', // build-configB.js 中写 src/style/themes/themeB.scss 'src/style/variable.scss', 'src/style/mixins.scss' ] } [代码] 全局注入后也就不需要在样式文件中一次次写 [代码]@import 'xxx.scss'[代码] 了。但在这里需要注意的是,必须完整的列出需要注入的 3 个文件。虽然像 variable.scss 和 mixins.scss 这种样式文件明显可以在所有项目共享,但如果只在 config/index.js 中注入,而在 build-configA.js 或者 build-configB.js 中只注入主题样式文件的话,是行不通的。 [代码]// build-configA.js sass: { resource: ['src/style/themes/themeA.scss'] } // config/index.js sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } // 以上两个配置 `merge` 之后的结果是 sass: { resource: [ 'src/style/themes/themeA.scss', 'src/style/mixins.scss' ], projectDirectory: path.resolve(__dirname, '..') } [代码] 也就是说,对于数组来说,是按索引位置进行 [代码]merge[代码] 的。 到现在为止,我们实现了为不同的小程序配置不同的名称,icon 以及主题样式。但是本着能偷懒就偷懒的原则,我觉得这些步骤已经有点麻烦了,可以想象下,如果又有新的项目需要发布,我们需要手动做这些事情: 在 config 目录下创建项目的编译配置文件 config-project.js; 如果需要,为新项目建立定制化的 icons 目录; 为新项目创建主题样式文件,并在 config-project.js 中全局注入; 在 config-project.js 编写 [代码]defineConstants[代码],写入不同项目间有差异的常量,其他的常量则写入 config/index.js; 在 config/index.js 合并新项目的编译配置; 目前所有的项目都共享了根目录下的 project.config.json,所以在编译前需要修改 [代码]appid[代码]。 如果哪一天这些项目都需要进行更新,可以想象下: 首先修改 config/index.js 中需要 [代码]merge[代码] 的项目配置路径; 然后修改 project.config.json 中的 [代码]appid[代码]; 最后编译打包; 如此循环; 那么,上面这些步骤是不是可以交给程序来完成呢?为了尽可能偷懒,我就写了一个简单的命令行工具。它可以代替我们完成以下事情: 以 config/index.js 为模版,提取部分编译配置,创建并写入到新项目的 Taro 编译配置文件; 以根目录 project.config.json 为模版,创建新项目的小程序项目配置文件; 创建新项目的主题样式文件,并在编译配置全局注入; 在打包时寻找新项目有没有定制化图标,如果有,则替换。 开发工具后 假定我们已经有了一份现有项目 小程序A 的编译配置: [代码]// config/index.js const config = { projectName: 'projectA', outputRoot: 'dist', copy: { patterns: [ { from: 'src/components/wxs', to: 'dist/components/wxs' }, // ... ] }, sass: { resource: [ 'src/style/variable.scss', 'src/style/mixins.scss', // ... ], projectDirectory: path.resolve(__dirname, '..') }, defineConstants: { HOST: JSON.stringify('www.baidu.com'), APP_NAME: JSON.stringify('小程序A'), MAIN_COLOR: JSON.stringify('#999'), // ... } } [代码] 在这份编译配置里,指定了项目的输出目录是 dist,全局注入了 variable.scss 和 mixins.scss 文件,并指定了 3 个常量。由于 Taro 不会打包 wxs,所以在 [代码]copy.patterns[代码] 手动将 wxs 复制到了输出目录。 在复制项目之前,我们先对编译配置进行一点改造。在 [代码]defineConstants[代码] 中,我们找到那些不同项目间存在差异的常量,在这里就是 [代码]APP_NAME[代码] 和 [代码]MAIN_COLOR[代码],添加双下划线 [代码]__[代码] 作为开头,这样工具就知道这些常量是存在差异的,而剩余的常量在所有项目中都是一样的。然后在 variable.scss 中找到那些与主题有关的变量,这些变量随后需要写入项目各自的主题样式文件中。 对于已存在的项目 projectA,我们最好也进行一次复制操作。这样一来它就可以拥有独立的编译配置,而 config/index.js 不仅会作为一份基础的编译配置被所有项目共享,也会作为创建新项目独立编译配置时的一份模版。 复制项目 以分离已有的 projectA 项目为例(复制新项目也是类似的),在根目录执行: [代码]qc copy projectA wx123456789a [代码] 工具可以代替我们完成这些工作: 创建 Taro 编译配置文件,路径为 config/config-projectA/index.js; 以根目录 project.config.json 为模版创建微信小程序项目配置文件,路径为 config/config-prjectA/project.config.json; [代码]{ "miniprogramRoot": "dist-projectA/", "projectname": "projectA", "appid": "wx123456789a" } [代码] 其余的内容则会与根目录下的 project.config.json 保持一致; 以 src/style、src/styles 以及 src/css 为顺序查找是否存在这些样式目录。如果存在,则在对应目录下创建 themes/projectA.scss 主题样式文件;如果以上几个目录都不存在,则默认在 src/style 下创建。具体的样式则需要手动写入; 从 config/index.js 找到需要全局注入的样式文件,即 [代码]sass.resource[代码],与上一步创建的主题样式文件一同注入到 config/config-projectA/index.js: [代码]sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss', ] } [代码] 主题样式文件会放在第一位,以便 variable.scss 和 mixins.scss 可以依赖主题样式。 从 config/index.js 找到需要复制到输出目录的文件,即 [代码]copy.patterns[代码],修改 [代码]to[代码] 指定的路径; [代码]copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] } [代码] 从 config/index.js 中找到不同项目间具有差异的常量,即 [代码]defineConstants[代码] 中 [代码]__[代码] 开头的常量,并自动添加一个名为 [代码]__PROJECT[代码] 的新常量; [代码]defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('projectA') } [代码] 所以最终的 config/config-projectA/index.js 就像这样: [代码]module.exports = { projectName: 'projectA', outputRoot: 'dist-projectA', defineConstants: { __APP_NAME: JSON.stringify('小程序A'), __MAIN_COLOR: JSON.stringify('#999'), __PROJECT: JSON.stringify('prjectA') }, copy: { patterns: [ { from: 'src/components/wxs', to: 'dist-projectA/components/wxs' } ] }, sass: { resource: [ 'src/style/themes/projectA.scss', 'src/style/variable.scss', 'src/style/mixins.scss' ] } } [代码] 至于上文的 icon 问题,因为 Taro 提供了插件能力,所以我们不再需要像上文一样引入 [代码]__ICON_DIR[代码] 常量并改造 [代码]selectedIconPath[代码]。只需要在 config/index.js 的 [代码]plugins[代码] 中添加 [代码]quickcopy/plugin-copy-assets[代码] 即可。 举个例子,我们原本将 icon 放在 src/assets/icons 目录下,如果我们想为 projectA 指定定制化的 [代码]tabBar.list.selectedIconPath[代码],只需要新建一个名为 src/assets/icons-projectA 的目录,在这个目录下存放 projectA 定制化的 icon 即可。 当打包 projectA 的时候,这个插件会去 assets/icons-projectA 查找是否存在定制化的 icon,如果存在,则使用这个 icon,如果不存在,则使用 assets/icons 中默认的 icon。 其他的 icon 也是同样的道理。 编译前准备 当我们需要编译 projectA 的时候,在根目录执行: [代码]qc prep projectA [代码] 工具会做以下两件事情: 创建 config/build.export.js 文件,并将 config/config-projectA/index.js 导出; [代码]const buildConfig = require('./config-projectA/index') module.exports = buildConfig [代码] 将 config/config-projectA/project.config.json 复制到根目录。 我们只需要在 config/index.js 的最后 [代码]merge[代码] build.export.js,随后在根目录执行 Taro 编译指令。 如何添加定制化页面 也许在未来的有一天,我们接到一个需求,需要为 小程序A 添加一个定制化的页面。我们将这个页面路径添加到 app.jsx 的 [代码]config[代码],但又不希望其他小程序打包的时候把这个页面也打包进去。 一开始我使用的方法简单粗暴:在打包其他小程序的时候把这个页面路径注释起来,在打包 小程序A 的时候再把注释打开。 我们可以借助 babel-plugin-preval(在 Taro 文档中也有提到)以及上文的 [代码]__PROJECT[代码] 常量编写逻辑,来确定哪个项目需要打包定制化页面,哪些项目又不需要打包。 首先,把 [代码]config.pages[代码] 提取出来作为一个独立文件,比如: [代码]// pages.js module.exports = function(project) { const pages = [ 'pages/tabBar/home/index', 'pages/tabBar/profile/index' ] if (project == 'projectA') { pages.push('pages/special/index') } return pages } [代码] 然后改造 app.jsx: [代码]const project = __PROJECT class App extends Component { config = { // 这里使用了 project 而没有直接传入 __PROJECT 是因为我在测试的时候发现直接使用 __PROJECT 编译的时候会报错 pages: preval.require('./pages.js', project) } } [代码] 这样一来我们只需要修改 pages.js 就可以添加定制化页面,不仅避免被不需要的项目打包,也能清楚地看出哪些项目有定制化页面哪些没有。对于 [代码]subpackages[代码] 和 [代码]tabBar.list[代码] 也可以做同样的处理。 最后 这个工具到目前为止是根据公司的业务需求开发的,主要功能也并不多,还是有挺大的局限。我也还在探索如何更方便地打包为不同项目编写的定制化页面,所以这个工具还会继续更新下去。
2020-11-18 - 安卓微信中“使用浏览器打开”window.location.href使用scheme拉起app失效?
我们团队要做一个h5唤起app的需求,如果安装则直接唤起,没有安装则引导安装。在微信中的处理是使用一个中转页面引导用户使用浏览器或者 safari 打开页面,然后进入页面时直接使用 window.location.href 指向 app 的 URL scheme。这个方案在 ios 微信中是可以正常使用的,但是在安卓的微信中,使用浏览器打开时在安装了 app 的前提下并没有拉起 app,而是等待2秒之后显示下载 app。使用 vconsole 打印了 URL scheme 和参数都是正常的。 而且如果是在安卓中直接使用浏览器打开就可以正常拉起 app。 请问社区中的各位大佬有没有遇到过类似问题的帮忙指点一下?
2020-06-24 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20