Webpack知识点整理

1,057 阅读11分钟

为什么要使用Webpack?

要理解 webpack 是什么,我们先记住这两个词:- 模块- 打包

  • 模块化理想的方式是在页面中引入一个 JS 入口文件,其余用到的模块可以通过代码控制按需加载
  • 模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来,零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。那是不是我把所有 JavaScript 文件合成一个文件就好了呢?没错,这样就减少了 http 请求数量,让我们的页面加载和显示更快,但是如果在开发过程就合并这些文件,将导致项目难以维护,在开发后完成的这个合并的过程就是打包

为什么Webpack要在JS中加载其他资源?

假设在开发页面上的某个局部功能,需要用到一个样式模块和一个图片文件,如果你还是将这些资源文件单独引入到 HTML 中,然后再到JS中添加对应的逻辑代码,试想如果后期这个局部功能不用了,你就需要同时删除JS中的代码和 HTML 中的资源文件引入,也就是需要同时维护两条线。而如果你遵照 Webpack 的这种设计,所有资源的加载都是由JS 代码控制,后期也就只需要维护 JS 代码这一条线。

常用的loader

  • css-loader:Webpack 是用 JS 写的,运行在 node 环境,所以默认 Webpack 打包的时候只会处理 JS 之间的依赖关系,如果在 JS 中导入了 css,那么就需要使用 css-loader 来识别这个模块,通过特定的语法规则进行转换内容最后导出 css-loader 会处理 import / require() / @import / url 引入的内容。
  • style-loader:css-loader 只会把 css 模块加载到 JS 代码中,并不会使用这个模块,因为 css-loader 处理之后导出的是个数组,页面是无法直接使用,这时我们需要用到零外一个 style-loader 来处理,style-loader 是通过一个 JS 脚本创建一个 style 标签,里面包含一些样式。
  • file-loader:在css文件中定义 background 的属性或者在html中引入 image 的src,我们知道在 webpack 打包后这些图片会打包至定义好的一个文件夹下,和开发时候的相对路径会不一样,这就会导致导入图片路径的错误。而 file-loader 正是为了解决此类问题而产生的,他修改打包后图片的储存路径,再根据配置修改我们引用的路径,使之对应引入。
  • url-loader:如果页面图片较多,发很多http请求,会降低页面性能。这个问题可以通过 url-loader 解决。url-loader 会将引入的图片编码,生成 dataURl 并将其打包到文件中,最终只需要引入这个 dataURL 就能访问图片了。当然,如果图片较大,编码会消耗性能。因此 url-loader 提供了一个limit参数,小于 limit 字节的文件会被转为 DataURl,大于 limit 的还会使用 file-loader 进行copy。url-loader 内置了 file-loader。
  • babel-loader:如今 ES6 语法在开发中已经非常普及,甚至也有许多开发人员用上了 ES7 或 ES8 语法。然而,浏览器对这些高级语法的支持性并不是非常好。因此为了让我们的新语法能在浏览器中都能顺利运行,Babel 应运而生。Babel是一个 JavaScript 编译器,能够让我们放心的使用新一代JS语法。

常用的plugin

  • clean-webpack-plugin:每次打包后自动清理dist目录。
  • html-webpack-plugin:相比于之前写死HTML文件的方式,自动生成HTML的优势在于: 1.HTML也输出到dist目录中了,上线时只需要把dist目录发布出去。
    2.HTML中的script标签是自动引入的,所以可以确保资源文件的路径是正常的。
  • DefinePlugin:这是一个定义全局变量的插件,定义的变量可以在webpack打包范围内任意 javascript 环境内访问,使用字符串或 JSON.stringify() 转换值。
  • MiniCssExtractPlugin:将 css 单独打包成一个文件的插件,它为每个包含 css 的 js 文件都创建一个 css 文件,结合 html-webpack-plugin,以 link 的形式插入到 html 文件中。它支持 css 和 sourceMaps 的按需加载。目前只有在 webpack V4 版本才支持使用该插件。这个插件应该只在生产环境构建中使用,并且在 loader链中不应该有style-loader,特别是我们在开发模式中使用 HMR 时。此插件不支持 HMR,若修改了样式文件,是不能即时在浏览器中显示出来的,需要手动刷新页面。
  • HotModuleReplacementPlugin:HMR 作为一个 Webpack 内置的功能,可以通过 --hot 或者 HotModuleReplacementPlugin 开启。具体介绍热更新请看之前有关热更新讲解的文章。

