# 谈谈你对 webpack 的看法

webpack 是一个模块打包工具,可以使用 webpack 管理模块依赖,并编译输出模块们所需的静态文件。它能很好地管理、打包 web 开发中所用到的 html、css、js 以及各种静态文件(图片、字体等),让开发过程更加高效。对于不同类型的资源,webpack 有对应的模块加载器。webpack 模块打包器会分析模块间的依赖关系,最后生成了优化且合并后的静态资源。

# 特点

  1. 对 CommonJS、AMD、ES6 的语法做了兼容处理
  2. 对 js、css、图片等资源文件都支持打包
  3. 串联模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如对 ES6 的支持
  4. 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化的时间
  5. 支持 sourcemap,易于调试
  6. 具有强大的 plugin 接口,大多是内部插件,使用起来比较灵活

# webpack 与 grunt、gulp、rollup 的不同

# 优点

  • 专注于模块化项目
  • plugins 能做很多事情
  • 社区非常活跃

# 缺点

  • 上手难度高
  • plugins 繁多,需要不断学习才能灵活掌握,经常出现文章中推荐的 plugin,但是实际上已经停止维护
  • 对于初学者,调试很难定位问题
  • 构建速度较慢,这也是后起之秀主要针对的点

# 什么情况下选择 webpack?什么情况下选择 rollup?

  • webpack 适用于构建大型复杂的前端站点
  • rollup 适用于基础库的打包

从另一个角度说:如果你需要进行代码分割,或者你有很多静态资源,再或者你的项目深度依赖于 CommonJS,选择 webpack。

如果你的代码基于 ES6 模块编写,并且做出来的是供他人使用,可以考虑 rollup

# webpack 有什么优劣势

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

rollup 和 webpack 类似,但更专注于 ES6 的模块打包。它最大亮点是利用 ES6 模块设计,生成更简洁、更简单的代码。

webpack 是模块化管理工具和打包工具,它是基于入口。webpack 会自动递归解析入口所需要加载的所有资源文件,然后用不同的 loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

虽然目前主流方向是 webpack,但是一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件;另外一些库也会使用 rollup 打包。

# loader 和 plugin 的区别

# 从功能上说

loader 用于加载待打包的资源,Plugin 用于扩展 webpack 功能。

loader 本质上就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果,主要用于加载某些资源文件。因为 webpack 只会加载 js 和 json,这就需要对应的 loader 将资源转化,加载进来。

plugin 用于扩展 webpack 的功能(loader 其实也是扩展功能,但是只专注于转化文件这一领域),在 webpack 运行的生命周期中会广播许多生命周期钩子事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

# 从运行时机角度区分

loader 运行在打包文件之前(loader 为模块加载时的预处理文件)

plugin 在整个编译周期都起作用

# 从使用角度区分

loader 在 rules 中配置,类型为数组,每一项都是一个 Object,内部包含了 test(类型文件)、loader、options(参数)等属性

plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入

# webpack 构建过程

webpack 的运行流程是一个串行的过程

# 初始化参数

从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

# 开始编译

用上一部得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译

# 确定入口

根据配置中的 entry 找出所有文件的入口

# 编译模块

从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤处理

# 完成模块编译

再经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

# 输出资源

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

# 输出完成

在确定输出内容后,根据配置确定输出的路径和文件名,把文件内容写入文件系统

# 其他

以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供 API 改变 webpack 的运行结果

简单来说:

  • 初始化:启动构建,读取与合并配置参数,加载 plugin,实例化 Compiler
  • 编译:从 entry 出发,针对每个 Module 串行调用对应的 loader 去翻译文件的内容,在找到该 module 依赖的 module,递归的进行编译处理
  • 输出:将编译后的 module 组合成 chunk,将 chunk 转换成文件,输出到文件系统中

# webpack 如何解析代码路径

webpack 依赖 enhanced-resolve 来解析代码模块路径,这个模块像 Node.js 那一套模块路径解析的增强版,有很多可以自定义的解析配置。

模块解析规则分三种:

  • 解析相对路径
    1. 查找想对当前模块的路径下是否有对应的文件或文件夹,是 文件 则直接加载
    2. 如果是文件夹则找到对应文件夹下是否有 package.json 文件
    3. 有的话就按照文件中的 main 字段的文件名来查找文件
    4. 没有 package.jsonmain 字段,则查找 index.js 文件
  • 解析绝对路径:直接查找对应路径文件,不建议使用,因为不同的机器会用绝对路径查找不到
  • 解析模块名:查找当前文件目录,父级直至根目录下的 node_modules 文件夹,看是否有对应名称的模块

