Skip to content

webpack的构建流程?

  1. 初始化:
    1. 从配置文件和 Shell 语句中读取与合并配置参数
    2. 初始化Compiler对象,遍历配置中的plugins集合,执行插件的apply方法
      1. apply方法其实就是插件的初始化,注册插件到Webpack的各个生命周期钩子函数上
  2. 编译阶段:
    1. 根据配置中的entry找出所有的入口模块(文件),模块ID就是文件的路径
    2. 从entry文件开始,调用所有配置的 Loader 将模块转译为webpack能识别的JS代码,之后转为AST结构,遍历AST从中找出该模块依赖的模块;
    3. 之后递归遍历所有依赖模块,找出依赖的依赖,直至遍历完所有项目资源后,构建出完整的模块依赖关系图
  3. 输出资源
    1. 根据入口和模块之间的依赖关系,将编译完成的模块组合成一个个代码块 chunk,将多个 chunk 合并为一个文件,过程中可能会进行代码拆分和文件合并
    2. 根据 output 参数,确定输出的路径和文件名,把文件内容写入到文件系统

简单来说

  • 初始化:合并参数实例化 Compiler注册Plugin
  • 编译:从 Entry出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

当Webpack调用 apply 方法时,插件通常会做以下两个工作:

  1. 定义一个钩子函数(hook)用来做一些特定的工作。
  2. 把这个钩子函数注册到Webpack的具体编译阶段。

compiler.hooks.emit.tap的作用就是把一个回调函数注册到对应 emit 阶段,当Webpack编译到这个阶段时,就会执行这个钩子函数。

typescript
class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // 在 emit 阶段做一些事情...
    });
  }
}

有哪些常见loader、plugin

  • style-loader
  • css-loader
  • sass-loader
  • vue-loader

  • html-webpack-plugin
  • terser-webpack-plugin
  • speed-measure-webpack-plugin
  • webpack-bundle-analyzer

Loader和 Plugin的区别?

Loader 本质就是一个函数,这个函数接收源文件的内容作为参数,对这个内容进行处理,然后返回一个有效的 JavaScript 模块。 Loader 就像一个“翻译员”,将某种语言(类型的文件)翻译(转换)成 JavaScript,从而让 webpack 能够识别并且处理。

typescript
// loader
module.exports = function(source) {
  console.log(source);
  return source;
};

Plugin 本质上是一个拥有 apply 方法的类; 基于Tapable的事件流机制,Plugin 可以让我们在 webpack 的生命周期的特定时刻插入一些自定义行为,

typescript
class MyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('My Plugin', (stats) => {
      console.log('MyPlugin: done');
    });
  }
}

文件指纹是什么?怎么用的

文件指纹常用于处理静态资源的缓存问题。每当文件内容改变时,文件指纹也会随之改变。文件指纹通常是通过对文件内容进行哈希(Hashing)计算得来的

  1. [hash]基于每次项目构建的哈希值,当项目文件有任何改动,整个项目构建的哈希值就会变化。
  2. [chunkhash]基于每个 chunk 的内容生成的哈希值,只有所属 chunk 内容改变时,哈希值才会变化,适合用在生产环境对 js 文件进行指纹设置。
  3. [contenthash]根据文件内容生成的哈希值,适合用在生产环境对 css 文件进行指纹设置。

JS文件指纹设置

JS ==> chunkhash

typescript
module.exports = {
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  }
};

CSS文件指纹位置

CSS ==> contenthash

首先需要使用 MiniCssExtractPlugin 插件来提取 CSS 为单独的文件,然后在设置 filename 时使用 [contenthash]:

typescript
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // CSS文件指纹
      filename: '[name].[contenthash].css'
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  }
};

Compiler 和 Compilation 的区别

Compiler

一个Webpack配置文件对应一个Compiler实例 Compiler 对象表示了Webpack从启动到关闭的整个生命周期。在编译过程中,Compiler 会统一管理所有的配置,包括但不限于选项(options)、loader、plugin等

Compilation

