Webpack api- Loader/Plugin 概括

109 阅读9分钟

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打包流程

  1. 读取webpack的配置参数
  2. 启动webpack 创建Compiler对象并开始解析项目
  3. 从入口文件开始解析 寻找导入的模块 并且递归遍历分析 形成依赖关系树
  4. 对不同的文件的`依赖文件使用配置好的对应loader进行编译 最终转换为javaScript代码
  5. 整个过程中webpack会通过发布订阅模式 回向外抛出一些hooksPlugin即可通过监听这些关键的事件节点 执行插件即可通过监听这些关键事件节点,执行插件任务进而达到干预输出结果

其中文件的解析与构建主要依赖于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指向的是叫loaderContextloader-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...
    })
  }
}