相信开发过插件的同窗,都看过 Writing a Plugin 或相似的文章,由于 mini-program-webpack-loader 这个工具开发时正好 webpack 4 发布了,因此就阅读了这篇文章,顺便看了如下几篇文档。javascript
若是你看过文档,相信你必定知道:java
若是感受无从着手,能够继续看看我是如何一步步开发并完善 mini-program-webpack-loader 来打包小程序的。node
小程序有一个固定的套路,首先须要有一个 app.json 文件来定义全部的页面路径,而后每一个页面有四个文件组成:.js,.json,.wxml,.wxss。因此我以 app.json 做为 webpack entry,当 webpack 执行插件的 apply 的时候,经过获取 entry 来知道小程序都有哪些页面。大概流程像下面一张图,一个小程序打包插件差很少就这样完成了。webpack
这里使用了两个插件 MultiEntryPlugin,SingleEntryPlugin。为何要这样作呢?由于 webpack 会根据你的 entry 配置(这里的 entry 不仅是 webpack 配置里的 entry,import(), require.ensure() 都会生成一个 entry)来决定生成文件的个数,咱们不但愿把全部页面的 js 打包到一个文件,须要使用 SingleEntryPlugin 来生成一个新的 entry module;而那些静态资源,咱们可使用 MultiEntryPlugin 插件来处理,把这些文件做为一个 entry module 的依赖,在 loader 中配置 file-loader 便可把静态文件输出。伪代码以下:git
const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
class MiniPlugin {
apply (compiler) {
let options = compiler.options
let context = compiler.rootContext
let entry = options.entry
let files = loadFiles(entry)
let scripts = files.filter(file => /\.js$/.test(file))
let assets = files.filter(file => !/\.js$/.test(file))
new MultiEntryPlugin(context, assets, '__assets__').apply(compiler)
scripts.forEach((file => {
let fileName = relative(context, file).replace(extname(file), '');
new SingleEntryPlugin(context, file, fileName).apply(compiler);
})
}
}
复制代码
固然,若是像上面那样作,你会发现最后会多出一个 main.js,xxx.js(使用 MultiEntryPlugin 时填的名字),main.js 对应的是配置的 entry 生成的文件,xxx.js 则是 MultiEntryPlugin 生成的。这些文件不是咱们须要的,因此须要去掉他。若是熟悉 webpack 文档,咱们有不少地方能够修改最终打包出来的文件,如 compiler 的 emit 事件,compilation 的 optimizeChunks 相关的事件均可以实现。其本质上就是去修改 compilation.assets 对象。github
在 mini-program-webpack-loader 中就使用了 emit 事件来处理这种不须要输出的内容。大概流程就像下面这样: web
小程序打包固然没这么简单,还得支持wxml、wxss、wxs和自定义组件的引用,因此这个时候就须要一个 loader 来完成了,loader 须要作的事情也很是简单 —— 解析依赖的文件,如 .wxml 须要解析 import 组件的 src,wxs 的 src,.wxss 须要解析 @import,wxs 的 require,最后在 loader 中使用 loadModule 方法添加便可。自定义组件一开始在 add entry 步骤的时候直接获取了,因此不须要 loader 来完成。这个时候的图:npm
这样作也没什么问题,但是开发体验是比较差的,如再添加一个自定义组件,一个页面,webpack 是无感知的,因此须要在页面中的 .json 发生改变时检查是否是新增了自定义组件或者新增了页面。这个时候遇到一个问题,自定义组件的 js 是不能经过 addModule 的方式来添加的,由于自定义组件的 js 必须做为独立的入口文件。在 loader 中是作不了,因此尝试把文件传到 plugin 中(由于 plugin 先于 loader 执行,因此是能够创建 loader 和 plugin 通讯的)。简单粗暴的方式:json
// loader.js
class MiniLoader {}
module.exports = function (content) {
new MiniLoader(this, content)
}
module.exports.$applyPluginInstance = function (plugin) {
MiniLoader.prototype.$plugin = plugin
}
// plugin.js
const loader = require('./loader')
class MiniPlugin {
apply (compiler) {
loader.$applyPluginInstance(this);
}
}
复制代码
可是...。文件是传到 plugin 了,但是再使用 SingleEntryPlugin 时你会发现,没效果。由于在 compiler make 以后 webpack 已经不能感知新的 module 添加了,因此是没有用的,这个时候就须要根据文档猜,怎么样才能让 webpack 感知到新的 module,根据文档中的事件作关键字查询,能够发如今编译完成的时候会调用 compilation needAdditionalPass 事件钩子:小程序
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
复制代码
若是在这个事件钩子返回一个 true 值,则可使 webpack 调用 compiler additionalPass 事件钩子,尝试在这里添加文件,果真是能够的。这个时候的图就成了这样:
固然,小程序打包还有些不一样的地方,好比分包,如何用好 splitchunk,就不在啰嗦了,当你开始之后你会发现有不少的方法来实现想要的效果。
插件开发到这里差很少了,总的来讲,webpack 就是变着花样的回调,当你知道每一个回调该作什么的时候,webpack 用起来就轻松了。明显我不知道,由于在开发过程当中遇到了一些问题。
1.如何在小程序代码中支持 resolve alias,node_modules?
既然是工具,固然须要作更多的事情,有赞的小程序那么复杂,若是支持 resolve alias,node_modules 可使得项目更方便维护,或许你会说这不是 webpack 最基本的功能吗,不是的,咱们固然是但愿能够在任何文件中使用 alias,node_modules 支持的不只仅是 js。固然这样作就意味着事情将变得复杂,首先就是获取文件路径,必须是异步的,由于在 webpack 4 中 resolve 再也不支持 sync。其次就是小程序的目录名不能是 node_modules,这时就须要一种计算相对路径的规则,仍是相对打包输出的,而不是相对当前项目目录。
2.多个小程序项目的合并
有赞从小程序来说,有微商城版,有零售版,以及公共版,其中大多基础功能,业务都是相同的,固然不能再每一个小程序在开发一次,因此这个工具具有合并多个小程序固然是必须的。这样的合并稍微又要比从 node_modules 中取文件复杂一些,由于须要保证多个小程序合并后的页面是正确的,并且要保证路径不变。
这两个问题的最终的解决方案既是以 webpack rootContext 的 src 目录为基准目录,以该目录所在路径计算打包文件的绝对路径,而后根据入口文件的 app.json 所在目录的路径计算出最终输出路径。
exports.getDistPath = (compilerContext, entryContexts) => {
/** * webpack 以 config 所在目录的 src 为打包入口 * 因此能够根据该目录追溯源文件地址 */
return (path) => {
let fullPath = compilerContext
let npmReg = /node_modules/g
let pDirReg = /^[_|\.\.]\//g
if (isAbsolute(path)) {
fullPath = path
} else {
// 相对路径:webpack 最后生成的路径,打包入口外的文件都以 '_' 表示上级目录
while (pDirReg.test(path)) {
path = path.substr(pDirReg.lastIndex)
fullPath = join(fullPath, '../')
}
if (fullPath !== compilerContext) {
fullPath = join(fullPath, path)
}
}
// 根据 entry 中定义的 json 文件目录获取打包后所在目录,若是不能获取就返回原路径
let contextReg = new RegExp(entryContexts.join('|'), 'g')
if (fullPath !== compilerContext && contextReg.exec(fullPath)) {
path = fullPath.substr(contextReg.lastIndex + 1)
console.assert(!npmReg.test(path), `文件${path}路径错误:不该该还包含 node_modules`)
}
/** * 若是有 node_modules 字符串,则去模块名称 * 若是 app.json 在 node_modules 中,那 path 不该该包含 node_modules */
if (npmReg.test(path)) {
path = path.substr(npmReg.lastIndex + 1)
}
return path
}
}
复制代码
3.如何把子包单独依赖的内容打包到子包内
解决这个问题的方法是经过 optimizeChunks 事件,在每一个 chunk 的依赖的 module 中添加这个 chunk 的入口文件,而后在 splitChunk 的 test 配置中检查 module 被依赖的数量。若是只有一个,而且是被子包依赖,则打包到子包内。
4.webpack 支持单文件失败
这是一个未解决的问题,当尝试使用 webpack 来支持单文件的时候,好像没那么方便:
最后固然是介绍 mini-program-webpack-loader 能够作什么了。
该工具主要解决如下问题:
重复一遍
最后的最后留下文档地址:mini-program-webpack-loader