小程序打包插件开发经验
相信自己已经开发了插件的学生已经阅读了编写插件或类似的文章。因为工具小程序-webpack-loader是在webpack 4发布时开发的,所以他们读了这篇文章,顺便读了下面的文档。
如果你读过这份文件,我相信你一定知道:
每个插件都必须有一个apply方法,以便webpack引擎执行您想要执行的代码。有两个重要的对象,编译器和编译,您可以在它们上面绑定事件钩子(当webpack执行此步骤时调用),以及哪些事件钩子可以读取编译器钩子。模块和组块的关系,我们可以理解为每个文件都会有一个模块,而一个组块是由多个模块组成的。webpack整个包装过程中有哪些事件?如何编写一个简单的加载器?如果你觉得启动不了,可以继续看看我是如何一步一步开发完善迷你程序-webpack-loader来打包小程序的。
小程序有一个固定的程序。首先,它需要一个app.json文件来定义所有的页面路径,然后每个页面由四个文件组成:js,json,wxml,wxss。因此,我使用app.json作为webpack条目。当webpack执行插件的应用时,我通过获取条目知道applet有哪些页面。过程大概如下图,一个小程序打包插件差不多就是这样完成的。
有两个插件MultiEntryPlugin,SingleEntryPlugin。你为什么要这么做?因为webpack会根据你的条目配置来决定生成文件的数量(这里,条目不仅仅是webpack配置中的条目,import(),require . success()都会生成一个条目),我们不想把所有页面的js打包到一个文件中,而是需要使用SingleEntryPlugin来生成一个新的条目模块;对于那些静态资源,我们可以使用MultiEntryPlugin插件,将这些文件作为一个入口模块的依赖项,并在加载器中配置文件加载器来输出静态文件。伪代码如下:
const multi entryplugin=require(' web pack/lib/multi entryplugin ');const singletentryplugin=require(' web pack/lib/singletentryplugin ');class MiniPlugin { apply(编译器){ let options=compiler . options let context=compiler . rootcontext let entry=options . entry let files=load files(entry)let scripts=files . filter(file=/\。js$/。测试(文件))让assets=files.filter(文件=!/\.js$/。测试(文件))新的MultiEntryPlugin(上下文,资产,' __assets__ ')。apply(编译器)scripts . foreach((file={ let fileName=relative(上下文,文件))。替换(extname(file),' ');新的SingleEntryPlugin(上下文、文件、文件名)。apply(编译器);} }}}复制代码。当然,如果你按照上面的方法做,你会发现会多一个main.js,xxx.js(使用MultiEntryPlugin时填写的名称),其中main.js对应的是配置条目生成的文件,xxx.js是MultiEntryPlugin生成的。这些文件不是我们需要的,所以我们需要扔掉它们。如果你熟悉webpack文档,有很多地方可以修改最终打包的文件,比如编译器的emit事件和编译的optimizeChunks相关事件。那
本质上就是去修改 compilation.assets 对象。在 mini-program-webpack-loader 中就使用了 emit 事件来处理这种不需要输出的内容。大概流程就像下面这样:
小程序打包当然没这么简单,还得支持wxml、wxss、wxs和自定义组件的引用,所以这个时候就需要一个 loader 来完成了,loader 需要做的事情也非常简单 —— 解析依赖的文件,如 .wxml 需要解析 import 组件的 src,wxs 的 src,.wxss 需要解析 @import,wxs 的 require,最后在 loader 中使用 loadModule 方法添加即可。自定义组件一开始在 add entry 步骤的时候直接获取了,所以不需要 loader 来完成。这个时候的图:
这样做也没什么问题,可是开发体验是比较差的,如再添加一个自定义组件,一个页面,webpack 是无感知的,所以需要在页面中的 .json 发生改变时检查是不是新增了自定义组件或者新增了页面。这个时候遇到一个问题,自定义组件的 js 是不能通过 addModule 的方式来添加的,因为自定义组件的 js 必须作为独立的入口文件。在 loader 中是做不了,所以尝试把文件传到 plugin 中(因为 plugin 先于 loader 执行,所以是可以建立 loader 和 plugin 通信的)。简单粗暴的方式:
// loader.jsclass MiniLoader {}module.exports = function (content) { new MiniLoader(this, content)}module.exports.$applyPluginInstance = function (plugin) { MiniLoader.prototype.$plugin = plugin}// plugin.jsconst 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 来支持单文件的时候,好像没那么方便:
- 单文件拆分为四个文件后,可以使用 emitFile 和 addDependency 来创建文件,但是创建的文件不会执行 loader
- 使用 loadModule 会因为文件系统不存在该文件会报错
版权声明:小程序打包插件开发经验是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。