##Foreword I have published "Related learning recommendations:WeChat Mini Program Tutorial
Practical Chapter - Exploration of WeChat Mini Program Engineering - Webpack" was the first stage of my exploration of WeChat Mini Program engineering. At first, I just wanted to verify whether the WeChat applet and webpack could be combined (largely driven by curiosity about technology), and I didn’t think too much about engineering continuous delivery. However, under the constant impact of internal needs, I began to have the idea of continuously simplifying the difficulty of developing WeChat mini programs through engineering methods. The final product was this rapid development solution for WeChat mini programs named after Medusa.Next, I will share the practical process of achieving this solution in more detail. The tools mentioned below have also been published on npm for everyone to download and use. This article will cover all the content of the previously published article and will be richer in content, so it will be longer. Please read it patiently.
|-- 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复制代码
.less
.scss
/** 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' } ]) ] };复制代码
SOURCEand
DESTINATIONConstants, they respectively represent the absolute paths of the source code directory and the target code directory. We extract them in separate dictionary files:
/** 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];复制代码
entry,plugin(plugin)andloaderof webpack. Only through cooperation can the moving work be completed.
Core Entry Function First of all, we have to solve the problem of how to generate the entrance. Only after solving the problem of entrance generation can we use the loader to complete the conversion of the file content. For the problem of entry generation, I developed another plug-in, entry-extract-webpack-plugin, to solve it. I do not intend to explain the implementation process of this plug-in in detail. I will only elaborate on its core implementation ideas (if you are interested in learning more, you can download it and look at the source code directly).WeChat applet needs to establish an entrance network according to the rules. The main package and sub-package will be configured in the app.json file, and the components required for the page will also be configured in the [page].json file. Grasping this feature, we can list the core of plug-in functions as follows:
and# provided by
node.js##fs
The module function, based on the path configured in the app.json file, recursively searches for the component paths that each page depends on, and finally integrates them into the same array.
Use the SingleEntryPlugin and MultiEntryPlugin plug-ins provided by webpack to inject the path array collected in the first step into the build in the entryOption life cycle hook to form the. In addition to the core implementation ideas, I also want to briefly explain how we write a webpack plug-in. :
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'] }) ]);复制代码
以上就是基础的 webpack 策略,接下来我们书写一个工具包的可执行文件便可以在项目当中通过medusa-server {mode}
使用 webpack 提供的功能了。我们需要在工具包的 package.json 文件中配置 bin 字段,它标志了我们通过命令会自动执行哪个文件。
{ ... main: "index.js", bin: { "medusa-server": "index.js" } }复制代码
/** index.js */const webpack = require('webpack');const { merge } = require('webpack-merge');const chalk = require('chalk');const commonConfig = require('./config/webpack.common');const { NODE_ENV } = require('./libs/dicts');const config = (function(mode) { if (mode === 'production') { return merge([commonConfig, { mode }]); } return merge([commonConfig, { mode: 'development', watch: true, watchOptions: { ignored: /node_modules/ } }]) })(NODE_ENV); webpack(config, (err, stats) => { if (err) { console.log(chalk.red(err.stack || err)); if (err.details) { console.log(chalk.red(err.details)); } return undefined; } const info = stats.toJson(); if (stats.hasErrors()) { console.log(chalk.red(info.errors)); } if (stats.hasWarnings()) { console.log(chalk.yellow(info.warnings)); } });复制代码
基础篇当中完成的 webpack 配置已经可以满足两点功能,一是对常规文件的输出,二是具备开发与生产两种不同的模式。对于基础篇我还有两点要说明一下:
为什么没有应用ES6(更改版本)转为ES5的相关插件?
因为在实践当中发现IOS的10.x版本存在async/await语法无法正常使用的情况,所以索性就让构建更加纯粹一些 ,然后启用微信开发者工具的ES6 转 ES5与增强编译这两项功能,由官方工具去处理新特性。
为什么 devtool 要设置为 none,难道不需要 sourceMap 吗?
微信官方已经原生提供了SourceMap功能,这在你上传版本时开发者工具中就已经有体现了。
接下来就进入进阶篇的梳理,在满足正常的输出后其实与原生开发好像并没有太大差异,这完全体现不出 webpack 的作用,所以我们要利用 webpack 的能力及其相关的工具生态来扩展下 Medusa 的功能。接下来我们将赋予 Medusa 以下几点功能:
原生微信小程序只支持相对路径的引入方式,但是我们难免会遇到必须移动某些文件的情况,假设这个文件在多处被引用,那就头疼了。所以我们通过 webpack 的能力以及搭配 jsconfig.json 配置文件可以让我们有更好的开发体验。
/** config/webpack.common.js */const config = { ... resolve: { alias: { '@': SOURCE } } };复制代码
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"], }, "target": "ES6", "module": "commonjs", "allowSyntheticDefaultImports": true, }, "include": ["src/**/*"], "exclude": ["node_modules"], }复制代码
通过 alias 别名配置,@ 符号就可以指代 src 源代码目录,然后在实际业务项目的根目录下创建一个 jsconfig.json 文件,通过以上的配置在使用 vscode 编辑器时就可以获得良好的智能路径提示功能。
在介绍这个功能之前,我需要先讲解一下 config.yaml 这个配置文件的作用。这是我一开始设想用来扩展各种功能的配置文件,它应用在实际业务项目的根目录中。现阶段它的功能还相当少,不过将来应该会逐步迭代增加的。
# 当前项目应用平台platform: wx# 样式单位 px 转换 rpx 的比例设定css_unit_ratio: 1# 域名配置development_host: api: https://www.miniprogram.dev.comproduction_host: api: https://www.miniprogram.pro.com复制代码
这其中的域名配置是可以自由增减的,书写形式形如例子当中所示。当我们在其中配置好对应环境的域名后,我们在业务代码当中便可以用mc.$hosts.api
变量轻松的访问域名。在构建过程中工具会自动帮你将对应环境的域名注入代码当中,你再也不用关心如何去管理和切换不同环境域名的问题了。下面展示一下我是如何实现这一功能的:
/** libs/dicts */exports.CONFIG = path.resolve(this.ROOT, 'config.yaml'); exports.DEFAULT_CONFIG = { platform: 'wx', css_unit_ratio: 1, };复制代码
/** libs/index.js */const fs = require('fs');const yaml = require('js-yaml');const { CONFIG, DEFAULT_CONFIG } = require('./dicts'); exports.yamlConfig = (function() { try { /** 将 yaml 格式的内容解析为 object 对象 */ const config = yaml.load(fs.readFileSync(CONFIG, { encoding: 'utf-8' })); return config; } catch(e) { return DEFAULT_CONFIG; } })();复制代码
/** webpack.common.js */const webpack = require('webpack');const { yamlConfig } = require('../libs');const { NODE_ENV } = require('../libs/dicts');const config = { ... plugins: [ ... new webpack.DefinePlugin({ mc: JSON.stringify({ $hosts: yamlConfig[`${NODE_ENV}_host`] || {} }) }) ] };复制代码
webpack.DefinePlugin
插件可以判别代码中 mc 这个标识符然后进行相应的替换功能,这样就能保证代码的正常运作。
编码规范已经是老生常谈的话题了,今年我所处的前端团队扩容到二十几人,在多人共同协作和不同成员维护的情况下,我们迫切的需要统一的编码规范来减少维护的成本。(在规范落地上,我还有两句题外话要说,当我们需要落地一项规范的时候,不要停留在讨论规则上,也不要妄想用人的自律去约束编码。如果你的团队也需要实施这些东西,希望你能够成为先驱者将规则先定下初版,然后找一个合适的工具,通过工具去约束人的行为。)接下来我们就来看下我们所需要的检查工具是如何配置的:
/** webpack.common.js */const StylelintPlugin = require('stylelint-webpack-plugin');const config = { module: { rules: [ { enforce: 'pre', test: /\.js$/, exclude: /node_modules/, loader: 'eslint-loader', options: { fix: true, cache: false, formatter: require('eslint-friendly-formatter') } } ] }, plugins: [ new StylelintPlugin({ fix: true, files: '**/*.(sa|sc|le|wx|c)ss' }) ] };复制代码
以上配置的两个工具可以对 js 与样式文件按照一定的规则集进行检查,对于部分不符合规范的代码可以自行修复,无法自动修复的部分将会给出相应的提示。应用什么规则集你可以通过业务项目根目录中的.eslintrc
和.stylelintrc
文件去决定(下面的章节我会对规范这一块再进行相应的展开)。
在应用 webpack 之后,我们为微信小程序开发赋予了 npm 加载依赖的能力。在模块化打包的机制下,工具包会被加载进引用它的入口当中,这会导致微信小程序包大小及其容易就超出限定值了,所以我们的解决方案是将多次出现的代码抽离为单独的公共部分,具体的实施代码如下:
/** config/webpack.common.js */const config = { ... output: { ..., globalObject: 'global' }, plugins: [ ... new webpack.BannerPlugin({ raw: true, include: 'app.js', banner: 'const vendors = require("./vendors");\nconst commons = require("./commons");\nconst manifest = require("./manifest");' }) ], optimization: { cacheGroups: { vendors: { chunks: 'initial', name: 'vendors', test: /[\\/]node_modules[\\/]/, minChunks: 3, priority: 20 }, commons: { chunks: 'initial', name: 'commons', test: /[\\/](utils|libs|services|apis|models|actions|layouts)[\\/]/, minChunks: 3, priority: 10 } }, runtimeChunk: { name: 'manifest' } } };复制代码
微信小程序的全局变量可以通过 global 访问,所以我们需要将output.globalObject
属性设置为 global。webpack 内置的 BannerPlugin 插件可以将我们需要的语句插入在指定的文件的头部,利用它我们就做到了将抽离出来的公共文件重新引入到依赖网格中。
我们在使用 Vue 、React 的脚手架创建的项目时会见到.env
这个文件,它的主要作用是扩展开发者所需的环境变量。在微信小程序扩展环境变量变量这样的需求可能少之又少,但是我们可以换一种思路,我们可以利用它来扩展开发者所需的全局变量。接下来我们将利用 dotenv-webpack 这一工具来实现这个功能,它可以读取业务项目根目录下的.env
文件内容,使得我们在编码中也可以使用当中的变量值。
/** config/webpack.common.js */const Dotenv = require('dotenv-webpack');const { ENV_CONFIG } = require('../libs/dicts');const config = { ... plugins: [ ... new Dotenv({ path: ENV_CONFIG }) ] };复制代码
通过 webpack 我已经实现了通用的许多功能,但是我所处的公司开发的微信小程序颇多,所以难免有一些项目需要个性化的定制策略。既然有这样的需求,我们就应该提供这样的能力给到业务开发者,一来我们可以从多个项目当中吸收更多需要集成的功能点,二来可以暂时减轻自身的负担。webpack-merge 这一工具其实在前面样式部分合并我已经使用过了,我们只需要提供业务项目 webpack 配置的路径即可,再修改下之前的执行文件。
/** index.js */const { WEBPACK_CONFIG } = require('./libs/dicts');const config = (function(mode) { if (mode === 'production') { return merge([commonConfig, { mode }, WEBPACK_CONFIG]); } return merge([commonConfig, { mode: 'development', watch: true, watchOptions: { ignored: /node_modules/ } }, WEBPACK_CONFIG]) })(NODE_ENV); ...复制代码
微信官方提供了关于路由跳转的 API ,但我认为官方的 API 在日常开发中有几点不便:
为了解决上述的三个问题,我将 API 进行了二次封装,从中抹除四种跳转类型的差异,通过统一的接口就可以达到四种跳转方法的效果。并且通过 webpack 的全局变量注入功能优化了路径字符串的获取,方便使用并且容易维护。
mc.$routes
我将页面的文件夹名称与路径相关联,两者形成映射关系的话,我们只需要书写文件夹名称便可。原来我们需要使用pages/home/index
访问 home 页面,通过我的改造之后,我们可以通过mc.$routes.home
访问 home 页面。
medusa-wx-router
medusa-wx-router 是我对路由功能进行二次封装后的工具包,具体的实现过程在这里我就不详述了,你可以自行下载使用或是依照你的需求进行再次改造,下面我只展示一下在业务代码中结合mc.$routes
如何使用:
mc.routerTo({ url: mc.$routes.home, type: 'push', query: { id: 0, bool: true }, success: () => console.log('successfully') });/** push 方式快捷形式 */mc.routerTo(mc.$routes.home, { id: 0, bool: true });复制代码
为了省去 import 路由工具包这一步骤,我使用了 webpack.ProvidePlugin 这一插件自动帮我们在有使用的地方补充 import 功能。
/** config/webpack.common.js */const config = { ... plugins: [ ... new webpack.ProvidePlugin({ 'mc.routerTo': ['medusa-wx-router', 'routerTo'], 'mc.decoding': ['medusa-wx-router', 'decoding'], 'mc.back': ['medusa-wx-router', 'back'], 'mc.goHome': ['medusa-wx-router', 'goHome'], }) ] };复制代码
规范是工程的重要一环,一个团队必须遵照同一套规则进行编码,规范的存在使得代码的质量得以提升,有统一的规范认知使得成员互相交接项目更加轻松高效。在前面的实践当中,我已经将规范的检查工具集成在了构建流程当中,本节我将补充一下应用于微信小程序的规则集配置和相关编辑器插件,当然我还希望你阅读一下我对于命名规范的一些总结希望对你有所启发《你可能需要的统一命名规范》。
规则集其实就是一个包含规则的配置文件,接下来我会给出具体的配置内容。当然在考虑到规则集的团队定制性和升级的问题,我将 ESLint 和 StyleLint 的规则都制作成了 npm 包,这就解决了所有业务项目统一规则的问题。对应的 npm 包分别是 eslint-config-medusa 和 stylelint-config-medusa ,这是我所处的团队所需要的,所以对于你们在实践时可以结合你们团队的现有情况进行改造。
# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true [*.md] trim_trailing_whitespace = false复制代码
在业务项目的根目录中创建 .editorconfig 文件配合 vscode 的 EditorConfig for VS Code 插件便可以对常规的文件进行基础的编码约束。
module.exports = { parserOptions: { ecmaVersion: "2018", sourceType: "module" }, parser: "babel-eslint", env: { node: true, commonjs: true, es6: true }, globals: { mc: false, wx: true, swan: true, App: true, Page: true, Component: true, Behavior: true, getApp: true, getCurrentPages: true }, extends: ["airbnb-base"], rules: { // disallow use of console "no-console": "warn", // disallow use of debugger "no-debugger": "error", // disallow dangling underscores in identifiers 'no-underscore-dangle': 'off', // specify the maximum length of a line in your program "max-len": ["error", 120], // require return statements to either always or never specify values "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], // disallow usage of expressions in statement position "no-unused-expressions": "off" }, settings: { "import/resolver": { alias: { map: [ ["@", "./src"], ["utils", "./src/utils"], ["services", "./src/services"] ], extensions: [".js", ".json", ".less"] } } } };复制代码
module.exports = { rules: { // Disallow unknown type selectors 'selector-type-no-unknown': null, // Disallow unknown units 'unit-no-unknown': null }, extends: [ 'stylelint-config-standard', 'stylelint-config-recess-order' ], plugins: ['stylelint-order'] };复制代码
上面展示的就是我所完成的两个规则集依赖包的入口文件,当我们 install 依赖包之后,我们就可以在业务项目的 .eslintrc 和 .stylelintrc 文件中通过 extends 字段将它们引入。搭配 vscode 编辑器的 ESLint 和 stylelint-plus 插件,它们可以在编码过程中就提醒你相应的错误规则。
前面说的构建与规范全都是服务于具体的业务项目的,在拥有了基础的能力之后,我们就该思考如何使得业务开发人员能够快速并且符合要求的进行业务系统开发。让他们不再需要考虑目录应该怎么约定,工具如何集成,编码规范究竟如何应用诸如这些问题。为了达成快速开发这一要求,我着手制作具备初始化项目这一简易功能的脚手架。最终我将这一工具分为两个项目,其一是具备初始化项目结构、下载相关依赖包功能的 @chirono/medusa-cli,另外是约定好项目结构与必要配置文件的 miniprogram-base 。
业务开发者只需要通过 npm 全局安装 @chirono/medusa-cli 工具,便可以通过medusa create
命令初始化一个工程项目,该工具还会提示必要的项目信息让开发者输入,用于完善业务项目的 package.json 文件。
以上是我对 Medusa 这一工程的总结,现在这一工程已经进入较为稳定的阶段也已经顺利投入到多个实际项目当中。对于上述的总结可能有某些部分写得相对简略,如果你却有兴趣我建议你直接下载源码去研究,因为编码的实际操作确实无法用三言两语解释清楚。我写下这一篇文章主要想表达的是工程链路的完整性能够为实际的项目开发产生相当大的效益,而且实践工程当中不仅是单一工具的开发,要将工具串起来。接下来我可能还会对UI打包、TS编译、测试工具这些内容进行知识总结。最后,感谢你的阅读,如果你有疑问或是建议可以留言与我共同探讨。
想了解更多编程学习,敬请关注php培训栏目!
The above is the detailed content of Detailed explanation of Medusa WeChat mini program engineering practice plan. For more information, please follow other related articles on the PHP Chinese website!