Compilation 对象表示了一次新的编译过程。 在开发模式下,每当文件发生变化时,Webpack会创建一个新的 Compilation 实例重新编译代码。


  • Compiler 是全局唯一的,代表了整个Webpack生命周期
  • 而 Compilation 则代表了一次单独的编译过程。所以在项目的构建过程中,Compiler 只会有一个,而 Compilation 会有多个。

webpack的 热更新(HMR)原理

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

流程==>

  1. 启动DevServer 使用webpack-dev-server(WDS)启动应用托管静态资源,并且DevServer会向html注入一段处理HMR逻辑的客户端代码。
  2. 建立连接:浏览器加载页面后,与 WDS 建立 WebSocket 连接
  3. **监听文件变化:**Webpack 监听到文件变化后,增量构建发生变更的模块,打包成一个或多个热更新Chunk
  4. **发送更新:**模块构建好后,WDS会通过之前建立的WebSocket连接向客户端发送包含本次构建时的hash,让客户端与上一次资源进行对比
  5. 加载更新: 客户端收到hash事件,对比差异后,会向WDS发送Ajax请求,去下载发生变更的增量模块。
  6. 热替换:Webpack 运行时触发变更模块的 module.hot.accept 回调,执行代码变更逻辑;

image.png

Babel的原理

Babel 是一个广泛使用的 JavaScript 编译器,作用是把开发者写的最新标准的 JavaScript 代码转化为旧版本的 JavaScript 语法,从而可以在当前浏览器或者旧版本的 NodeJs 环境中运行。Babel 大概的工作原理如下:


Babel 的编译过程主要包含三个阶段:解析(Parsing)、转化(Transforming)和生成(Generating)。


解析阶段: 在解析阶段,Babel 把源码转化为抽象语法树(Abstract Syntax Tree,AST)。 转化阶段: 这个阶段是 Babel 的核心阶段,它会遍历抽象语法树上所有的节点,并应用一系列的插件(如 ES6 语法插件、JSX语法插件等)逐个对节点进行转化或者替换。比如说,如果一段代码中使用了箭头函数,Babel 就会在这个阶段将箭头函数的节点替换为函数表达式的节点。 生成阶段: 在这个阶段,Babel 会把经过转化的抽象语法树重新生成为 JavaScript 代码。这一步也叫做代码生成。这个过程中,Babel 会尽可能地保留源代码中的格式,包括空格、逗号等,如果无法保留,就会使用默认的格式。

是否写过Plugin?简单描述一下编写Plugin的思路?

首先我们使用的时候,实例化了一个插件对象(new Plugin)**,**所以要编写的插件是需要定义成一个类的,并且它的原型上需要定义一个apply方法。 apply方法会在插件安装的时候执行一次,相当于事件绑定一样,监听特定的时机让我们可以做一些自定义行为。

typescript
class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* stats is passed as an argument when done hook is tapped.  */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

然后,开发插件时最重要的两个资源是compilercompilation对象。它们是 Plugin 和 Webpack 之间的桥梁。

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件

  • 我们在apply方法中,指定要用到的事件钩子,然后自定义一些操作。那如果是异步操作,比如tapAsync,在功能完成后调用 webpack 提供的回调
typescript
// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // Specify the event hook to attach to
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log(
          'Here’s the `compilation` object which represents a single build of assets:',
          compilation
        );

        // Manipulate the build using the plugin API provided by webpack
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

webpack性能优化

使用webpack-bundle-analyzer可以审查打包后的体积分布,进而进行相应的体积优化

优化构建速度

  1. HappyPack/ thread-loader多进程打包,可以大大提高构建的速度,
  2. 优化loader配置,**cache-loader**(w4)
  3. 启用持久化缓存,提高增量构建的速度
typescript
// webpack5 
module.exports = {
  // 其他配置项...
  cache: {
    type: 'filesystem', // 'memory' for in-memory caching
  }
}
  1. 缩小打包作用域exclude&include合理配置这两个属性,大大提高构建速度
  2. 开发环境,开启热更新HMR
  3. 使用高版本的Webpack 和 Node.js

