工具·

webpack和vite

学习webpack和vite构建工具,优化项目

webpack

什么是webpack?

用于构建 JavaScript 应用程序的静态模块打包器,将各类型资源进行打包

webpack 基本概念

  • entry:入口
  • output: 出口
  • module: 模块,Webpack 会从配置的 entry 开始递归找出所有依赖的模块。
  • chunk: 代码块,打包过程中的中间产物,用于代码合并和分割
  • bundle: 输出文件,根据模块之间的依赖关系生成的最终输出文件
  • loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • plugin:扩展插件,在构建过程中,通过不同时机暴露出来的事件,处理不同的事情,解决loader无法实现的内容

原理

1.运行流程:

  • 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数。执行配置插件实例化 new Plugin()
  • 开始编译:通过初始化参数初始化compiler对象,加载所有配置的插件plugin,执行对象的run方法,开始执行编译
  • 确定入口:根据配置的entry找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的loader对模块进行转换,再找出所有依赖模块,通过递归编译入口所有依赖文件
  • 完成编译模块:得到编译后的内容和依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:根据output出口文件,把文件写入到文件系统

总结:初始化所有参数,执行插件实例化,初始化compiler对象,加载插件,执行run方法开始编译。基于入口entry文件递归遍历依赖文件,通过loader编译所有文件。处理模块合并、分割成Chunk。基于出口文件output输出

2.热更新HMR

作用:代码修改,浏览器页面跟着修改

原理:

  • 使用 webpack-dev-server (后面简称 WDS)托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码
  • 浏览器加载页面后,与 WDS 建立 WebSocket 连接
  • Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash 事件
  • 浏览器接收到 hash 事件后,请求 manifest 资源文件,确认增量变更范围
  • 浏览器加载发生变更的增量模块
  • Webpack 运行时触发变更模块的 module.hot.accept 回调,执行代码变更逻辑

总结:基于webpack-dev-server托管静态资源,当webpack发现文件变化后,通过websocket发送hash事件,通知浏览器执行变更

开启热更新:

// webpack.config.js 
module.exports = { 
    // ... 
    devServer: {
        // 必须设置 devServer.hot = true,启动 HMR 功能 
        hot: true 
    } 
};

3.Tree shaking

作用:移除未使用的代码

原理:

  • make阶段:收集模块导出变量并记录到模块依赖关系图中
  • seal阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
  • 构建阶段:利用Terser将没有被用到的导出语句删除

总结:通过遍历依赖关系图,标记未使用的模块,在构建时删除

Loader

Loader 的职责

以处理 SCSS 文件为例:

  1. SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  2. 把 sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  3. 把 css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;
module.exports = {
  module: {
    rules: [
      {
        // 增加对 SCSS 文件的支持
        test: /.scss$/,
        // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
        use: [
          'style-loader',
          {
            loader:'css-loader',
            // 给 css-loader 传入配置项
            options:{
              minimize:true, 
            }
          },
          'sass-loader'],
      },
    ]
  },
};

通过loader转换,将无法直接运用的代码,转化成可以运行的代码。保持每个loader职责单一

Loader的Demo

Loader API

处理source源文件内容,并通过return输出一个编译后的文件

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

Plugin

Plugin API

事件Compiler 和 Compilation

定义

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

区别: Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

Plugin的Demo

读取输出资源、代码块、模块及其依赖,并且可以修改输出资源的内容。代码如下:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一个代码块
        // 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
        chunk.forEachModule(function (module) {
          // module 代表一个模块
          // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
        // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
        // 该 Chunk 就会生成 .js 和 .css 两个文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放当前所有即将输出的资源
          // 调用一个输出资源的 source() 方法能获取到输出资源的内容
          let source = compilation.assets[filename].source();
        });
      });

      // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
      callback();
    })
  }
}

实践优化

1.缩小文件搜索范围

优化loader配置

  • test 正确书写正则匹配
  • cacheDirectory 开启缓存
  • include 匹配需要编译的文件
  • exclude 过滤编译的文件

注:不同loader开启缓存配置不同

module.exports = {
  module: {
    rules: [
      {
        // 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能
        test: /.js$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
    ]
  },
};

优化 resolve.alias 配置

配置项通过别名来把原导入路径映射成一个新的导入路径

module.exports = {
  resolve: {
    // 减少耗时的递归解析操作
    alias: {
      '@': path.resolve(__dirname, './src'),
    }
  },
};

2.压缩代码

js

  • UglifyJsPlugin:通过封装 UglifyJS 实现压缩。
  • ParallelUglifyPlugin:多进程并行处理压缩

css

3.代码分割

  • SplitChunksPlugin: 公共模块抽取

4.CDN加速

将静态文件上传到服务器上配置图片路径,通过配置publicPath设置前缀路径

5.Tree Shaking

保留ESModule,修改 .babelrc 文件为如下:

{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ]
}

6.按需加载

分割代码的功能去实现按需加载

/* webpackChunkName: "名称" */

举例:

  • 网页首次加载时只加载 main.js 文件,网页会展示一个按钮,main.js 文件中只包含监听按钮事件和加载按需加载的代码。
  • 当按钮被点击时才去加载被分割出去的 show.js 文件,加载成功后再执行 show.js 里的函数。
window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});
module.exports = function (content) {
  window.alert('Hello ' + content);
};

Webpack,配置如下:

module.exports = {
  // JS 执行入口文件
  entry: {
    main: './main.js',
  },
  output: {
    // 为从 entry 中配置生成的 Chunk 配置输出文件的名称
    filename: '[name].js',
    // 为动态加载的 Chunk 配置输出文件的名称
    chunkFilename: '[name].js',
  }
};

常用loader

1.加载文件
2.编译模版
3.转换脚本语言
4.转换样式文件
5.检查代码
6.其它

常用plugin

1.用于修改行为
2.用于优化
3.其它

Vite

介绍

一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  • 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能
  • 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

特点

  • 快速的冷启动:esbuild 预构建
  • 即时的模块热更新:基于ESMHMR,同时利用浏览器缓存策略提升速度
  • 按需加载:利用浏览器ESM支持,实现按需加载

依赖解析和预构建

依赖解析

  1. 预构建 它们可以提高页面加载速度,并将 CommonJS / UMD 转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包器都要快得多。
  2. 重写导入为合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便浏览器能够正确导入它们。
  3. 解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable 强缓存,以提高在开发时的页面重载性能。

预构建目的

  1. CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
  2. 性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能

热更新

一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或清除应用程序状态。