通过配置 resolve.aliasresolve.extensionsresolve.modules等字段优化路径查找速度

# sourcemap 的作用和原理

sourcemap 是将编译、打包、压缩之后的代码映射回源码的过程。打包压缩后,代码不具备良好的可读性,想要调试源码就需要 sourcemap,出错的时候控制台会直接显示原始代码出错的位置。

map 文件只要不打开开发者工具,浏览器是不会加载的。

# 模块打包原理

webpack 根据 webpack.config.js 中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用 CommonJS 写的,还是 ES6 Module 规范写的,webpack 会自动进行分析,并通过转换、编译代码,打包成最终文件。最终文件中的模块实现是基于 webpack 自己实现的 webpack_require(es5 代码),所以打包后的文件可以跑在浏览器上。

同时意味着在 webpack 环境中,你可以使用 es6 语法,也可以使用 commonjs 语法,因为从 webpack2 开始内置了对 ES6、CommonJS、AMD 模块化语句的支持,webpack 会对各种模块进行语法分析,并作转换编译

另外,针对异步模块:webpack 实现模块的异步加载方式有点像 jsonp 的流程

遇到异步模块时,使用 __webpack_require__.e 函数去把异步代码加载进来。该函数会在 html 的 head 中动态增加 script 标签,src 指向指定的异步模块存放的文件。

加载的异步模块文件会执行 webpackJsonpCallback 函数,把异步模块加载到主文件中。

所以后续可以像同步模块一样,直接使用 __webpack_require__("**.js") 加载异步模块。

# 文件监听原理

发现源码发生改变时,自动重新构建出新的输出文件

缺点:每次需要手动刷新浏览器

原理:轮询判断文件的最后编辑时间是否变化,初次构建是把文件的修改时间储存起来,下次有修改时会和上次的修改时间做比对,发现不一致时,不会立即告诉监听者,而是缓存起来,等 aggregateTimeout 后,把变更列表一起构建,并生成到 bundle 文件夹

module.export = {
  // 默认为 false,也就是不开启
  watch: true,
  watchOptions: {
    // 默认为空,不监听的文件夹或文件,支持正则匹配
    ignore: /node_modules/,
    // 监听到变化发生后会等 300ms,再去执行,默认 300ms
    aggregateTimeout: 300,
    // 判断文件是否发生变化的轮询在1s内发生的次数
    poll: 1000
  }
}

# webpack 热更新原理

Webpack 的热更新也称热替换(Hot Module Replacement),这个机制可以做到不用刷新浏览器而将变更的模块替换掉旧的模块。

相对于手动刷新页面,HMR 的优势在于可以保存应用的状态,提高开发效率。

  1. webpack 构建的项目,分为 server 端和 client 端,项目启动时,双方会保持一个 socket 连接用于通话
  2. 当本地资源发生变化时,server 向浏览器发送新资源的 hash 值,浏览器调用 reloadApp 方法,检查是否有变化,有差异会向 server 发起 ajax 获取更改内容(文件列表、hash),这样浏览器继续借助这些信息向 server 端发起请求,通过 jsonp 的方式获取 chunk 的增量更新

# 文件指纹

文件指纹是指打包后输出的文件后缀

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
  • Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

js 文件指纹设置:

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name][chunkhash:8].js',
    path: __dirname + '/dist'
  }
}

css 文件指纹设置:

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name][contenthash:8].css'
    }),
    new ExtractTextPlugin('[name][contenthash].css')
  ]
}

图片文件指纹设置 file-loader 或 url-loader 的 name,使用 hash

# webpack 如何保证各个 loader 的执行顺序

通过配置 module.rules 中的 enforce 字段,将 loader 分为 preLoaderpostLoaderloader 三种,执行顺序为 pre -> loader -> inline -> post。

pre 代表所有正常 loader 之前执行,post 是所有 loader 之后执行,inline 官方不推荐使用

# 代码分割的意义

用可接受的服务器性能压力增加来换取更好的用户体验

  • 复用的代码抽离到公共模块,解决代码冗余
  • 公共模块再按照使用的页面多少(或其他思路)进一步拆分,用来减小文件体积,顺便优化首屏速度

拆分原则:

如何拆分因项目而异,但是普遍使用的原则:

  • 业务代码和第三方库代码分离打包,实现代码分割
  • 业务代码中的公共业务模块提取打包到一个模块
  • 首屏相关模块单独打包