减少打包体积

  1. CSS - css-minimizer-webpack-plugin 代码压缩
  2. JS - terser-webpack-plugin JS代码压缩
  3. 使用 Tree-shaking 剔除无用代码(webpack5默认开启)
  4. 组件库按需引入,
  5. 开启 Gzip 压缩
  6. 使用dayjs替换moment.js
  7. 使用Scope Hoisting(w5默认启用)(记得禁用Babel的模块编译)
  8. 小图使用 base64 编码减少体积
  9. 图片压缩 image-minimizer-webpack-plugin(无损)

优化加载性能,提升(优化)用户体验

  1. 按需加载(懒加载)配置
  2. 使用魔法注释对某些模块开启prefetchpreload
typescript
// 预加载
import(/* webpackPreload: true */ 'LazyComponent');
// 预获取 (下一个页面,页面空闲的时候获取)
import(/* webpackPrefetch: true */ 'LazyComponent');
  1. Code Splitting:将代码拆分成多个块(chunks)以减少初始加载时间和提高页面加载速度。
typescript
// webpack.config.js
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      // 指定最小大小,小于此尺寸的模块,将不会被拆分
      minSize: 30000,
      // 最大尺寸,用于限制合成块的最大尺寸。优化以达到更好的缓存效果
      maxSize: 0,
      // 最小 chunk 引用次数
      minChunks: 1,
      // 最大异步请求 chunks 数,设置后并行加载的最大数量将会更小
      maxAsyncRequests: 5,
      // 最大初始化加载 chunk 数
      maxInitialRequests: 3,
      // 分割块之间彼此连接的字符,例如 vendors~main.js
      automaticNameDelimiter: '~',
      // 打包策略
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          // 如果当前块包含已经从主 bundle 中拆分出来的模块,则重用该模块,不会再重新生成一个新的模块
          reuseExistingChunk: true
        }
      }
    }
  }
  // ...
}
  1. 开启 Gzip 压缩
  2. 基础依赖使用CDN引入externals配置
tsx
module.exports = {
  // ...
  externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter'
  },
}
// 从HTML标签中引入这些CDN库
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router/dist/vue-router.js"></script>
  1. 开启文件名 hash filename: 'js/chunk-[contenthash].js'
    1. 改过的文件需要更新hash值,而没改过的文件依然保持原本的hash值,这样才能保证在上线后,浏览器访问时没有改变的文件会命中缓存,从而达到性能优化的目的

提高首页加载速度,可以怎么优化

  1. 使用资源压缩:使用 gzip对资源进行压缩,可以有效减少资源的体积,缩短网络传输时间。
  2. 压缩和优化图片:对于大尺寸、大体积的图片,可以考虑使用 WebP 或者压缩工具进行优化。
  3. 利用 CDN:使用内容分发网络(Content Delivery Network,CDN)能够加速资源的加载速度,特别是对地理位置远离服务器的用户能够提供好的访问体验。
  4. 代码分割 Code Splitting:对代码进行分割,只加载当前页面真正需要的脚本和样式,可以明显提高网页的初次加载速度。
  5. 服务端配置开启HTTP/2:相较于 HTTP/1,HTTP/2 支持多路复用,同时处理多个请求,能够显著减少网页加载时间。
  6. 预加载/预读取 Preload/Prefetch:可用于某些未来会用到的资源,使其提前加载,达到优化的效果。
  7. Tree Shaking:这是一种只打包引用到的模块的方法,未引用到的模块不会被包含在最终的 bundle 中,可以减少JavaScript的体积。
  8. 缓存优化:你可以使用 cache-control、ETag、last-modified 等HTTP缓存控制头来优化缓存,避免不必要的请求。
  9. Lazy Loading:懒加载是一种只有当资源需要运行或者需要显示在屏幕上时才加载的策略。对于图片、组件、路由等都可以进行懒加载。
  10. 使用骨架屏(Skeleton Screen):骨架屏是一种优化首屏白屏的 UI 设计模式,它先显示一个与最终页面类似的空白版本,随后通过懒加载的方式,当页面内容渲染完成后再替换骨架屏。这样用户会有内容正在加载的感知。
  11. 服务端渲染 (SSR):服务端渲染可以使得用户在首次请求时就可以获取到完整的 HTML 页面,避免白屏的出现。对于 JavaScript-heavy 的应用(如React、Vue等),服务端渲染是一种有效的优化策略。
  12. Lighthouse、Webpack Bundle Analyzer等工具进行性能分析和优化