loader和plugin的区别

  • loader是转换器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

    1.处理一个文件可以使用多个loader,loader 的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个 loader 最后执行。
    2.第一个执行的 loader 接收源文件内容作为参数,其它 loader 接收前一个执行的 loader 的返回值作为参数,最后执行的 loader 会返回此模块的 JavaScript 源码 。

  • plugin 是插件扩展器,在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出果。

    针对webpack打包的过程,它不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些事件钩子,执行任务。plugin 比 loader 强大,通过plugin 可以访问 compliler 和 compilation 过程,通过钩子拦截 webpack 的执行。

Webpack编译流程

  1. 解析 webpack 配置参数,合并从 shell 传入和 webpack.config.js 文件里配置的参数,进行错误检查,生产最后的配置结果。
  2. 注册所有配置的插件和初始化 complier,好让插件监听 webpack 构建生命周期的事件节点,以做出对应的反应。(利用 tapable 将 plugin 挂载在特定的钩子上 hook)
  3. 开始编译主入口,从配置的 entry 入口文件开始解析文件,使用@babel/parser构建 AST 抽象语法树,然后使用@babel/traverse找出每个文件所依赖的文件,然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code,将找到的入口文件的依赖模块,进行遍历递归,重复执行前面的步骤。重写require函数,并与生成的递归关系图一起,输出到bundle
  4. 在解析文件递归的过程中根据文件类型和 loader 配置找出合适的 loader 用来对文件进行转换。
  5. module 优化,为 module 增加ID。
  6. 递归完后得到每个文件的最终结果,根据 entry 配置生成代码块 chunk。
  7. 输出所有 chunk 到文件系统。

模块加载

  • 核心方法

    1. webpack_modules:所有的模块标记使用和被使用used、unused(production环境下或sideEffect:true,会被去掉)
    2. webpack_module_cache:缓存执行过的模块
    3. webapck_require:以 moduleId 为参数,调用时先从 cache 中查找,如果找到则直接返回(return cachedModule.exports),如果找不到则创建一个新模块放入缓存并执行这个模块方法,返回模块的导出。__webpack_require__所在的闭包能访问外层变量modules和缓存installedModules。这个很关键,因为modules是 webpack 打包后立即执行函数传入的参数。
  • 同步加载

    直接通过 webpack_require 加载模块

  • 异步加载

    require.ensure()和import()本质上都是动态创建一个script

    1. 将import()的模块打包成单独的 chunk
    2. 入口文件执行,为全局的 self[webpackJsonp]构造 push,即当下载完成后执行:webpackJsonpCallback(①将 module 保存进全局当 modules ②遍历 chunkIds,设置 installedChunks 标志已下载)
    3. 执行 import(),构造 script,利用 jsonP 下载,返回一个 promise包裹的结果,调用__webpack_require__,拿到保存在全局 modules 中的模块,执行 then()方法。

loader解析

  • 构造ruleSet实例:
    1. 合并用户配置、内置的规则,在后面匹配起到过滤作用.
  • 分类处理:
    1. 有内联路径则处理字符串,生成内联的loader数组,通过 resolveRequestsArray 递归解析 loader,完成回调 continueCallback。
    2. 无内联路径则直接解析文件路径,完成回调 continueCallback。
  • loader处理module:
    1. module 创建——>开始 build——>创建 loaderContext 上下文——>runloader——>parse module content
    2. 核心在于 runLoader,pitch 向下,达到最后一个 loader,相当于是一个拦截器功能,再回溯向上执行loader

