本文最后更新于:2023年3月19日 晚上
本文转自:https://www.jianshu.com/p/0fc6bb85ef5b 、https://juejin.im/post/6844903689442820110
webpack 中 loader 和 plugin 的区别 一、webpack 的常见配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const webpack = require("webpack" ); const path = require("path" ); const HtmlWebpackPlugin = require("html-webpack-plugin" ); module.exports = { entry: { app: path.join(__dirname, "../src/js/index.js" ) }, output: { filename: "[name].bundle.js" , path: path.resolve(__dirname, "dist" ), publicPath: "/" }, module: { rules: [ { test: /\.scss/, use: [ "style-loader" , "css-loader" ] } ...... ] }, plugins: [ new HtmlWebpackPlugin({ title: "首页" , filename: "index.html" , template: path.resolve(__dirname, "../src/index.html" ) }) ...... ] }
二、webpack 的打包原理
识别入口文件
通过逐层识别模块依赖(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析,来获取代码的依赖)
webpack 做的就是分析代码,转换代码,编译代码,输出代码
最终形成打包后的代码
三、什么是 loader loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
处理一个文件可以使用多个 loader,loader 的执行顺序和配置中的顺序是相反的,即最后一个 loader 最先执行,第一个 loader 最后执行
第一个执行的 loader 接收源文件内容作为参数,其它 loader 接收前一个执行的 loader 的返回值作为参数,最后执行的 loader 会返回此模块的 JavaScript 源码
四、什么是 plugin 在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MyPlugin { constructor (options ) { console .log("MyPlugin constructor:" , options); } apply (compiler ) { compiler.plugin("compilation" , compilation => { console .log("MyPlugin" ); }); } }module .exports = MyPlugin; webpack.config.js配置:module .exports = { ... plugins : [ new MyPlugin({param : "my plugin" }) ] }
使用该 plugin 后,执行的顺序:
webpack 启动后,在读取配置的过程中会执行 new MyPlugin(options)初始化一个 MyPlugin 获取其实例
在初始化 compiler 对象后,就会通过 compiler.plugin(事件名称,回调函数)监听到 webpack 广播出来的事件
并且可以通过 compiler 对象去操作 webpack
五、loader 和 plugin 的区别 对于 loader,它是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 转换为 A.css,单纯的文件转换过程 plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务
webpack loader 和 plugin 编写 1 基础回顾 首先我们先回顾一下 webpack 常见配置,因为后面会用到,所以简单介绍一下。
1.1 webpack 常见配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 entry : { app : './src/js/index.js' , }, output : { filename : '[name].bundle.js' , path : path.resolve(__dirname, 'dist' ), publicPath : '/' }, devtool : 'inline-source-map' , devServer : { contentBase : './dist' , hot : true }, plugins : [ new CleanWebpackPlugin(['dist' ]), new HtmlWebpackPlugin({ title : 'Output Management' }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ], mode : "development" , module : { rules : [ { test : /\.css$/ , use: [ 'style-loader' , 'css-loader' ] }, { test : /\.(png|svg|jpg|gif)$/ , use: [ 'file-loader' ] } ] } 复制代码
这里面我们重点关注 module 和 plugins 属性,因为今天的重点是编写 loader 和 plugin,需要配置这两个属性。
1.2 打包原理
识别入口文件
通过逐层识别模块依赖。(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析。来获取代码的依赖)
webpack 做的就是分析代码。转换代码,编译代码,输出代码
最终形成打包后的代码
这些都是 webpack 的一些基础知识,对于理解 webpack 的工作机制很有帮助。
2 loader OK 今天第一个主角登场
2.1 什么是 loader? loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
处理一个文件可以使用多个 loader,loader 的执行顺序是和本身的顺序是相反的,即最后一个 loader 最先执行,第一个 loader 最后执行。
第一个执行的 loader 接收源文件内容作为参数,其他 loader 接收前一个执行的 loader 的返回值作为参数。最后执行的 loader 会返回此模块的 JavaScript 源码
2.2 手写一个 loader 需求:
处理.txt 文件
对字符串做反转操作
首字母大写
例如:abcdefg 转换后为 Gfedcba
OK,我们开始 1)首先创建两个 loader(这里以本地 loader 为例)
为什么要创建两个 laoder?理由后面会介绍
reverse-loader.js
1 2 3 4 5 6 7 8 9 module .exports = function (src) { if (src) { console.log ('--- reverse-loader input:' , src) src = src.split('' ).reverse ().join('' ) console.log ('--- reverse-loader output:' , src) } return src; } 复制代码
uppercase-loader.js
1 2 3 4 5 6 7 8 9 10 11 module .exports = function (src) { if (src) { console .log('--- uppercase-loader input:' , src) src = src.charAt(0 ).toUpperCase() + src.slice(1 ) console .log('--- uppercase-loader output:' , src) } // 这里为什么要这么写?因为直接返回转换后的字符串会报语法错误, // 这么写import 后转换成可以使用的字符串 return `module .exports = '${src}' ` } 复制代码
看,loader 结构是不是很简单,接收一个参数,并且 return 一个内容就 ok 了。 然后创建一个 txt 文件 2)mytest.txt
3)现在开始配置 webpack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 module.exports = { entry: { index: './src/js/index.js' }, plugins: [...], optimization: {...} , output: {...} , module: { rules: [ ..., { test: /\.txt$/, use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ] } ] } } 复制代码
这样就配置完成了 4)我们在入口文件中导入这个脚本
为什么这里需要导入呢,我们不是配置了 webapck 处理所有的.txt 文件么?
因为 webpack 会做过滤,如果不引用该文件的话,webpack 是不会对该文件进行打包处理的,那么你的 loader 也不会执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import _ from 'lodash' ;import txt from '../txt/mytest.txt' import '../css/style.css' function component() { var element = document .createElement('div' ); var button = document .createElement('button' ); var br = document .createElement('br' ); button.innerHTML = 'Click me and look at the console!' ; element.innerHTML = _.join('【' + txt + '】' ); element.className = 'hello' element.appendChild(br); element.appendChild(button); // Note that because a network request is involved, some indication // of loading would need to be shown in a production-level site/app. button.onclick = e => import ( './print' ).then (module => { var print = module .default ; print (); }); return element; }document .body.appendChild(component()); 复制代码
package.json 配置
1 2 3 4 5 6 7 8 9 10 11 { ..., "scripts" : { "test" : "echo \" Error : no test specified\" && exit 1" , "build" : "webpack --config webpack.prod.js" , "start" : "webpack-dev-server --open --config webpack.dev.js" , "server" : "node server.js" }, ... } 复制代码
然后执行命令
这样我们的 loader 就写完了。
现在回答为什么要写两个 loader?
看到执行的顺序没,我们的配置的是这样的
1 2 3 4 5 use : [ './loader/uppercase-loader.js' , './loader/reverse-loader.js' ] 复制代码
正如前文所说,处理一个文件可以使用多个 loader,loader 的执行顺序是和本身的顺序是相反的 我们也可以自己写 loader 解析自定义模板,像 vue-loader 是非常复杂的,它内部会写大量的对.vue 文件的解析,然后会生成对应的 html、js 和 css。 我们这里只是讲述了一个最基础的用法,如果有更多的需要,可以查看 《loader 官方文档》
3 plugin 3.1 什么是 plugin? 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
plugin 和 loader 的区别是什么?
对于 loader,它就是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 或 A.less 转变为 B.css,单纯的文件转换过程 plugin 是一个扩展器,它丰富了 wepack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。
3.2 一个最简的插件 /plugins/MyPlugin.js(本地插件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class MyPlugin { constructor (options ) { console .log('MyPlugin constructor:' , options) } apply (compiler) { compiler.plugin('compilation' , compilation => { console .log('MyPlugin' ) )) } }module .exports = MyPlugin 复制代码
webpack 配置
1 2 3 4 5 6 7 8 9 10 11 12 const MyPlugin = require ('./plugins/MyPlugin' )module .exports = { entry: { index: './src/js/index.js' }, plugins: [ ..., new MyPlugin({param: 'xxx' }) ], ... }; 复制代码
这就是一个最简单的插件(虽然我们什么都没干)
webpack 启动后,在读取配置的过程中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 获得其实例。
在初始化 compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。
插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
并且可以通过 compiler 对象去操作 webpack。
看到这里可能会问 compiler 是啥,compilation 又是啥?
Compiler 对象包含了 Webpack 环境所有的的配置信息 ,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等 。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别在于: Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
3.3 事件流
webpack 通过 Tapable 来组织这条复杂的生产线。
webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。
绑定事件
1 2 3 4 compiler.plugin('event-name' , params => { ... }); 复制代码
触发事件
1 2 compiler.apply ('event-name' ,params ) 复制代码
3.4 需要注意的点
只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 webpack,才会进入下一处理流程 。例如:
1 2 3 4 5 6 7 8 compiler.plugin('emit' ,function (compilation, callback) { ... // 处理完毕后执行 callback 以通知 Webpack // 如果不执行 callback,运行流程将会一直卡在这不往下执行 callback(); }); 复制代码
关于 complier 和 compilation,webpack 定义了大量的钩子事件。开发者可以根据自己的需要在任何地方进行自定义处理。《compiler 钩子文档》 《compilation 钩子文档》
3.5 手写一个 plugin 场景: 小程序 mpvue 项目,通过 webpack 编译,生成子包(我们作为分包引入到主程序中),然后考入主包当中。生成子包后,里面的公共静态资源 wxss 引用地址需要加入分包的前缀:/subPages/enjoy_given。 在未编写插件前,生成的资源是这样的,这个路径如果作为分包引入主包,是没法正常访问资源的。 所以需求来了: 修改 dist/static/css/pages 目录下,所有页面的样式文件(wxss 文件)引入公共资源的路径。 因为所有页面的样式都会引用通用样式 vender.wxss
1 2 那么就需要把@import "/static/css/vendor.wxss" ; 改为:@import "/subPages/enjoy_given/static/css/vendor.wxss" ; 复制代码
OK 开始! 1)创建插件文件 CssPathTransfor.js CssPathTransfor.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class CssPathTransfor { apply (compiler) { compiler.plugin('emit' , (compilation, callback ) => { console .log('--CssPathTransfor emit' ) for (var filePathName in compilation.assets) { if (/static\/css\/pages/i .test(filePathName)) { const reg = /\/static\/css\/vendor\.wxss/i const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss' let content = compilation.assets[filePathName].source() || '' content = content.replace(reg, finalStr) compilation.assets[filePathName] = { source () { return content; }, size () { return content.length; } } } } callback() }) } }module .exports = CssPathTransfor 复制代码
看着挺多,实际就是遍历 compilation.assets 模块。对符合要求的文件进行正则替换。 2)修改 webpack 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 var baseWebpackConfig = require('./webpack.base.conf')var CssPathTransfor = require('../plugins/CssPathTransfor .js')var webpackConfig = merge(baseWebpackConfig, { module: {...} , devtool: config.build.productionSourceMap ? ' output: {...} , plugins: [ ..., // 配置插件 new CssPathTransfor (), ] }) 复制代码
插件编写完成后,执行编译命令 搞定~ 如果有更多的需求可以参考《如何写一个插件》