Vite为什么比Webpack快?

  1. 快速的冷启动: Vite 使用 esbuild(go) 预构建依赖, 并且以原生 ESM 方式提供源码,让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。
  2. 高效的热更新: Vite的热更新HMR 是在原生 ESM 上执行的,无论应用大小如何,HMR 始终能保持快速更新。并且Vite增加了缓存策略:源码模块使用304协商缓存,依赖模块使用cache-control强缓存
  3. 基于Rollup打包:Rollup 针对 ESM 的 tree-shaking 操作更加高效,可以清除更多无效的冗余代码。

Vite的HRM热更新和webpack HMR的有什么不同?

  1. 按需编译:Webpack 在启动时需要对整个应用应用进行一次完整的编译,而Vite只对请求的模块进行编译,而不需要像传统的打包工具那样等待整个项目编译完成
  2. 基于 ESM 的 HMR:Vite 的更新则是基于浏览器原生的 ESM ,浏览器可以直接请求和缓存更新的文件,无需通过 webpack 的运行时进行处理,从而提高更新的效率
  3. 响应速度:热更新实质上是一次新的 HTTP 请求,而 Vite 在发送这个请求的时候使用了 HTTP/2,避免了 HTTP/1.1 的队头阻塞(Head-of-line Blocking)问题,加快了更新。

Vue-loader的实现原理

Vue-loader 是 webpack 的一个 loader 转换器,是用来解析和转换 .vue 文件的工具。

  1. 解析vue-loader 通过 vue-template-compiler 解析.vue文件,并返回一个描述该 .vue 文件结构的JS对象。这个对象中包含 template、script 和 styles,以及它们的内容,和一些其他的元信息。
typescript
{
  template: {
    content: '<div class="demo">{{ msg }}</div>', // template内容
    attrs: { lang: 'html' } // template的属性
  },
  script: {
    content: 'export default { data() { return { msg: 'Hello world!' }; } }', // script内容
    attrs: { lang: 'js' } // script的属性
  },
  styles: [{
    content: '.demo { color: red; }', // style内容
    attrs: { lang: 'css', scoped: true },  // style的属性
  }], 
  customBlocks: [], // 其他自定义块
  errors: [] // 指示解析过程中出错的错误数组
}
  1. 转换为module

解析出的每一个部分(template、script 和 styles)都会转化为一个 JavaScript 的模块。方便 webpack 能够理解和处理这些代码。

  1. 对应 Loader 处理:
  • 对于 script,使用 babel-loader 或者 ts-loader 对其进行处理;
  • 对于 template,首先通过 vue-template-compiler 将模板转换为 JavaScript 渲染函数,然后交给 babel-loader 或者其它 JavaScript loader 进行处理;
  • 如果 styles 是 SCSS 或者 Less,会先通过对应的 loader 进行预编译,然后交给 css-loader。
  1. 热重载:

vue-loader 会注入一些热重载的代码,让修改后的组件可以在不刷新页面的情况下替换掉之前的组件。

  1. Scope CSS:

vue-loader 提供了 CSS 作用域的功能,就是通过为模板生成的元素添加特殊的属性,让这部分 CSS 仅作用于当前的组件。

vue-loader 的实现原理是利用 webpack loader 对 .vue 文件的各个部分进行逐个处理,然后使用 webpack 的强大功能将这些不同类型的资源整合到一起,制作成为一个前端项目需要的外部资源。