从 esm cjs 到 webpack vite

111 阅读6分钟

从 esm cjs 到 webpack vite

esm cjs

两者的导入导出

esm

// 从模块中按名称导入指定的导出
import { somePackage } from "path";

// 导入模块的默认导出
import defaultPackage from "path";

// 将模块的所有导出作为一个对象导入
import * as allFromPackage from "path";

// 导出一个命名变量
export const package = "value";

// 导出默认值
export default defaultPackage;

cjs

// 导入整个模块
const singlePackage = require("path");

// 从模块中解构导入指定的属性
const { somePackage } = require("path");

// 导出单个值
module.exports = singlePackage;

// 导出多个值
module.exports = {
  somePackage,
  anotherPackage: "anotherValue", // 补充一个额外的导出示例
};

// 动态导出
exports.somePackage = "value";
exports.anotherPackage = "anotherValue";

补充说明

ESM 特性
  1. 静态加载import 是静态的,编译时就能确定依赖关系,支持 Tree Shaking。
  2. 默认导出与命名导出:可以同时使用 export defaultexport
  3. 模块作用域:每个模块都有自己的作用域,避免全局变量污染。
CJS 特性
  1. 动态加载require 是动态的,可以在运行时加载模块。
  2. 单一导出与多重导出:通过 module.exportsexports 实现。
  3. 兼容性:CJS 是 Node.js 的默认模块系统,适用于服务器端。
两者的主要区别
特性ESMCJS
导入语法importrequire
导出语法export / export defaultmodule.exports / exports
加载方式静态加载动态加载
支持环境浏览器、Node.js(12+ 原生支持)Node.js 原生支持
Tree Shaking支持不支持

webpack vite

webpack

入口起点
  • 单个入口:entry: string | [string]

  • 对象语法:entry: { string | [string] } | {} 描述入口对象:

    • dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载。
    • filename: 指定要输出的文件名称。
    • import: 启动时需加载的模块。
    • library: 指定 library 选项,为当前 entry 构建一个 library。
    • runtime: runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。
    • publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。

    runtime 和 dependOn 不应在同一个入口上同时使用,所以如下配置无效,并且会抛出错误 另外 dependOn 不能是循环引用的

  • 常见场景

    • 分离 app 和 vendor 入口: 这样你就可以在 vendor.js 中存入未做修改的必要 library 或文件(例如 Bootstrap, jQuery, 图片等),然后将它们打包在一起成为单独的 chunk。内容哈希保持不变,这使浏览器可以独立地缓存它们,从而减少了加载时间。
    • 多页面应用程序: 在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 optimization.splitChunks 为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益
输出

output 属性的最低要求是,将它的值设置为一个对象,然后为将输出文件的文件名配置为一个 output.filename

module.exports = {
  output: {
    filename: "bundle.js",
  },
};

多入口起点 如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions) 来确保每个文件具有唯一的名称。

module.exports = {
  entry: {
    app: "./src/app.js",
    search: "./src/search.js",
  },
  output: {
    filename: "[name].js",
    path: __dirname + "/dist",
  },
};
loader

将 js、json 以外的文件进行转换,按需进行,这里只说 js、json 的原因是 webpack 只能识别 js、json

比如:

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: "css-loader" },
      { test: /\.ts$/, use: "ts-loader" },
    ],
  },
};

使用方式有两种:配置方式(推荐)、内联方式 内联方式需要在每个 import 时显式使用,较为复杂,不做考虑 配置方式就像上面的例子一样

特性

  • loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推
  • loader 可以是同步的,也可以是异步的
  • loader 运行在 Node.js 中,并且能够执行任何操作
  • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块
  • 插件(plugin)可以为 loader 带来更多特性
  • loader 能够产生额外的任意文件
plugin

插件目的在于解决 loader 无法实现的其他事

webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。

const pluginName = "ConsoleLogOnBuildWebpackPlugin";

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log("webpack 构建正在启动!");
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

配置方式(不考虑 node):

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件

module.exports = {
  ...
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};
配置

webpack 遵循 cjs 模块规范

模块
  • import 语句
  • require 语句
  • define require (AMD)
  • css/sass/less @import 语句
  • stylesheet url 等
模块解析

resolver:帮助寻找模块绝对路径 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。

依赖图

当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。 从 入口 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载

HMR

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面

vite

首先总结优点:

  • 冷启动 vite 会将模块区分为依赖和源码两类
    • 依赖:使用 esbuild 与构建依赖;esbulid 使用 go 编写,速度快
    • 源码:以原生 esm 方式提供源码,让浏览器接管打包程序的部分工作,vite 只需要在浏览器请求源码时进行转换并按需提供源码即可
  • 热更新 实际上也就是 HMR 同时利用 HTTP 投加速真个页面重新加载,源码请求会更具 304 进行协商缓存,依赖模块则会强缓存

使用上实际上和 webpack 大差不差,不做特别补充

环境变量

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量

.env 文件

Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:

.env                # 所有情况下都会加载
.env.local          # 所有情况下都会加载,但会被 git 忽略
.env.[mode]         # 只在指定模式下加载
.env.[mode].local   # 只在指定模式下加载,但会被 git 忽略

总结

这里涉及到个人理解了

首先就是为什么是这个标题,不难发现,webpack 和 vite 对于依赖的解析都是遵循 cjs、esm,同时这也是 vite 所谓冷启动的原因

这个笔记的很多内容实际上都是直接从官网的 cv,在我学习的初期是很抗拒阅读的,当时对于这些的理解也很浅薄,只是知道了 vite 优点是什么,但没有深入,所以进行了一定总结和摘录