webpack 学习笔记

2020-10-15  本文已影响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?

核心概念

module.exports = {
  entry: {
    app: './src/index.js'
  }
}
module.exports = {
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}
module.exports = {
  mode: "production",
}
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

plugins

loader 和 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

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

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 来优化前端性能

如何提高 webpack 的构建速度

上一篇下一篇

猜你喜欢

热点阅读