美文网首页
webpack 学习笔记

webpack 学习笔记

作者: 常威爆打来福 | 来源:发表于2020-10-15 17:58 被阅读0次

    写在前面

    学习 webpack 建议先按照官网指南流程操作一遍,对于指南中不理解的问题建议再看完整个指南后再去研究。webpack 目前最新已经是 5.1.0,指南中有些方法已经过时,在内容最后我整理了操作时遇到的问题。

    何为 webpack?

    官网定义: 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

    简单来说 webpack 就是一个前端资源打包工具,它根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应静态资源。下图完美阐释了这一点


    webpack

    为什么会需要使用它?

    想象我们最开始学习 html 和 js 写过的 demo,在 html 中通过<script />引入 js 文件,并且在各个 js 文件中互相引用,当时我们的目的是可以运行,这样做并没有什么问题。
    当一个正式项目中,有几百上千个 js 文件时,想象一下,调用一个 js 方法,那么浏览器就要依次发送请求去请求这个 js 中所引用别的 js 文件中方法,其中一个因网络问题返回延迟会导致这个页面显示错误。这时候你会想,是不是我把所有 js 文件合成一个文件就好了呢?是的,webpack 就是帮我们做这件事的。webpack 依赖强大的 loader 和 plugins 为前端开发提供了更多的可能。

    什么是 bundle?什么是 chunk?什么是 module?

    • bundle: 是由 webpack 打包出来的文件

    • chunk: 代码块,是指 webpack 在进行模块依赖分析的时候代码分割出来的代码块。主要体现在CommonsChunkPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk

    • module: 是开发中的单个模块,在 webpack 中,一个模块对应一个文件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块

    核心概念

    • 入口(entry): 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。每个依赖项随即被处理,最后输出到 bundles 文件中。详细介绍
    module.exports = {
      entry: {
        app: './src/index.js'
      }
    }
    
    • 出口(output): 告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist详细介绍
    module.exports = {
      output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
      }
    }
    
    • 模式(mode): 通过选择 developmentproduction 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化。详细介绍
    module.exports = {
      mode: "production",
    }
    
    • loader: loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。详细介绍
    module.exports = {
      module: {
        rules: [
          { test: /\.css$/, use: 'css-loader' },
          { test: /\.ts$/, use: 'ts-loader' }
        ]
      }
    };
    
    const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
    
    class ConsoleLogOnBuildWebpackPlugin {
        apply(compiler) {
            compiler.hooks.run.tap(pluginName, compilation => {
                console.log("webpack 构建过程开始!");
            });
        }
    }
    

    常见 loader 和 plugins

    loader

    • style-loader: 通过注入<style>标签将 CSS 添加到 DOM

    • css-loader: 加载 CSS,解释 @importurl(),建议将 style-loadercss-loader 结合使用

    • file-loader: 把文件输出到一个文件夹中,在代码中通过相对 url 去引用

    • url-loader: 和 file-loader 类似,但是能在很小的情况下以base64的方式把文件内容注入到代码中

    • slint-loader: 使用 ESLint 清理代码

    plugins

    • CleanwebpackPlugin: 在每次构建前清理 /dist 文件夹

    • HtmlwebpackPlugin: 解决在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle而造成的引用名称没有变的问题

    • CommonsChunkPlugin: 通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用

    • HotModuleReplacementPlugin: 启用热替换模块

    • DefinePlugin: 允许创建一个在编译时可以配置的全局常量

    loader 和 plugin 的不同?

    作用

    • loader: 加载器。让 webpack 具有加载和解析非 JavaScript 文件的能力。
    • plugin: 插件。监听 webpack 的运行生命周期事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

    用法

    • loader: 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
    • plugin: 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

    webpack与 grunt、gulp 的不同?

    gruntgulp 是基于任务和流(Task、Stream)的。找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。

    const { src, dest, parallel } = require('gulp');
    const pug = require('gulp-pug');
    const less = require('gulp-less');
    const minifyCSS = require('gulp-csso');
    const concat = require('gulp-concat');
    
    function html() {
      return src('client/templates/*.pug')
        .pipe(pug())
        .pipe(dest('build/html'))
    }
    
    function css() {
      return src('client/templates/*.less')
        .pipe(less())
        .pipe(minifyCSS())
        .pipe(dest('build/css'))
    }
    
    function js() {
      return src('client/javascript/*.js', { sourcemaps: true })
        .pipe(concat('app.min.js'))
        .pipe(dest('build/js', { sourcemaps: true }))
    }
    
    exports.js = js;
    exports.css = css;
    exports.html = html;
    exports.default = parallel(html, css, js);
    

    webpack是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 loader 来处理不同的文件,用 plugin 来扩展webpack 功能。

    const path = require('path');
    const HtmlwebpackPlugin = require('html-webpack-plugin');
    const { CleanwebpackPlugin } = require('clean-webpack-plugin');
    const webpack = require('webpack');
    
    module.exports = {
      entry: {
        app: './src/index.js' // 配置多个入口
      },
      output: {
        filename: '[name].bundle.js', // 输出文件名
        path: path.resolve(__dirname, 'dist') // 输出路径,绝对路径
      },
      mode: "production", // 模式
      devtool: 'inline-source-map', // 追踪错误和警告在源代码中的原始位置
      devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        open: true,
        port: 3000,
        hot: true, // 开启热更新
        hotOnly: true // 热更新失败时不刷新页面
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new HtmlwebpackPlugin({
          title: 'Output Management'
        }),
        new CleanwebpackPlugin(), // 每次编译前清理历史
        new webpack.HotModuleReplacementPlugin() // 热部署
      ],
    };
    

    webpack 的构建流程?

    1 解析配置:合并 shell 传入和 webpack.config.js 文件配置参数,生产最终配置。
    2 开始编译:初始化 compiler 对象,注册所有配置的插件,插件监听 webpack 构建生命周期的事件,执行对象的 run 方法开始执行编译。
    3 确定入口:从 webpack 配置中的 entry 开始,解析文件构建 AST(抽象语法树)
    4 编译模块:根据文件类型和 loader 配置,递归对文件进行转换。再找出该模块依赖的模块,再处理,直到所有入口依赖文件全部经过处理。
    5 编译完成输出:得到每个文件编译结果,包含每个模块以及他们之间的依赖关系,根据 entry 置生成 chunk
    6 输出完成:输出所有的 chunk 到文件系统,生成浏览器可以运行的 bundle。

    webpack 的热更新是如何做到的?

    webpack HMR

    先理解图中这几个名称概念:

    • webpack-dev-server :一个服务器插件,相当于 express 服务器,启动一个 Web 服务,只适用于开发环境。
    • webpack-dev-middleware:一个 webpack-dev-server 的中间件,作用简单总结为:通过watch mode,监听资源的变更,然后自动打包。
    • webpack-hot-middleware:结合 webpack-dev-middleware 使用的中间件,它可以实现浏览器的无刷新更新,也就是 HMR。

    1 监控代码变化,重新编译打包

    在 webpack 的 watch 模式下,若发现文件中代码发生修改,则根据配置文件对模块重新编译打包。

    2 保存编译结果

    webpack 与 webpack-dev-middleware 交互,webpack-dev-middleware 调用 webpack 的 API 对代码变化进行监控,并通知 webpack 将重新编译的代码通过 JavaScript 对象保存在内存中。

    3 监控文件变化,刷新浏览器

    webpack-dev-server 开始监控文件变化,与第 1 步不同的是,这里并不是监控代码变化重新编译打包。
    当我们在配置文件中配置了 devServer.watchContentBasetrue ,webpack-dev-server 会监听配置文件夹中静态文件的变化,发生变化时,通知浏览器端对应用进行浏览器刷新,这与 HMR 不一样。

    4 建立 WS,同步编译阶段

    这一步都是 webpack-dev-server 中处理,主要通过 sockjs(webpack-dev-server 的依赖),在 webpack-dev-server 的浏览器端(Client)和服务器端(webpack-dev-middleware)之间建立 WebSocket 长连接

    然后将 webpack 编译打包的各个阶段状态信息同步到浏览器端。其中有两个重要步骤:

    • 发送状态

    webpack-dev-server 通过 webpack API 监听 compile 的 done 事件,当 compile 完成后,webpack-dev-server 通过 _sendStats 方法将编译后新模块的 hash 值用 socket 发送给浏览器端。

    • 保存状态

    浏览器端将_sendStats 发送过来的 hash 保存下来,它将会用到后模块热更新

    5 浏览器端发布消息

    当 hash 消息发送完成后,socket 还会发送一条 ok 的消息告知 webpack-dev-server,由于客户端(Client)并不请求热更新代码,也不执行热更新模块操作,因此通过 emit 一个 webpackHotUpdate 消息,将工作转交回 webpack。

    6 传递 hash 到 HMR

    webpack/hot/dev-server 监听浏览器端 webpackHotUpdate 消息,将新模块 hash 值传到客户端 HMR 核心中枢的 HotModuleReplacement.runtime ,并调用 check 方法检测更新,判断是浏览器刷新还是模块热更新。
    如果是浏览器刷新的话,则没有后续步骤。

    7 检测是否存在更新

    当 HotModuleReplacement.runtime 调用 check 方法时,会调用 JsonpMainTemplate.runtime 中的 hotDownloadUpdateChunk (获取最新模块代码)和 hotDownloadManifest (获取是否有更新文件)两个方法。其中 hotEnsureUpdateChunk 方法中会调用 hotDownloadUpdateChunk 。

    8 请求更新最新文件列表

    在调用 check 方法时,会先调用 JsonpMainTemplate.runtime 中的 hotDownloadManifest 方法, 通过向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将 mainfest 返回给浏览器端。

    9 请求更新最新模块代码

    hotDownloadManifest 方法中,还会执行 hotDownloadUpdateChunk 方法,通过 JSONP 请求最新的模块代码,并将代码返回给 HMR runtime 。然后 HMR runtime 会将新代码进一步处理,判断是浏览器刷新还是模块热更新。

    10 更新模块和依赖引用

    这一步是整个模块热更新(HMR)的核心步骤,通过 HMR runtime 的 hotApply 方法,移除过期模块和代码,并添加新的模块和代码实现热更新。
    hotApply 方法可以看出,模块热替换主要分三个阶段:

    (1)找出过期模块 outdatedModules 和过期依赖 outdatedDependencies
    (2)从缓存中删除过期模块、依赖和所有子元素的引用;
    (3)将新模块代码添加到 modules 中,当下次调用 __webpack_require__ (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。

    hotApply 方法执行之后,新代码已经替换旧代码,但是我们业务代码并不知道这些变化,因此需要通过 accept 事件通知应用层使用新的模块进行“局部刷新”,我们在业务中是这么使用。

    11 热更新错误处理

    在热更新过程中,hotApply 过程中可能出现 abort 或者 fail 错误,则热更新退回到刷新浏览器(Browser Reload),整个模块热更新完成。

    webpack 长缓存

    通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash] 替换可以用于在文件名中包含一个构建相关(build-specific)的 hash,但是更好的方式是使用 [chunkhash] 替换,在文件名中包含一个 chunk 相关(chunk-specific)的哈希。

      const path = require('path');
      const CleanwebpackPlugin = require('clean-webpack-plugin');
      const HtmlwebpackPlugin = require('html-webpack-plugin');
    
      module.exports = {
        entry: './src/index.js',
        plugins: [
          new CleanwebpackPlugin(['dist']),
          new HtmlwebpackPlugin({
    -       title: 'Output Management'
    +       title: 'Caching'
          })
        ],
        output: {
    -     filename: 'bundle.js',
    +     filename: '[name].[chunkhash].js',
          path: path.resolve(__dirname, 'dist')
        }
      };
    

    如何利用 webpack 来优化前端性能

    • 多入口情况下,使用CommonsChunkPlugin来提取公共代码
    • 通过externals配置来提取常用库
    • 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度
    • 使用Tree-shakingScope Hoisting来剔除多余代码
    • 利用DllPluginDllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。

    如何提高 webpack 的构建速度

    • 压缩代码。可以利用 webpack 的UglifyJsPluginParallelUglifyPlugin来压缩 JS 文件
    • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的publicPath参数来修改资源路径
    • 删除死代码。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
    • 提取公共代码。使用CommonsChunkPlugin来提取公共代码

    相关文章

      网友评论

          本文标题:webpack 学习笔记

          本文链接:https://www.haomeiwen.com/subject/bionpktx.html