webpack知识点

388 阅读10分钟

loader是什么?

Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为Webpack只认识JavaScript,所以Loader就成了翻译官,对其他类型的资源进行转译预处理工作。

常见的loader:

  • file-loader 使得我们可以在JS文件中引入png\jpg等图片资源
  • url-loader 跟file-loader类似;唯一不同的是在于用户可以设置一个文件大小的阈值,当大于阈值时跟file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码。
  • style-loader css-loader 其中css-loader处理js中import require() @import/url 引入的内容;style-loader 负责把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML里。
  • sass-loader 把scss转成css
  • less-loader 把less转成css
  • babel-loader 中间桥梁,通过调用babel/core中的api来告诉webpack要如何处理 js。把 ES6 转换成 ES5

Plugin

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展Webpack的功能,在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。

常用的plugin:

  • html-webpack-plugin 自动生成HTML5文件,并引入webpack打包好的 js 等文件。简化 HTML 文件创建

  • clean-webpack-plugin 用于打包前先把dist文件夹清空

  • hot-module-replacement-plugin 模块热替换插件,即HMR,webpack4 自带插件,无需安装,在开发模式下配合 devServer 使用

  • mini-css-extract-plugin 将CSS提取到单独的文件中。

  • PurgecssPlugin 可以去除未使用的 css, 一般与glob、glob-all 配合使用。

  • optimize-css-assets-webpack-plugin 用于 CSS 压缩

  • split-chunks-plugin 用于提取 js 中公共代码。

  • ignore-plugin:忽略部分文件

  • webpack-bundle-analyzer 可视化 webpack 输出文件的体积

它可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作。

  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度

webpack--HMR的使用

1 当我们使用webpack来构建项目的同时开启了热更新,这些的好处就是每当我们编写代码,webpack都会帮我们 更新代码内容,浏览器也会更新

2 有没有一种方法,当我们代码发生改变的同时,页面不刷新呢?那就要用到HMR了

3 用法

3.1 例如新建一个a模块并导出 export defalut "这天气很热~~"

3.2 在使用到这模块的地方使用module.hot.accept

module.hot.accept("./a.js",function(){
    // 第一个参数为你要监听的模块  这样就实现当你a模块更新的代码的时候,浏览器不会刷新  
    let str =require('./a.js)
}) 

Loader和Plugin的区别?

(就知道你会问这个,我用手掩盖着嘴角的微笑)

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

Plugin 就是插件,扩展webpack的功能,但是 plugin 是作用于webpack本身上的。而且plugin不仅只局限在打包,资源的加载上,它的功能要更加丰富。从打包优化和压缩,提取公共JS文件,忽略指定打包文件,到重新定义环境变量,功能强大到可以用来处理各种各样的任务

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

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

webpack热更新原理

webpack本身命令行并不支持HMR,我们可以使用 webpack-dev-server 配合 HotModuleReplacementPlugin 开启HMR:

const webpack = require('webpack');
module.exports = {
	plugins: [
    	new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
    	hot: true
    }
}

在本地开发环境下,浏览器是客户端,webpack-dev-server (WDS) 相当于我们的本地服务器。HMR 的核心就是客户端(浏览器)从本地服务端拉取更新后的资源(准确地说,HMR 拉取的不是整个资源文件,而是 chunk diff, 即chunk需要更新的部分。

实际上,WDS 与浏览器之间维护了一个 websocket,

1、当本地资源发生变化时,WDS 会向浏览器推送更新事件,并带上这次构建的 hash,让浏览器与上一次资源进行对比。

2、浏览器根据 hash 值对比文件是否发生变化。如果有了差别,浏览器就会向 WDS 发起一个请求,来获取文件的列表,即哪些模块有了改动。浏览器就可以再借助这些信息继续向WDS获取chunk的增量更新

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 转换成文件,输出到文件系统中。

源码分析文章:juejin.cn/post/684490…

module chunk bundle

  • module: 各个源文件,webpack 中一切皆模块
  • chunk:多个模块合并成的,可以设置chunk的地方如 entry、import()、splitChunks
  • bundle: 最终输出的文件

source map是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。

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

线上环境处理方案:sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

文件监听原理呢?

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

Webpack开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 --watch 参数
  • 在配置 webpack.config.js 中设置 watch:true

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

原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。

module.export = {    
    // 默认false,也就是不开启    
    watch: true,    
    // 只有开启监听模式时,
    watchOptions才有意义    
    watchOptions: {        
        // 默认为空,不监听的文件或者文件夹,支持正则匹配        
        ignored: /node_modules/,        
        // 监听到变化发生后会等300ms再去执行,默认300ms        
        aggregateTimeout:300,        
        // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次        
        poll:1000    
}}

如何优化 Webpack 的构建速度?

(这个问题就像能不能说一说**「从URL输入到页面显示发生了什么」**一样)

  • 多进程/多实例构建:HappyPack(不维护了)、thread-loader

  • 压缩代码

    • 多进程并行压缩
      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
      • terser-webpack-plugin 开启 parallel 参数
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  • 图片压缩

    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  • 缩小打包作用域

    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  • 提取页面公共资源

    • 基础包分离:
      • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • 充分利用缓存提升二次构建速度

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin

优化打包效率

  • 优化 babel-loader: 开启缓存,明确打包范围
  • IgnorePlugin: 避免引用无用模块
  • noParse 避免重复打包
  • happyPack 多进程打包
  • ParallelUglyPlugin 多进程压缩 JS
  • 自动刷新
  • 自动更新(新代码生成,网页不会刷新,状态不会丢失):webpack-dev-server 和 HotModuleReplacementPlugin
  • DllPlugin:同一个版本只构建一次,不用每次都重新构建。

优化产出代码

  • 小图片通过 url-loader 转为 base64 编码

  • bundle 加 hash 命中缓存(contentHash)

  • 懒加载

  • 提取公共代码(splitChunks)

  • 使用 CND 加速

  • 使用 production 模式

  • gzip 压缩 (CompressionWebpackPlugin)

代码分割的本质是什么?有什么意义呢?

代码分割的本质其实就是在源代码直接上线打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

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

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。

(Easy peezy right)

聊一聊Babel原理吧

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:

  • 解析:将代码转换成 AST
    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
    • Taro就是利用 babel 完成的小程序语法转换
  • 生成:以新的 AST 为基础生成代码