chunk生成

  • 准备工作:
    1. 以入口文件建立一个 chunkGroup
    2. 建立 chunk 和 chunkGroup 关系
    3. 建立 module 和 chunk 关系
  • visitModule:
    1. 根据 module graph 建立 chunk graph
  • 优化chunk graph:
    1. 剔除重复的依赖

Webpack性能优化

  • 提高构建速度

    1. 通过 exclude、include 缩小搜索范围
    2. 配置 resolve.modules:[path.resolve(__dirname,'node_modules')]避免层层查找
    3. 设置 resolve.alias,使 webpack 直接使用库的 min 文件,避免库内解析,利用UnsafeCachePlugin缓存,并且减少文件路径的查找
    4. 设置 resolve.extensions ,减少文件查找,设置 resolve.extensions 中的值,列表值尽量少,频率高的文件类型的后缀写在前面。['js','less']
  • 缓存之前构建过的js

    1. 将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。 loader:'babel-loader?cacheDirectory=true'
  • 并行构建:

    受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。HappyPack和ThreadLoader作用是一样的,都是同时执行多个进程,从而加快构建速度。而Thread-Loader是webpack4提出的。

    采用HappyPack开启多进程Loader

    使用方式

    npm i happypack -D
    // webpack.config.json
    const path = require('path');
    const HappyPack = require('happypack');
    ​
    module.exports = {
        //...
        module:{
            rules:[{
                    test:/.js$/,
                    // 这里的id对应下面的id
                    use:['happypack/loader?id=babel']
                    exclude:path.resolve(__dirname, 'node_modules')
                },{
                    test:/.css/,
                    use:['happypack/loader?id=css']
                }],
            plugins:[
                new HappyPack({
                    id:'babel',
                    loaders:['babel-loader?cacheDirectory']
                }),
            ]
        }
    }
    

    ThreadLoader开启多进程

    使用方式

    module.exports = {
           {
            test:/.js$/,
            exclude:/node_modules/,
            use:[
              {
                loader:'thread-loader',
                options:{
                  wordkers:2 // 进程2个
                }
              },
              ...
            ]
    }
    

    需要注意的是,开启多进程的意义在于需要构建的文件的体积足够大,如果不够大,不然效果不够明显,明显有高射炮打小苍蝇的感觉。因为进程开启需要时间,进程之间的通信也需要时间,如果这点时间都不能忽略不计,那么意义不大。

  • 采用Oneof

    1. 通常来讲,同一种类型的文件只能由一个 loader 处理,那么正常来讲的逻辑应该是,比如我是一个 css 文件,那么我匹配到 test 为 css 后缀的 loader 我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。
      而 Oneof 语法就是解决这个问题的,使文件一旦匹配上 loader 之后就立即解析,省去了全盘遍历这个不必要的过程。
  • 压缩打包体积

    1. 使用 TreeShaking 删除无用代码

    2. splitChunks 将体积较大的包提取出来

      image.png

  • sourceMap提高调试体验

    1. 在开发环境下可以提升构建速度和调试速度
    2. 在生产环境可以减小代码体积

Rollup和webpack区别

特性:
rollup 所有资源放同一个地方,一次性加载,利用 tree-shake 特性来 剔除未使用的代码,减少冗余
webpack 拆分代码、按需加载 webpack2 已经逐渐支持 tree-shake
rollup:

  • 打包你的 js 文件的时候如果发现你的无用变量,会将其删掉。
  • 可以将你的 js 中的代码,编译成你想要的格式 webpack:
  • 代码拆分
  • 静态资源导入(如 js、css、图片、字体等),拥有如此强大的功能,所以 webpack 在进行资源打包的时候,就会产生很多冗余的代码。

应用场景:
项目(特别是类库)只有 js,而没有其他的静态资源文件,使用 webpack 就有点大才小用了,因为 webpack bundle 文件的体积略大,运行略慢,可读性略低。这时候 rollup就是一种不错的解决方案