详解Medusa 微信小程序工程化实践方案
前言
我曾发布过《实战篇--微信小程序工程化探索之webpack》一文,当时是我探索微信小程序工程化的第一阶段。起初我只是为了验证微信小程序与 webpack 是否能够相结合(很大程度是被对于技术的好奇心驱使),对于工程化的持续交付并没有过多的思考。但是在内部需求的不断冲击下,我开始萌生以工程化手段持续简化微信小程序开发难度的想法,最终衍生的产物就是这套以 Medusa 命名的微信小程序快速开发方案。
接下来我将较为详细的分享达成这一方案的实践过程,下文中将提到的工具我也已经发布在 npm 上供大家下载使用。这篇文章将会覆盖之前发表的那篇文章的全部内容并且内容更加丰富,所以篇幅方面也较为长请读者们耐心阅读。
webpack-build-miniprogram
webpack-build-miniprogram 是 Medusa 方案的基础也是核心,这一工具包提供了以 webpack 构建微信小程序的能力,并且我们可以利用 webpack 的生态持续丰富 Medusa 的功能。在讲述基础构建配置之前,我们先来看看 Medusa 的目录结构基础,有了相应的目录约束才使得项目更加规范化。
|-- dist 编译结果目录|-- src 源代码目录| |-- app.js 项目入口文件| |-- app.json 小程序配置文件| |-- sitemap.json sitemap配置文件| |-- assets 静态资源存放目录| | |-- .gitkeep| |-- components 公共组件存放目录| | |-- .gitkeep| |-- dicts 公共字典存放目录| | |-- .gitkeep| |-- libs 第三方工具库存放目录(外部引入)| | |-- .gitkeep| |-- pages 页面文件存放目录| | |-- index| | |-- index.js| | |-- index.json| | |-- index.less| | |-- index.wxml| |-- scripts 公共脚本存放目录(wxs)| | |-- .gitkeep| |-- services API服务存放目录| | |-- .gitkeep| |-- styles| | |-- index.less 项目总通用样式| | |-- theme.less 项目主题样式| |-- templates 公共模板存放目录| | |-- .gitkeep| |-- utils 公共封装函数存放目录(自我封装)| |-- .gitkeep|-- .env 环境变量配置文件|-- config.yaml 编译配置文件|-- webpack.config.js webpack 配置扩展文件|-- project.config.json 开发者工具配置文件└── package.json复制代码
基础篇
webpack 这一工具现在已经成为前端工程师的必备技能,复杂的工作原理让我们对它总是有种敬畏感,所以在做微信小程序构建策略过程中,我们先将它简单的理解为一个“搬运工具”。它将源代码目录中的文件加以某些处理之后再输出到目标目录中。现在我们明确一下我们要搬运哪些文件,微信小程序中涉及到的主要有:
基础搬运功能
接下来我们将书写 webpack 的公共部分配置,利用 copy-webpack-plugin 这一插件来完成大部分文件的搬运工作。
/** config/webpack.common.js */const CopyPlugin = require("copy-webpack-plugin");const config = { context: SOURCE, devtool: 'none', entry: { app: './app.js' }, output: { filename: '[name].js', path: DESTINATION }, plugins: [ new CopyPlugin([ { from: 'assets/', to: 'assets/', toType: 'dir' }, { from: '**/*.wxml', toType: 'dir' }, { from: '**/*.wxss', toType: 'dir' }, { from: '**/*.json', toType: 'dir' }, { from: '**/*.wxs', toType: 'dir' } ]) ]};复制代码
以上简单的配置我们就实现了除逻辑文件与预编译语言文件以外的搬运工作,在配置中出现了 SOURCE 、 DESTINATION 两个常量,它们分别代表的是源代码目录与目标代码目录的绝对路径,我们将它们抽离在单独的字典文件中:
/** libs/dicts.js */const path = require("path");exports.ROOT = process.cwd();exports.SOURCE = path.resolve(this.ROOT, 'src');exports.DESTINATION = path.resolve(this.ROOT, 'dist');exports.NODE_ENV = process.argv.splice(2, 1)[0];复制代码
上面搬运的文件因为不需要特殊的内容处理,所以完全交由插件去实现,剩余两种类型的文件我们就需要使用到 webpack 的入口(entry)、插件(plugin) 和 loader 协同合作才能完成搬运工作。
核心入口功能
首先我们要解决如何生成入口的问题,解决了入口生成的问题才能借助 loader 去完成文件内容的转化。对于入口生成这一问题,我开发了另外一个插件 entry-extract-webpack-plugin 去解决。这一插件我并不打算详细的讲解实现的过程,我只会阐述它的核心实现思路(如果你有兴趣进一步了解可以下载下来直接看源码)。
微信小程序需要建立入口网络其实是有规律可循的,主包、分包都会配置在 app.json 文件中,页面所需要的组件也会配置在 [page].json 文件中。抓住这一特点,我们可以将实现插件功能的核心罗列为以下几点:
以上三点是实现生成入口这一功能的核心思路,除了核心的实现思路外,我还想简单的讲解下我们如何去写一个 webpack 插件:
class EntryExtractPlugin { constructor(options) {} apply(compiler) { compiler.hooks.entryOption.tap('EntryExtractPlugin', () => { ... }); compiler.hooks.watchRun.tap('EntryExtractPlugin', () => { ... }); }}复制代码
webpack 的插件大致是以类的形式存在,当你使用插件时,它会自动执行 apply 方法, 然后使用 compiler.hooks 对象上的各种生命周期属性便可以将我们需要的处理逻辑植入到 webpack 的构建流程当中。
逻辑与样式
上面解决了生成入口(entry)的问题,接下来我们在原有的基础上完善一下策略。由于预编译语言的类型较多,我为了策略的可扩展性将样式部分的策略抽离为单独的部件,然后在通过 webpack-merge 这一工具将它们合并起来,完整的实现如下:
/** config/webpack.parts.js */exports.loadCSS = ({ reg = /.css$/, include, exclude, use = [] }) => ({ module: { rules: [ { include, exclude, test: reg, use: [ { loader: require('mini-css-extract-plugin').loader }, { loader: 'css-loader' } ].concat(use) } ] }});复制代码
/** config/webpack.common.js */const { merge } = require('webpack-merge');const MiniCssExtractPlugin = require('mini-css-extract-plugin');const parts = require('./webpack.parts.js');const config = { ... module: { rules: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/ } ] }, plugins: [ ... new MiniCssExtractPlugin({ filename: '[name].wxss' }) ]};module.export = merge([ config, parts.loadCSS({ reg: /.less$/, use: ['less-loader'] })]);复制代码