webpack 核心概念
Entry (入口文件路径)
入口起点 指示webpack应该使用哪个模块 来作为构建其内部依赖图的开始
进入入口起点后 webpack会寻找有哪些模块和库是入口起点(直接 间接)依赖的
每个依赖即将被处理后 最后输出到命名为bundles的文件中
Output (输出路径)
output属性告诉webpack在哪里输出它所创建的bundles 以及如何命名这些文件 默认为./dist
基本上整个程序模块都会编译到你指定的输出路径文件夹
Module (模块)
在webpack一些都是模块 一个模块对应着一个文件 webpack会从配置的入口文件开始递归寻找所有依赖的模块
Chunk (代码块)
一个Chunk 由 多个模块组合而成 用于代码合并和分割
Loader
loader 让webpack能够处理一些非JavaScript(webpack本身只能理解JavaScript)
loader 可以将所有类型的文件 转换为 webpack可以处理的有效模块 然后就可以webpack的打包能力 进行处理
本质上webpack loader 讲所有类型的文件 转**换为应用程序的依赖图(最终为bundle) **可以直接引用的模块
Plugin
loader 被用于转换某些类型的模块 **而Plugin则可以用于执行范围更广的任务 **
插件的范围包括 **从打包优化 和压缩 **一直到重新定义环境中的变量,插件接口功能极其强大 可以处理各种各样的任务
webpack打包流程
- 读取webpack的
配置参数 - 启动webpack
创建Compiler对象并开始解析项目 - 从入口文件开始解析 寻找导入的模块 并且
递归遍历分析 形成依赖关系树 - 对不同的文件的`依赖文件使用配置好的对应loader进行编译 最终转换为javaScript代码
- 整个过程中webpack会通过
发布订阅模式 回向外抛出一些hooks而Plugin即可通过监听这些关键的事件节点 执行插件即可通过监听这些关键事件节点,执行插件任务进而达到干预输出结果
其中文件的解析与构建主要依赖于compiler和compilation两个核心对象
Compiler 对象是一个全局单实例 他负责整个webpack打包的构建流程 compilation对象是每一次构建的上下文对象 它包含了当次构建的所需要的信息 每次热更新和重新构建 compile都会重新生成一个新的compilation 负责此次更新的构建过程
而每个模块间的依赖关系 依赖于ast语法树 每个模块文件在通过loader解析完成后 会通过acorn库生成模块代码的ast语法树 通过语法树就可以分析出来这个模块是否还有依赖模块 进而继续循环执行下一个模块的编译
sourceMap
它是一项将编译,打包,压缩后的代码映射回源代码的一种技术 由于打包压缩后的代码并没有阅读行可言,一旦在开发中报错或者遇见问题 直接无法快速的查找问题
sourceMap 可以帮助我们快速定位到源代码的位置 提高我们的开发效率 sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap。
既然是源代码的映射 那必然会有一份映射文件 来标记代码里面对应的源代码位置 通常这份映射文件都是以.map结尾 (map文件只要不打开开发者工具 浏览器就不回去加载的) 里面的数据结构大概是这样
{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}
当有了这份映射文件 我们只需要在压缩代码的最末端加上sourceURL的注解 就可让sourceMap配置文件生效
//# sourceURL=/path/to/file.js.map
有了这段注解后 浏览器就会通过sourceURL去获取映射文件 通过解析器解析后 实现源码和代码之间的一个映射 因此sourceMap也是需要浏览器支持的技术
其实webpack默认下的devtool:"development"开发模式下 打包下的bundle文件 每一个_webpack_modules_文件模块的末端 都会加上//# sourceURL=webpack://file-path 从而实现对sourceMap的支持
项目实践中 需要我们在webpack的配置参数里面 设置devtool:"source-map" 在打包编译后的bundle.js文件后面加上注解引用 这样的话 在浏览器控制台报错了 会直接定位到报错代码
关于source map的各种模式
他一共有12种模式打包 不同模式下生成的source map效果都会有一定的差别 并且打包速度以及webpack-dev-server 重新创建速度也不一样
// 使用这段代码记得安装babel-babel,yarn add babel-loader @babel/core @babel/preset-env --dev
// 使用babel-loader编译转换后更容易区分,带有module的模式与不带module模式的区别
const allModes = [
'eval',// 带有eval的,表示以使用eval执行模块代码
'eval-source-map',
'eval-cheap-source-map',// cheap表示包含行信息
'eval-cheap-module-source-map', // 有module的模式下,source map中的代码是没经过loader加工过的,也就是我们手写的代码
'cheap-source-map',
'cheap-module-source-map',
'inline-source-map',// 和普通的soure map效果基本一样,只不过sourcemap模式下,它的sourcemap文件是以物理文件的方式存在,inline-source-map是将sourcemap以DataUrl的方式嵌入到代码中(和eval有些类似),这会导致代码体积大很多
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'hidden-source-map',// 这个模式下,我们在开发工具中是看不到效果的,但是它确实生成了sourcemap文件,这和jquery一样,生成了sourcemap文件但并没有在代码中注释引入,这个模式一般是我们在开发一些第三方包的时候可能会用到
'nosources-source-map'// 这个模式下我们能看到错误出现的位置,但是我们点击错误信息进去是看不到源代码的,nosources指的就是没有源代码,但还是提供了行列信息,我们可以结合源代码找到错误代码
]
module.exports = allModes.map( item => {
return {
devtool:item,
mode:"none",
entry:"./src/main.js",
output:{
filename:`js/${item}.js`
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:"babel-loader",
options:{
presets:['@babel/preset-env']
}
}
}
]
}
}
})
建议
开发环境 :建议使用eval-cheap-module-source-map 一般情况下我们的代码每一行都不会太长 经过loader处理后会有较大的差异 因为在这种模式下 map里面的代码都是没有经过loader加工过的 并且也都是我们手写的代码 方便查看
生产环境 :mode建议是none 因为sourcemap会在控制台上面暴露源代码
- 也可以是使用
nosources-source-map这种模式他只是看见报错的位置行数 但是看不见源代码 这样就不会有被人盗窃源代码的危险 并且还是可以根据行列信息找到错误代码 安全系数比sourcemap高 hidden-source-map也可以用这种模式 他属于在开发工具看不见 但是也生成map文件 需要借助三方错误监控平台Sentry来查看- 也可以通过nginx设置将map文件只对白名单开发 (公司内网)
注意事项 避免在生产中使用inline-和eval-因为他们会增加bundle打包后的文件的体积大小 并降低整体的性能
Loader 原理与实现
Loader支持链式调用 所以在开发上需要严格遵循单一指责 每一个Loader只负责自己需要负责的事情
-
Loader 他会针对配一个文件类型 支持一个数组格式 并且配置多个loader 因为在webpack在转换该文件的时候 会按照顺序链式调用每一个loader。前一个loader返回的内容会作为下一个loader的入参数(比如转换css的时候需要先使用less/在使用css/在使用style)
-
loader运行在node环境下 它可以使用node自带的api或安装第三方模块进行调用,并且webpack传给loader的原内容都是UTF-8的格式编码字符串 当某些场景下loader需要处理二进制文件 需要通过
exports.raw=true告诉webpack 该loader是否需要二进制数据 所以说按照开发规范 返回的标准值是一个标准的**js代码字符串 **以保证下一个loader的可以正常工作 -
loader的函数中的
this有webpack提供 可以通过this对象提供相关的属性 获取到当前loader各种信息数据 ,源码中上 这个this指向的是叫loaderContext和loader-runner特有对象 -
尽可能的
异步化loader如果计算量很小 也可以同步 -
loader是无状态的 我们不应该在loader种保留状态
-
加载本地loader方法 NPM link ResolveLoader
Plugin的 原理与实现
webpack在运行的生命周期中会广播出许多时间 Plugin可以监听到这些时间 在特定的阶段钩入想要添加的自定义功能 webpack的Tapable事件流机制 保证了插件的有序性 使得整个系统扩展性变得良好
在webpack中暴露了compiler和compilation 两个核心的对象
- 其中compiler暴露了和webpack整个生命周期的钩子
- compilation暴露了模块和依赖相关的粒度更小的事件钩子
- 插件必须是一个函数或者一个包含apply方法的对象 这样才能访问compiler
- 因为传递给每一个插件的compiler和compilation都是一个对象的引用 若在一个插件中修改了他们属性 则会影响到后面的插件使用
- 异步的事件需要在插件处理完任务时调用回调函数 通知webpack 进行下一个流程 要不会卡住的
- 找到合适的时间点去完成打算要做的事
-
- emit 事件发生时 可以读取到最终输出的资源,代码块,模块以及依赖 并可以进行修改(emit事件是修改webpack输出资源的最后时机)
-
- watch-run 则是监听文件发生变化时出发
const { SyncHook } = require("tapable");
class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}