第一次接触webpack 的第一反应是什么(⊙_⊙)? 为什么这么复杂,感觉这么难实战webpack ,算了! 时间是个好东西。 随着对后端工程的实践和理解逐渐深入,我接触webpack也越来越多,但最终还是被ta吓倒了,忍不住大喊“webpack yyds(永远)!”
去年年中实战webpack ,本想写一些关于webpack的文章,但由于种种原因而被耽搁了(主要是对webpack了解不够,所以不敢自己写); 快过年了,有时间了,不如去“钓鱼”一下Touch webpack,整理一些“年货”分享给需要的xdm吧! 以后会继续写一些[Webpack]系列文章,由xdm监督...
指导
本文主要介绍通过实现一个cdn优化插件CdnPluginInject来进行webpack插件插件开发的具体流程,其中会涉及到html-webpack-plugin插件的使用、vue/cli3+项目中webpack插件的配置以及webpack相关知识的讲解点。 全文约2800+字,预计时长5~10分钟。 希望xdm读完之后能学到一些东西,思考一下,输出一些东西!
注:文章中的例子基于vue/cli3+项目!
1.cdn常规使用
索引.html:
<script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js" > <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js" > ···
vue.config.js:
module.exports = { ··· configureWebpack: { ··· externals: { 'vuex' : 'Vuex' , 'vue-router' : 'VueRouter' , ··· } },
2. 开发webpack插件
webpack官网是这样介绍的:该插件为第三方开发者提供了webpack引擎中完整的能力。 使用分阶段构建反弹,开发人员可以将自己的行为引入到 webpack 构建过程中。 创建插件比创建加载器更中间,因为您需要了解 webpack 的一些底层内部结构才能实现相应的钩子!
插件包括:
// 一个 JavaScript class class MyExampleWebpackPlugin { // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数 apply(compiler) { // 指定要附加到的事件钩子函数 compiler.hooks.emit.tapAsync( 'MyExampleWebpackPlugin' , (compilation, callback) => { console.log('This is an example plugin!' ); console.log('Here’s the `compilation` object which represents a single build of assets:' , compilation); // 使用 webpack 提供的 plugin API 操作构建结果 compilation.addModule(/* ... */); callback(); } ); } }
3、cdn优化插件的实现
想法:
实施步骤:
1.创建一个命名的JavaScript函数(使用ES6类实现)
创建一个类cdnPluginInject,添加该类的构造函数来接收传入的参数; 这里我们定义接收参数的格式如下:
modules:[ { name: "xxx" , //cdn包的名字 var: "xxx" , //cdn引入库在项目中使用时的变量名 path: "http://cdn.url/xxx.js" //cdn的url链接地址 }, ··· ]
定义该类的变量模块,用于接收传入的cdn参数的处理结果:
class CdnPluginInject { constructor({ modules, }) { // 如果是数组,将this.modules变换成对象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey" ]: modules } : modules; } ··· } module.exports = CdnPluginInject;
2.在其原型上定义apply方法
插件由构造函数实例化,其原型对象具有 apply 方法。 当插件安装时,webpack 编译器会调用一次 apply 方法。 apply方法可以接收一个webpack编译器对象的引用,这样就可以在回调函数中访问该编译器对象
cdnPluginInject.js的代码如下:
class CdnPluginInject { constructor({ modules, }) { // 如果是数组,将this.modules变换成对象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey" ]: modules } : modules; } //webpack plugin开发的执行入口apply方法 apply(compiler) { ··· } module.exports = CdnPluginInject;
3.指定一个接触webpack本身的storm hook
这里接触到编译钩子:编译(compilation)创建后,执行插件。
编译是编译器的一个钩子函数。 编译将创建一个新的编译过程实例。 编译实例可以访问所有模块及其依赖项。 获得这样的模块后,就可以根据需要进行操作了!
class CdnPluginInject { constructor({ modules, }) { // 如果是数组,将this.modules变换成对象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey" ]: modules } : modules; } //webpack plugin开发的执行入口apply方法 apply(compiler) { //获取webpack的输出配置对象 const { output } = compiler.options; //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置 output.publicPath = output.publicPath || "/" ; if (output.publicPath.slice(-1) !== "/" ) { output.publicPath += "/" ; } //触发compilation钩子函数 compiler.hooks.compilation.tap("CdnPluginInject" , compilation => { ··· } } module.exports = CdnPluginInject;
4.在钩子风暴中操作index.html
这一步主要是将CDN的script标签插入到index.html中; 如何实现? 虽然webpack在vue项目中打包时使用html-webpack-plugin生成.html文件,但是我们也可以使用html-webpack-plugin来操作html文件,在这里插入cdn脚本标签。
// 4.1 引入html-webpack-plugin依赖 const HtmlWebpackPlugin = require("html-webpack-plugin" ); class CdnPluginInject { constructor({ modules, }) { // 如果是数组,将this.modules变换成对象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey" ]: modules } : modules; } //webpack plugin开发的执行入口apply方法 apply(compiler) { //获取webpack的输出配置对象 const { output } = compiler.options; //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置 output.publicPath = output.publicPath || "/" ; if (output.publicPath.slice(-1) !== "/" ) { output.publicPath += "/" ; } //触发compilation钩子函数 compiler.hooks.compilation.tap("CdnPluginInject" , compilation => { // 4.2 html-webpack-plugin中的hooks函数,当在资源生成之前异步执行 HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration .tapAsync("CdnPluginInject" , (data, callback) => { // 注册异步钩子 //获取插件中的cdnModule属性(此处为undefined,因为没有cdnModule属性) const moduleId = data.plugin.options.cdnModule; // 只要不是false (禁止)就行 if (moduleId !== false ) { // 4.3得到所有的cdn配置项 let modules = this.modules[ moduleId || Reflect.ownKeys(this.modules)[0] ]; if (modules) { // 4.4 整合已有的js引用和cdn引用 data.assets.js = modules .filter(m => !!m.path) .map(m => { return m.path; }) .concat(data.assets.js); // 4.5 整合已有的css引用和cdn引用 data.assets.css = modules .filter(m => !!m.style) .map(m => { return m.style; }) .concat(data.assets.css); } } // 4.6 返回callback函数 callback(null, data); }); } } module.exports = CdnPluginInject;
接下来逐步分析上面的实现:
5、设置webpack externals的外部扩展
在执行apply方法之前,还需要完成一步:将cdn参数配置到外部扩展externals; 可以直接通过compiler.options.externals获取webpack中的externals属性,通过操作配置cdn配置中的数据就可以了。
6. 回调;
返回回调告诉webpack CdnPluginInject插件已经完成;
// 4.1 引入html-webpack-plugin依赖 const HtmlWebpackPlugin = require("html-webpack-plugin" ); class CdnPluginInject { constructor({ modules, }) { // 如果是数组,将this.modules变换成对象形式 this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey" ]: modules } : modules; } //webpack plugin开发的执行入口apply方法 apply(compiler) { //获取webpack的输出配置对象 const { output } = compiler.options; //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置 output.publicPath = output.publicPath || "/" ; if (output.publicPath.slice(-1) !== "/" ) { output.publicPath += "/" ; } //触发compilation钩子函数 compiler.hooks.compilation.tap("CdnPluginInject" , compilation => { // 4.2 html-webpack-plugin中的hooks函数,当在资源生成之前异步执行 HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration .tapAsync("CdnPluginInject" , (data, callback) => { // 注册异步钩子 //获取插件中的cdnModule属性(此处为undefined,因为没有cdnModule属性) const moduleId = data.plugin.options.cdnModule; // 只要不是false (禁止)就行 if (moduleId !== false ) { // 4.3得到所有的cdn配置项 let modules = this.modules[ moduleId || Reflect.ownKeys(this.modules)[0] ]; if (modules) { // 4.4 整合已有的js引用和cdn引用 data.assets.js = modules .filter(m => !!m.path) .map(m => { return m.path; }) .concat(data.assets.js); // 4.5 整合已有的css引用和cdn引用 data.assets.css = modules .filter(m => !!m.style) .map(m => { return m.style; }) .concat(data.assets.css); } } // 4.6 返回callback函数 callback(null, data); }); // 5.1 获取externals const externals = compiler.options.externals || {}; // 5.2 cdn配置数据添加到externals Reflect.ownKeys(this.modules).forEach(key => { const mods = this.modules[key]; mods .forEach(p => { externals[p.name] = p.var || p.name; //var为项目中的使用命名 }); }); // 5.3 externals赋值 compiler.options.externals = externals; //配置externals // 6 返回callback callback(); } } module.exports = CdnPluginInject;
至此,一个完整的webpack插件CdnPluginInject就已经开发完成了! 接下来,尝试一下。
四、cdn优化插件的使用
在vue项目的vue.config.js文件中引入并使用CdnPluginInject:
cdn配置文件CdnConfig.js:
/* * 配置的cdn * @name: 第三方库的名字 * @var:第三方库在项目中的变量名 * @path:第三方库的cdn链接 */ module.exports = [ { name: "moment" , var: "moment" , path: "https://cdn.bootcdn.net/ajax/libs/moment.js/2.27.0/moment.min.js" }, ··· ];
在configureWebpack中配置:
const CdnPluginInject = require("./CdnPluginInject" ); const cdnConfig = require("./CdnConfig" ); module.exports = { ··· configureWebpack: config => { //只有是生产山上线打包才使用cdn配置 if (process.env.NODE.ENV =='production' ){ config.plugins.push( new CdnPluginInject({ modules: CdnConfig }) ) } } ··· }
chainWebpack中的配置:
const CdnPluginInject = require("./CdnPluginInject" ); const cdnConfig = require("./CdnConfig" ); module.exports = { ··· chainWebpack: config => { //只有是生产山上线打包才使用cdn配置 if (process.env.NODE.ENV =='production' ){ config.plugin("cdn" ).use( new CdnPluginInject({ modules: CdnConfig }) ) } } ··· }
通过使用 CdnPluginInject:
五、总结
一些webpack大鳄看完之后肯定有一点疑惑。 这个插件不就是webpack-cdn-plugin的乞丐版吗! CdnPluginInject 只是我在研究 webpack-cdn-plugin 源码的基础上,结合我的项目实际需要修改的扩展版本。 相比于 webpack-cdn-plugin 封装了 cdn 链接的生成,CdnPluginInject 直接进行 cdn 链接配置,选择 CDN 显示配置更加简单。 如果你想了解更多关于xdm的知识,可以查看webpack-cdn-plugin的源码。 经过作者的不断迭代更新,它提供的可配置参数更加丰富,功能更加强大(再次膜拜)。
发表评论