webpack 学习笔记

写在前面

学习 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来提取公共代码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349