webpack 做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包📦 生成到指定的文件中
Loaders
loader 则是用于对模块的 源代码 进行转换,在 import 或 require 模块(加载模块)时预处理文件
在 webpack 中,任何文件都是模块,默认情况下,在遇到 import 或者 require 加载模块的时候,webpack 只支持对 js 和 json 文件打包;像 css、sass、png 等这些类型的文件,webpack 则无能为力,这时候就需要配置对应的 loader 进行文件内容的解析
在加载模块的时候,执行顺序为: 入口 entry ➡️ loaders ➡️ output 输出
当 webpack 碰到无法识别的模块时,则去配置的 loaders 中查找该文件的解析规则(一般推荐配置在 webpack.config.js 中)
// webpack.config.js 示例配置
module.exports = {
entry: './src/main.ts',
// ... 其他配置
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [
/* 创建 <style> 标签插入 html */
'style-loader',
/* 识别解析 css 语法 ⬆️ */
'css-loader',
/* 识别解析 sass 语法 ⬆️ */
'sass-loader'
]
}
]
}
}
loader 特性
-
loader支持 链式调用链中的每个
loader会处理之前已处理过的资源,最终变为js代码。顺序为 相反 的顺序执行,即上述执行顺序为sass-loader、css-loader、style-loader -
loader可以是同步的,也可以是异步的 -
loader运行在Node环境中,并且能够执行任何操作(比如读取文件,输出文件等) -
插件(
plugin)可以进一步增强loader的功能 -
loader能够产生额外的任意文件
使用 loader 可以将各功能模块进行更细粒度的拆分
常见 loader
style-loader:将css添加到DOM的内联样式标签<style>里css-loader:允许将css文件通过require的方式引入,并返回css代码less-loader:处理lesssass-loader:处理sasspostcss-loader:用postcss来处理css(如:添加浏览器兼容类名前缀-webkit-)file-loader:分发文件到output目录并返回相对路径url-loader:和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Urlhtml-minify-loader:压缩HTMLbabel-loader:用babel来转换ES6文件到ES5
Plugins
plugin是一种计算机应用程序,它和主应用程序(host application)互相交互,以提供特定的功能
webpack 中的 plugin 是遵循一定规范编写出来的程序,为 webpack 赋予灵活的功能,如:打包优化、资源管理、环境变量注入等,它们会运行在 webpack 的不同阶段(钩子 / 生命周期),贯穿了 webpack 整个编译周期
它们与
loader不同,loader是串行运行的,只在某个阶段运行,plugin则是贯穿编译的整个周期,旨在解决loader无法解决的问题
// webpack.config.js 示例配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.ts',
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' })
]
}
特性
本质上它是一个具有 apply 方法的 javascript (类)对象;apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象
// 示例
class HtmlWebpackPlugin {
constructor (options) {
this.userOptions = options || {};
this.version = HtmlWebpackPlugin.version;
}
apply(compiler) {
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
const userOptions = this.userOptions;
const defaultOptions = { /* ... default options */ }
const options = Object.assign(defaultOptions, userOptions)
this.options = options
// ...
})
}
}
module.exports = HtmlWebpackPlugin;
从上面的示例代码,我们可以知道,compiler.hooks tap 方法的第一个参数就是插件名称(大驼峰命名)
钩子函数类型来自Tapable 🔗,暴露的钩子函数有webpack plugin hooks 🔗,比如:
-
entryOption:[SyncBailHook] 初始化option,在webpack选项中的entry被处理过之后调用(回调参数:context,entry)compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => { /* ... */ }) -
run:[AsyncSeriesHook] 在开始读取records之前调用 -
compile:[SyncHook]beforeCompile之后立即调用,但在一个新的compilation创建之前。这个钩子 不会 被复制到子编译器 -
compilation:[SyncHook]compilation创建之后执行(回调参数:compilation,compilationParams) -
make:[AsyncParallelHook]compilation结束之前执行。这个钩子 不会 被复制到子编译器(回调参数:compilation) -
afterCompile:[AsyncSeriesHook] 编译build过程结束,compilation结束和封印之后执行(回调参数:compilation) -
emit:[AsyncSeriesHook] 输出asset到output目录之前执行。这个钩子 不会 被复制到子编译器 -
afterEmit:[AsyncSeriesHook] 输出asset到output目录之后执行。这个钩子 不会 被复制到子编译器 -
done:[AsyncSeriesHook] 在compilation完成时执行。这个钩子 不会 被复制到子编译器 -
failed:[SyncHook] 在compilation失败时执行(回调参数:error)
常见 plugin
html-webpack-plugin:在打包结束后,⾃动生成⼀个html文件,并把打包生成的js模块引⼊到该html中clean-webpack-plugin:删除(清理)构建目录(每次打包都清除上次打包生成的文件)copy-webpack-plugin:复制文件或目录到打包结果目录中
二者的区别
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中plugin赋予了webpack各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决loader无法实现的其他事
-
在运行时机上:
loader运行在打包之前plugins在整个编译周期都起作用
-
在功能扩展上:
webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果- 对于
loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是 文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
编写 Loader
loader 的本质:
loader的本质就是函数,函数中的this作为上下文会被webpack填充,因此我们 不能 将loader设为一个箭头函数- 这个函数接受一个参数,为
webpack传递给loader的 文件源内容- 函数中
this是由webpack提供的对象,能够获取当前loader所需要的各种信息- 函数中有异步操作或同步操作,异步操作通过
this.callback返回,返回值要求为String或者Buffer
示例:
// 导出一个函数,source 为 webpack 传递给 loader 的文件源内容
module.exports = function(source) {
const content = doSomeThing2JsString(source);
// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;
// 可以用作解析其他模块路径的上下文
console.log('this.context', this.context);
/* this.callback(
* // 当 loader 出错时向外抛出一个 error
* err: Error | null,
* // 经过 loader 编译后需要导出的内容
* content: string | Buffer,
* // 可选参数,返回 source-map(为方便调试生成的编译后内容的 source-map)
* sourceMap?: SourceMap,
* // 可选参数,返回 meta(官方表示:ignored by webpack, can be anything)
* meta?: any
* );
*/
this.callback(null, content); // 异步
return content; // 同步
}
原则:在编写 loader 时,尽量保持功能单一,方便维护与查找问题
编写 Plugin
由于
webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务
webpack 编译会创建两个核心对象:
compiler:包含了webpack环境的所有的配置信息,包括options,loader和plugin,和webpack整个生命周期相关的钩子compilation:作为plugin内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的Compilation将被创建
编写 plugin 要遵循的规范:
- 插件必须是一个函数或者是一个包含
apply方法的对象(一般是class类),这样才能访问compiler实例 - 由于传给每个插件的
compiler和compilation对象都是同一个引用,因此不建议修改 - 异步的事件需要在插件处理完任务时调用回调函数通知
webpack进入下一个流程,不然会卡住
示例:
class MyPlugin {
// webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}
在 emit 事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将 输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容