评论

一套代码发布多个微信小程序的实践

以现有小程序为基础,通过编写不同编译配置,生成多个可发布的小程序。

之前接手了公司的一个微信小程序项目,上线了一段时间之后,需要以这个项目为基础再发布多个小程序。这些小程序的内容基本上都是一样的,只不过它们有不同的名称、主题、图标等等;或者,某几个小程序需要加一些定制化页面,功能等。本文主要记录下我从纯手工复制项目进化到使用命令行工具复制项目的实践过程。这个简单的命令行工具简单粗暴地叫做 quickcopy,文档在这里

我是使用 Taro 2.2.13 开发小程序的,所以这个工具目前也是在这个环境下开发的。除了 Taro 插件功能 以外,2.x 都可以使用这个工具。

开发工具前

defineConstants

一开始,因为项目也不多,所以我就直接纯手工操作了。比如,我们已经有了一个小程序叫做 小程序A,现在我们需要以这个小程序为基础复制出一个新的小程序,并且在打包之后实现以下 3 个简单的需求:

  1. 设置 config.window.navigationBarTitleText,在 navigation bar 显示各自的小程序名称;

  2. 设置 config.tabBar.selectedColor,在 tabBar 选中时显示不同的颜色;

  3. 为新的小程序定制 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.jsbuild-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.scssmixins.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 以及主题样式。但是本着能偷懒就偷懒的原则,我觉得这些步骤已经有点麻烦了,可以想象下,如果又有新的项目需要发布,我们需要手动做这些事情:

  1. config 目录下创建项目的编译配置文件 config-project.js

  2. 如果需要,为新项目建立定制化的 icons 目录;

  3. 为新项目创建主题样式文件,并在 config-project.js 中全局注入;

  4. config-project.js 编写 defineConstants,写入不同项目间有差异的常量,其他的常量则写入 config/index.js

  5. config/index.js 合并新项目的编译配置;

  6. 目前所有的项目都共享了根目录下的 project.config.json,所以在编译前需要修改 appid

如果哪一天这些项目都需要进行更新,可以想象下:

  1. 首先修改 config/index.js 中需要 merge 的项目配置路径;

  2. 然后修改 project.config.json 中的 appid

  3. 最后编译打包;

  4. 如此循环;

那么,上面这些步骤是不是可以交给程序来完成呢?为了尽可能偷懒,我就写了一个简单的命令行工具。它可以代替我们完成以下事情:

  1. config/index.js 为模版,提取部分编译配置,创建并写入到新项目的 Taro 编译配置文件

  2. 以根目录 project.config.json 为模版,创建新项目的小程序项目配置文件;

  3. 创建新项目的主题样式文件,并在编译配置全局注入;

  4. 在打包时寻找新项目有没有定制化图标,如果有,则替换。

开发工具后

假定我们已经有了一份现有项目 小程序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.scssmixins.scss 文件,并指定了 3 个常量。由于 Taro 不会打包 wxs,所以在 copy.patterns 手动将 wxs 复制到了输出目录。

在复制项目之前,我们先对编译配置进行一点改造。在 defineConstants 中,我们找到那些不同项目间存在差异的常量,在这里就是 APP_NAMEMAIN_COLOR,添加双下划线 __ 作为开头,这样工具就知道这些常量是存在差异的,而剩余的常量在所有项目中都是一样的。然后在 variable.scss 中找到那些与主题有关的变量,这些变量随后需要写入项目各自的主题样式文件中。

对于已存在的项目 projectA,我们最好也进行一次复制操作。这样一来它就可以拥有独立的编译配置,config/index.js 不仅会作为一份基础的编译配置被所有项目共享,也会作为创建新项目独立编译配置时的一份模版

复制项目

以分离已有的 projectA 项目为例(复制新项目也是类似的),在根目录执行:

qc copy projectA wx123456789a

工具可以代替我们完成这些工作:

  1. 创建 Taro 编译配置文件,路径为 config/config-projectA/index.js

  2. 以根目录 project.config.json 为模版创建微信小程序项目配置文件,路径为 config/config-prjectA/project.config.json

    {
      "miniprogramRoot": "dist-projectA/",
      "projectname": "projectA",
      "appid": "wx123456789a"
    }
    

    其余的内容则会与根目录下的 project.config.json 保持一致;

  3. src/stylesrc/styles 以及 src/css 为顺序查找是否存在这些样式目录。如果存在,则在对应目录下创建 themes/projectA.scss 主题样式文件;如果以上几个目录都不存在,则默认在 src/style 下创建。具体的样式则需要手动写入;

  4. 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.scssmixins.scss 可以依赖主题样式。

  5. config/index.js 找到需要复制到输出目录的文件,即 copy.patterns,修改 to 指定的路径;

    copy: {
      patterns: [
        {
          from: 'src/components/wxs',
          to: 'dist-projectA/components/wxs'
        }
      ]
    }
    
  6. 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.jsplugins 中添加 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

工具会做以下两件事情:

  1. 创建 config/build.export.js 文件,并将 config/config-projectA/index.js 导出;

    const buildConfig = require('./config-projectA/index')
    module.exports = buildConfig
    
  2. config/config-projectA/project.config.json 复制到根目录。

我们只需要在 config/index.js 的最后 merge build.export.js,随后在根目录执行 Taro 编译指令

如何添加定制化页面

也许在未来的有一天,我们接到一个需求,需要为 小程序A 添加一个定制化的页面。我们将这个页面路径添加到 app.jsxconfig,但又不希望其他小程序打包的时候把这个页面也打包进去。

一开始我使用的方法简单粗暴:在打包其他小程序的时候把这个页面路径注释起来,在打包 小程序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 就可以添加定制化页面,不仅避免被不需要的项目打包,也能清楚地看出哪些项目有定制化页面哪些没有。对于 subpackagestabBar.list 也可以做同样的处理。

最后

这个工具到目前为止是根据公司的业务需求开发的,主要功能也并不多,还是有挺大的局限。我也还在探索如何更方便地打包为不同项目编写的定制化页面,所以这个工具还会继续更新下去。

最后一次编辑于  2020-11-18  
点赞 0
收藏
评论
登录 后发表内容