Vite: Esbuild的使用与其插件开发

news2024/11/24 10:39:48

概述

  • 作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如 依赖预编译 、 TS 语法转译 、 代码压缩 ) 让 Vite 获得了相当优异的性能,是 Vite 高性能的得力助手
  • 无论是在 Vite 的配置项还是源码实现中,都包含了不少 Esbuild 本身的基本概念和高阶用法

高性能的Esbuild

  • Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。获得这样超高的构建性能,主要原因可以概括为 4 点

1 )使用 Golang 开发

  • 构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。

2 ) 多核并行

  • 内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是
    得益于 Go 当中多线程共享内存的优势。

3 )从零造轮子

  • 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小
    到字符串的操作,保证极致的代码性能

4 )高效的内存利用

  • Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费

使用

  • 执行 pnpm init -y 新建一个项目, 然后通过如下的命令完成 Esbuild 的安装:
    • $ pnpm i esbuild
  • 使用 Esbuild 有 2 种方式,分别是 命令行调用和代码调用

1 )命令行调用

  • 命令行方式调用也是最简单的使用方式。我们先来写一些示例代码,新建 src/index.jsx 文件,内容如下

    // src/index.jsx
    import Server from "react-dom/server";
    
    let Greet = () => <h1>Hello, juejin!</h1>;
    console.log(Server.renderToString(<Greet />));
    
  • 注意安装一下所需的依赖,在终端执行如下的命令:

    • $ pnpm install react react-dom
  • 接着到 package.json 中添加 build 脚本:

    "scripts": {
    	 "build": "./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=dist/out.js"
    },
    
  • 现在,你可以在终端执行 $ pnpm run build,可以发现如下的日志信息

  • 说明我们已经成功通过命令行完成了 Esbuild 打包!但命令行的使用方式不够灵活,只能传入一些简单的命令行参数,稍微复杂的场景就不适用了,所以一般情况下我们还是会用代码调用的方式

2 ) 代码调用

  • Esbuild 对外暴露了一系列的 API,主要包括两类: Build API 和 Transform API ,我们可以在 Nodejs 代码中通过调用这些 API 来使用 Esbuild 的各种功能

项目打包——Build API

  • Build API 主要用来进行项目打包,包括 build 、 buildSync 和 serve 三个方法。
    首先我们来试着在 Node.js 中使用 build 方法。你可以在项目根目录新建 build.js 文
    件,内容如下:
const { build, buildSync, serve } = require("esbuild");
async function runBuild() {
 // 异步方法,返回一个 Promise
 const result = await build({
	 // ---- 如下是一些常见的配置 --- 
	 // 当前项目根目录
	 absWorkingDir: process.cwd(),
	 // 入口文件列表,为一个数组
	 entryPoints: ["./src/index.jsx"],
	 // 打包产物目录
	 outdir: "dist",
	 // 是否需要打包,一般设为 true
	 bundle: true,
	 // 模块格式,包括`esm`、`commonjs`和`iife`
	 format: "esm",
	 // 需要排除打包的依赖列表
	 external: [],
	 // 是否开启自动拆包
	 splitting: true,
	 // 是否生成 SourceMap 文件
	 sourcemap: true,
	 // 是否生成打包的元信息文件
	 metafile: true,
	 // 是否进行代码压缩
	 minify: false,
	 // 是否开启 watch 模式,在 watch 模式下代码变动则会触发重新打包
	 watch: false,
	 // 是否将产物写入磁盘
	 write: true,
	 // Esbuild 内置了一系列的 loader,包括 base64、binary、css、dataurl、file、js(x)、ts(x)、text
 	// 针对一些特殊的文件,调用不同的 loader 进行加载
	 loader: {
	 	'.png': 'base64',
	 }
 });
 console.log(result);
}
runBuild();
  • 随后,你在命令行执行 node build.js ,就能在控制台发现如下日志信息:
  • 以上就是 Esbuild 打包的元信息,这对我们编写插件扩展 Esbuild 能力非常有用。

  • 接着,我们再观察一下 dist 目录,发现打包产物和相应的 SourceMap 文件也已经成功写入磁盘:

  • 其实 buildSync 方法的使用几乎相同,如下代码所示:

    function runBuild() {
     // 同步方法
     const result = buildSync({
     // 省略一系列的配置
     });
     console.log(result);
    }
    runBuild();
    
  • 但我并不推荐大家使用 buildSync 这种同步的 API,它们会导致两方面不良后果。一方面容易使 Esbuild 在当前线程阻塞,丧失 并发任务处理 的优势。另一方面,Esbuild 所有插件中都不能使用任何异步操作,这给 插件开发 增加了限制

  • 因此更推荐使用 build 这个异步 API,它可以很好地避免上述问题。在项目打包方面,除了 build 和 buildSync ,Esbuild 还提供了另外一个比较强大的 API
    —— serve 。这个 API 有 3 个特点

    • 开启 serve 模式后,将在指定的端口和目录上搭建一个静态文件服务 ,这个服务器用原生 Go 语言实现,性能比 Nodejs 更高
    • 类似 webpack-dev-server,所有的产物文件都默认不会写到磁盘,而是放在内存中,通过请求服务来访问
    • 每次请求到来时,都会进行重新构建( rebuild ),永远返回新的产物
  • 值得注意的是,触发 rebuild 的条件并不是代码改动,而是新的请求到来

  • 现在,举一个例子

    // build.js
    const { build, buildSync, serve } = require("esbuild");
    function runBuild() {
     serve({
    	 port: 8000,
    	 // 静态资源目录
    	 servedir: './dist'
     }, {
    	 absWorkingDir: process.cwd(),
    	 entryPoints: ["./src/index.jsx"],
    	 bundle: true,
    	 format: "esm",
    	 splitting: true,
    	 sourcemap: true,
    	 ignoreAnnotations: true,
    	 metafile: true,
     }).then((server) => {
     	console.log("HTTP Server starts at port", server.port);
     });
    }
    runBuild();
    
  • 我们在浏览器访问 localhost:8000 可以看到 Esbuild 服务器返回的编译产物如下所示:

  • 后续每次在浏览器请求都会触发 Esbuild 重新构建,而每次重新构建都是一个增量构建的过程,耗时也会比首次构建少很多(一般能减少 70% 左右)。
  • Serve API 只适合在开发阶段使用,不适用于生产环境。

单文件转译——Transform API

  • 除了项目的打包功能之后,Esbuild 还专门提供了单文件编译的能力,即 Transform API ,与 Build API 类似,它也包含了同步和异步的两个方法,分别是 transformSync 和 transform 。下面,我们具体使用下这些方法。

  • 首先,在项目根目录新建 transform.js ,内容如下:

    // transform.js
    const { transform, transformSync } = require("esbuild");
    async function runTransform() {
     // 第一个参数是代码字符串,第二个参数为编译配置
     const content = await transform(
     	"const isNull = (str: string): boolean => str.length > 0;",
    	 {
    		 sourcemap: true,
    		 loader: "tsx",
    	 }
     );
     console.log(content);
    }
    runTransform();
    
  • transformSync 的用法类似,换成同步的调用方式即可

    function runTransform {
    	 const content = await transformSync(/* 参数和 transform 相同 */)
    	 console.log(content);
    }
    
  • 不过由于同步的 API 会使 Esbuild 丧失 并发任务处理 的优势( Build API 的部分已经分析过),我同样也不推荐大家使用 transformSync 。出于性能考虑,Vite 的底层实现也是采用 transform 这个异步的 API 进行 TS 及 JSX 的单文件转译的

Esbuild 插件开发

  • 在使用 Esbuild 的时候难免会遇到一些需要加上自定义插件的场景,并且 Vite 依赖预编译的实现中大量应用了 Esbuild 插件的逻辑。因此,插件开发是 Esbuild 中非常重要的内容,接下来,我们就一起来完成 Esbuild 的插件开发,带你掌握若干个关键的钩子使用。

1 ) 基本概念

  • 插件开发其实就是基于原有的体系结构中进行 扩展 和 自定义 。 Esbuild 插件也不例外,通过 Esbuild 插件我们可以扩展 Esbuild 原有的路径解析、模块加载等方面的能力,并在Esbuild 的构建过程中执行一系列自定义的逻辑。

  • Esbuild 插件结构被设计为一个对象,里面有 name 和 setup 两个属性, name 是插件的名称, setup 是一个函数,其中入参是一个 build 对象,这个对象上挂载了一些钩子可供我们自定义一些钩子函数逻辑。

2 ) 插件示例

  • 以下是一个简单的 Esbuild 插件示例:
    const envPlugin = {
    	 name: 'env',
    	 setup(build) {
    		 build.onResolve({ filter: /^env$/ }, args => ({
    			 path: args.path,
    			 namespace: 'env-ns',
    	 	 }))
    	 	 
    		 build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
    			 contents: JSON.stringify(process.env),
    			 loader: 'json',
    		 }))
    	  },
    }
    
    require('esbuild').build({
    	 entryPoints: ['src/index.jsx'],
    	 bundle: true,
    	 outfile: 'out.js',
    	 // 应用插件
    	 plugins: [envPlugin],
    }).catch(() => process.exit(1))
    

2 ) 使用插件后效果如下:

// 应用了 env 插件后,构建时将会被替换成 process.env 对象
import { PATH } from 'env'
console.log(`PATH is ${PATH}`)
  • 那么, build 对象上的各种钩子函数是如何使用的呢?

3 ) 钩子函数的使用

3.1 onResolve 钩子 和 onLoad 钩子

  • 在 Esbuild 插件中, onResolve 和 onload 是两个非常重要的钩子,分别控制路径解析和模块内容加载的过程

  • 首先,我们来说说上面插件示例中的两个钩子该如何使用

    build.onResolve({ filter: /^env$/ }, args => ({
    	 path: args.path,
    	 namespace: 'env-ns',
    }));
    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
    	 contents: JSON.stringify(process.env),
    	 loader: 'json',
    }));
    
  • 可以发现这两个钩子函数中都需要传入两个参数: Options 和 Callback

  • 先说说 Options 。它是一个对象,对于 onResolve 和 onload 都一样,包含 filter 和 namespace 两个属性,类型定义如下:

    interface Options {
     filter: RegExp;
     namespace?: string;
    }
    
    • filter 为必传参数,是一个正则表达式,它决定了要过滤出的特征文件。
    • 📢 注意: 插件中的 filter 正则是使用 Go 原生正则实现的,为了不使性能过于劣化,规则应该尽可能严格。同时它本身和 JS 的正则也有所区别,不支持前瞻(?
      <=)、后顾(?=)和反向引用(\1)这三种规则。
  • namespace 为选填参数,一般在 onResolve 钩子中的回调参数返回 namespace 属性作为标识,我们可以在 onLoad 钩子中通过 namespace 将模块过滤出来。如上述插件示例就在 onLoad 钩子通过 env-ns 这个 namespace 标识过滤出了要处理的 env 模块。

  • 除了 Options 参数,还有一个回调参数 Callback ,它的类型根据不同的钩子会有所不同。相比于 Options,Callback 函数入参和返回值的结构复杂得多,涉及很多属性。不过,我们也不需要看懂每个属性的细节,先了解一遍即可

  • 在 onResolve 钩子中函数参数和返回值梳理如下:

    build.onResolve({ filter: /^env$/ }, (args: onResolveArgs): onResolveResult => {
    	 // 模块路径
    	 console.log(args.path)
    	 // 父模块路径
    	 console.log(args.importer)
    	 // namespace 标识
    	 console.log(args.namespace)
    	 // 基准路径
    	 console.log(args.resolveDir)
    	 // 导入方式,如 import、require
    	 console.log(args.kind)
    	 // 额外绑定的插件数据
    	 console.log(args.pluginData)
    	 
    	 return {
    		 // 错误信息
    		 errors: [],
    		 // 是否需要 external
    		 external: false;
    		 // namespace 标识
    		 namespace: 'env-ns';
    		 // 模块路径
    		 path: args.path,
    		 // 额外绑定的插件数据
    		 pluginData: null,
    		 // 插件名称
    		 pluginName: 'xxx',
    		 // 设置为 false,如果模块没有被用到,模块代码将会在产物中会删除。否则不会这么做
    		 sideEffects: false,
    		 // 添加一些路径后缀,如`?xxx`
    		 suffix: '?xxx',
    		 // 警告信息
    		 warnings: [],
    		 // 仅仅在 Esbuild 开启 watch 模式下生效
    		 // 告诉 Esbuild 需要额外监听哪些文件/目录的变化
    		 watchDirs: [],
    		 watchFiles: []
    	 }
    }
    
  • 在 onLoad 钩子中函数参数和返回值梳理如下

    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, (args: OnLoadArgs): OnLoadResult => {
    	 // 模块路径
    	 console.log(args.path);
    	 // namespace 标识
    	 console.log(args.namespace);
    	 // 后缀信息
    	 console.log(args.suffix);
    	 // 额外的插件数据
    	 console.log(args.pluginData);
     
    	 return {
    		 // 模块具体内容
    		 contents: '省略内容',
    		 // 错误信息
    		 errors: [],
    		 // 指定 loader,如`js`、`ts`、`jsx`、`tsx`、`json`等等
    		 loader: 'json',
    		 // 额外的插件数据
    		 pluginData: null,
    		 // 插件名称
    		 pluginName: 'xxx',
    		 // 基准路径
    		 resolveDir: './dir',
    		 // 警告信息
    		 warnings: [],
    		 // 同上
    		 watchDirs: [],
    		 watchFiles: []
    	 }
    });
    

3.2 其他钩子

  • 在 build 对象中,除了 onResolve 和 onLoad ,还有 onStart 和 onEnd 两个钩子用来在构
    建开启和结束时执行一些自定义的逻辑,使用上比较简单,如下面的例子所示:
const examplePlugin = {
 name: 'example',
 setup(build) {
	 build.onStart(() => {
		 console.log('build started')
	 });
	 build.onEnd((buildResult) => {
		 if (buildResult.errors.length) {
			 return;
		 }
		 // 构建元信息
		 // 获取元信息后做一些自定义的事情,比如生成 HTML
		 console.log(buildResult.metafile)
 	})
 },
}
  • 在使用这些钩子的时候,有 2 点需要注意。

    • onStart 的执行时机是在每次 build 的时候,包括触发 watch 或者 serve 模式下的
      重新构建。
    • onEnd 钩子中如果要拿到 metafile ,必须将 Esbuild 的构建配置中 metafile 属性
      设为 true 。
  • 接下来我们进入插件实战,通过编写一些特定功能的插件来熟悉 Esbuild 插件的开发流程
    和技巧。

CDN 依赖拉取插件

  • Esbuild 原生不支持通过 HTTP 从 CDN 服务上拉取对应的第三方依赖资源,如下代码所示:
    // src/index.jsx
    // react-dom 的内容全部从 CDN 拉取
    // 这段代码目前是无法运行的
    import { render } from "https://cdn.skypack.dev/react-dom";
    import React from 'https://cdn.skypack.dev/react'
    let Greet = () => <h1>Hello, juejin!</h1>;
    render(<Greet />, document.getElementById("root"));
    
  • 示例代码中我们用到了 Skypack 这个提供 npm 第三方包 ESM 产物的 CDN 服务 ,我们
    可以通过 url 访问第三方包的资源,如下图所示:
  • 现在我们需要通过 Esbuild 插件来识别这样的 url 路径,然后从网络获取模块内容并让
    Esbuild 进行加载,甚至不再需要 npm install 安装依赖了,这看上去是不是很酷呢?

  • 顺便提一句,ESM CDN 作为面向未来的前端基础设施,对 Vite 的影响也至关重
    大,可以极大提升 Vite 在生产环境下的构建性能。

  • 我们先从最简单的版本开始写起:

    // http-import-plugin.js
    module.exports = () => ({
      name: "esbuild:http",
      setup(build) {
        let https = require("https");
        let http = require("http");
        // 1. 拦截 CDN 请求
        build.onResolve({
          filter: /^https?:\/\//
        }, (args) => ({
          path: args.path,
          namespace: "http-url",
        }));
    
        // 2. 通过 fetch 请求加载 CDN 资源
        build.onLoad({
          filter: /.*/,
          namespace: "http-url"
        }, async (args) => {
          let contents = await new Promise((resolve, reject) => {
            function fetch(url) {
              console.log(`Downloading: ${url}`);
              let lib = url.startsWith("https") ? https : http;
              let req = lib
                .get(url, (res) => {
                  if ([301, 302, 307].includes(res.statusCode)) {
                    // 重定向
                    fetch(new URL(res.headers.location, url)
                      .toString());
                    req.abort();
                  } else if (res.statusCode === 200) {
                    // 响应成功
                    let chunks = [];
                    res.on("data", (chunk) => chunks.push(chunk));
                    res.on("end", () => resolve(Buffer.concat(chunks)));
                  } else {
                    reject(
                      new Error(`GET ${url} failed: status ${res.statusCode}`)
                    );
                  }
                })
                .on("error", reject);
            }
            fetch(args.path);
          });
          return {
            contents
          };
        });
      },
    });
    
  • 然后我们新建 build.js 文件,内容如下:

    const { build } = require("esbuild");
    const httpImport = require("./http-import-plugin");
    async function runBuild() {
      build({
          absWorkingDir: process.cwd(),
          entryPoints: ["./src/index.jsx"],
          outdir: "dist",
          bundle: true,
          format: "esm",
          splitting: true,
          sourcemap: true,
          metafile: true,
          plugins: [httpImport()],
        })
        .then(() => {
          console.log("🚀 Build Finished!");
        });
    }
    runBuild();
    
  • 通过 node build.js 执行打包脚本,发现插件不能 work,抛出了这样一个错误:

  • 这是为什么呢?你可以回过头观察一下第三方包的响应内容:

    export * from '/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports/optimized/r
    export {default} from '/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports/opt
    
  • 进一步查看还有更多的模块内容:

  • 因此我们可以得出一个结论:除了要解析 react-dom 这种直接依赖的路径,还要解析它
    依赖的路径,也就是间接依赖的路径。那如何来实现这个效果呢?我们不妨加入这样一段 onResolve 钩子逻辑:

    // 拦截间接依赖的路径,并重写路径
    // tip: 间接依赖同样会被自动带上 `http-url`的 namespace
    build.onResolve({ filter: /.*/, namespace: "http-url" }, (args) => ({
     // 重写路径
     path: new URL(args.path, args.importer).toString(),
     namespace: "http-url",
    }));
    
  • 加了这段逻辑后,Esbuild 路径解析的流程如下:

  • 现在我们再次执行 node build.js ,发现依赖已经成功下载并打包了。

实现 HTML 构建插件

  • Esbuild 作为一个前端打包工具,本身并不具备 HTML 的构建能力。也就是说,当它把
    js/css 产物打包出来的时候,并不意味着前端的项目可以直接运行了,我们还需要一份对
    应的入口 HTML 文件。而这份 HTML 文件当然可以手写一个,但手写显得比较麻烦,尤
    其是产物名称带哈希值的时候,每次打包完都要替换路径。那么,我们能不能通过
    Esbuild 插件的方式来自动化地生成 HTML 呢?

  • 刚才我们说了,在 Esbuild 插件的 onEnd 钩子中可以拿到 metafile 对象的信息。那么,这个对象究竟什么样呢?

    {  
      "inputs": { /* 省略内容 */ },  
      "output": {  
        "dist/index.js": {  
          "imports": [],  
          "exports": [],  
          "entryPoint": "src/index.jsx",  
          "inputs": {  
            "http-url:https://cdn.skypack.dev/-/object-assign@v4.1.1-LbCnB3r2y2yFmhmiCfPn/dist=es": true, // 假设使用布尔值来表示引入  
            "http-url:https://cdn.skypack.dev/-/react@v17.0.1-yH0aYV1FOvoIPeKBbHxg/dist=es2019,mode=imports-auto": true, // 添加了可能的mode参数  
            "http-url:https://cdn.skypack.dev/-/scheduler@v0.20.2-PAU9F1YosUNPKr7V4s0j/dist=es2015": true, // 假设是es2015而不是es201(可能是个打字错误)  
            "http-url:https://cdn.skypack.dev/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports-auto": true, // 添加了可能的mode参数  
            // 注意:以下条目格式可能不正确,通常我们不会在这里使用对象,除非有特定的原因  
            // "http-url:https://cdn.skypack.dev/react-dom": { "bytesInOutput": 0 }, // 这可能不是有效的输入表示  
            "src/index.jsx": { "bytesInOutput": 178 }  
          },  
          "bytes": 205284  
        },  
        "dist/index.js.map": { /* 省略内容 */ }  
      }  
    }
    
  • 从 outputs 属性中我们可以看到产物的路径,这意味着我们可以在插件中拿到所有 js 和
    css 产物,然后自己组装、生成一个 HTML,实现自动化生成 HTML 的效果

  • 我们接着来实现一下这个插件的逻辑,首先新建 html-plugin.js ,内容如下:

    const fs = require("fs/promises");
    const path = require("path");
    const {
      createScript,
      createLink,
      generateHTML
    } = require('./util');
    
    module.exports = () => {
       return {
         name: "esbuild:html",
         setup(build) {
           build.onEnd(async (buildResult) => {
             if (buildResult.errors.length) {
               return;
             }
             const {
               metafile
             } = buildResult;
             // 1. 拿到 metafile 后获取所有的 js 和 css 产物路径
             const scripts = [];
             const links = [];
             if (metafile) {
               const {
                 outputs
               } = metafile;
               const assets = Object.keys(outputs);
               assets.forEach((asset) => {
                 if (asset.endsWith(".js")) {
                   scripts.push(createScript(asset));
                 } else if (asset.endsWith(".css")) {
                   links.push(createLink(asset));
                 }
               });
             }
             // 2. 拼接 HTML 内容
             const templateContent = generateHTML(scripts, links);
             // 3. HTML 写入磁盘
             const templatePath = path.join(process.cwd(), "index.html");
             await fs.writeFile(templatePath, templateContent);
           });
       },
    };
    
    // util.js
    // 一些工具函数的实现
    const createScript = (src) => `<script type="module" src="${src}"></script>`;
    const createLink = (src) => `<link rel="stylesheet" href="${src}"></link>`;
    const generateHTML = (scripts, links) => `
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Esbuild App</title>
    ${links.join("\n")}
    </head>
    <body>
    <div id="root"></div>
    ${scripts.join("\n")}
    </body>
    </html>
    `;
    
    module.exports = {
      createLink,
      createScript,
      generateHTML
    };
    
  • 现在我们在 build.js 中引入 html 插件

    const html = require("./html-plugin");
    // esbuild 配置
    plugins: [
     // 省略其它插件
     html()
    ],
    
  • 然后执行 node build.js 对项目进行打包,你就可以看到 index.html 已经成功输出到根
    目录。接着,我们通过 serve 起一个本地静态文件服务器:

    // 1. 全局安装 serve
    npm i -g serve
    // 2. 在项目根目录执行
    serve .
    
  • 可以看到如下的界面:

  • 再访问 localhost:3000 ,会默认访问到 index.html 的内容:
  • 这样一来,应用的内容就成功显示了,也说明 HTML 插件正常生效了。当然,如果要做
    一个足够通用的 HTML 插件,还需要考虑诸多的因素,比如 自定义 HTML 内容 、 自定义
    公共前缀(publicPath) 、 自定义 script 标签类型 以及 多入口打包 等等,大家感兴趣的话
    可以自行扩展。可以参考这个插件: esbuild-plugin-html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1871192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

postGreSQL关系数据库介绍

什么是postGreSQL关系数据库&#xff1f; PostgreSQL 是一个强大的、开源的对象关系型数据库管理系统&#xff08;ORDBMS&#xff09;。它基于POSTQUEL查询语言的继承&#xff0c;提供了对SQL标准的广泛支持&#xff0c;并扩展了许多高级功能&#xff0c;如事务处理、多版本并…

被年轻人整不会的1688,终于开始反击了!

“我可以买贵的&#xff0c;但我不能买贵了。”成为年轻人新晋购物哲学。 最近&#xff0c;在豆瓣、小红书等社交媒体上出现了一种独特的消费趋势。在这些平台被种草之后&#xff0c;越来越多对价格敏感、注重性价比的年轻人开始挖宝“大牌平替”。他们涌入阿里巴巴旗下以批发…

配置Nginx二级域名

一、环境 &#xff08;一&#xff09;配置 1.服务器 linux CentOS 2.反向代理 Nginx 3.开放端口 云服务器开放端口80和443 二、域名备案 &#xff08;一&#xff09;腾讯云 1.腾讯云域名备案流程 备注&#xff1a;一级域名备案后&#xff0c;二级域名可以不用再备案&a…

第一个Flask程序

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 一切准备就绪&#xff0c;现在我们开始编写第一个Flask程序&#xff0c;由于是第一个Flask程序&#xff0c;当然要从最简单的“Hello World&#xff…

Qt Object:智能即时聊天室项目

目录 1.项目介绍 2.设计思路 3.Pro文件配置 4.项目演示 5.项目开源 项目介绍 智能即时聊天室系统&#xff08;AIChatProject&#xff09;是一个高效、灵活的即时通讯解决方案。它融合了百度的开源大型语言模型——文心一言&#xff0c;通过API接口实现深度集成。系统专为聊天和…

深入理解RLHF技术

在《LLM对齐“3H原则”》这篇文章中&#xff0c;我们介绍了LLM与人类对齐的“3H”原则&#xff0c;但是这些对齐标准主要是基于人类认知进行设计的&#xff0c;具有一定的主观性。因此&#xff0c;直接通过优化目标来建模这些对齐标准较为困难。本文将介绍基于人类反馈的强化学…

JavaScript的单线程与任务队列

目录 一、JavaScript为什么设计为单线程&#xff1f; 二、任务队列 &#x1f44b;&#x1f3fb; 扩展&#xff1a;异步队列里的宏任务与微任务 一、JavaScript为什么设计为单线程&#xff1f; JavaScript语言的一大特点就是单线程&#xff0c;换言之就是同一个时间只能做一…

三元和磷酸铁锂电池有什么区别?

现在的电动车大多都会使用到锂电池&#xff0c;在常见的锂电池分为两种&#xff0c;一种是三元锂电池另外一种是磷酸铁锂电池&#xff0c;面对这两种锂电池时&#xff0c;它们到底有什么不同&#xff1f; 1、材料不同 这两种锂电池的不同之处便是材料不同&#xff0c;磷酸铁锂…

中霖教育:税务师考试通过率怎么样?

税务师考试的通过率通常在20%至30%的范围内&#xff0c;涵盖五个科目&#xff1a;《税法一》、《税法二》、《财务与会计》、《涉税服务实务》和《涉税服务相关法律》&#xff0c;成绩有效期为五年。 针对税务师备考&#xff0c;有效的学习策略至关重要。 1、熟悉各科的题型和…

柯桥商务英语培训under是“在下面”,dog是“狗”,那underdog是什么意思?

英语中有很多单词 拆开看都认识 但合在一起意思就变了 就比如这个表达&#xff1a;underdog 大家都知道&#xff1a;under是下面&#xff0c;dog是狗 那么underdog是啥意思呢&#xff1f; 在小狗下面&#xff1f; 还是活得连狗都不如&#xff1f; 当然没有那么简单 但我…

C++日期类的完整实现,以及this指针的const修饰等的介绍

文章目录 前言一、日期类的实现二、this指针的const修饰总结 前言 C日期类的完整实现&#xff0c;以及this指针的const修饰等的介绍 一、日期类的实现 // Date.h #pragma once#include <iostream> using namespace std;#include <assert.h>class Date {// 友元函…

利用Python控制终端打印字体的颜色和格式

利用Python控制终端打印字体的颜色和格式—操作详解&#xff08;ANSI转义序列&#xff09; 一、问题描述二、ANSI转义序列三、具体代码和显示效果&#xff08;看懂这段代码&#xff0c;以后可随心控制字体的打印格式&#xff09; 欢迎学习交流&#xff01; 邮箱&#xff1a; z……

ONLYOFFICE 桌面编辑器 8.1 版发布:全面提升文档处理效率的新体验

文章目录 什么是ONLYOFFICE &#xff1f;ONLYOFFICE 桌面编辑器 8.1 发布&#xff1a;新功能和改进功能强大的 PDF 编辑器幻灯片版式功能从右至左语言支持多媒体功能增强无缝切换工作模式其他改进和优化总结 什么是ONLYOFFICE &#xff1f; https://www.onlyoffice.com/zh/off…

光伏发电项目是如何提高开发效率的?

随着全球对可再生能源需求的持续增长&#xff0c;光伏发电项目的高效开发成为关键。本文将深入探讨如何在实际操作中提高光伏发电项目的开发效率。 一、优化选址流程 1、数据收集与分析&#xff1a;利用卫星地图和遥感技术&#xff0c;收集目标区域的光照资源、地形地貌、阴影…

【图像分类】Yolov8 完整教程 |分类 |计算机视觉

目标&#xff1a;用YOLOV8进行图像分类。 图像分类器。 学习资源&#xff1a;https://www.youtube.com/watch?vZ-65nqxUdl4 努力的小巴掌 记录计算机视觉学习道路上的所思所得。 1、文件结构化 划分数据集&#xff1a;train,val,test 知道怎么划分数据集很重要。 文件夹…

OSI七层模型TCP/IP四层面试高频考点

OSI七层模型&TCP/IP四层&面试高频考点 1 OSI七层模型 1. 物理层&#xff1a;透明地传输比特流 在物理媒介上传输原始比特流&#xff0c;定义了连接主机的硬件设备和传输媒介的规范。它确保比特流能够在网络中准确地传输&#xff0c;例如通过以太网、光纤和无线电波等媒…

等保测评初级简答题试题

基本要求&#xff0c;在应用安全层面的访问控制要求中&#xff0c;三级系统较二级系统增加的措施有哪些&#xff1f; 答&#xff1a;三级比二级增加的要求项有&#xff1a; 应提供对重要信息资源设置敏感标记的功能&#xff1b; 应按照安全策略严格控制用户对有敏感标记重要…

MySQL高级-索引-使用规则-单列索引联合索引

文章目录 1、单列索引2、联合索引3、查看表索引4、创建 name 和 phone 索引5、查询 phone17799990010 and name韩信6、执行计划 phone17799990010 and name韩信7、创建联合唯一索引 idx_user_phone_name8、再次执行计划 phone17799990010 and name韩信9、使用了USE INDEX提示来…

为什么叫云计算?云计算的优势有哪些

说起云计算大家并不会感到陌生&#xff0c;那么为什么叫云计算&#xff1f;云计算技术的引入通常会使企业的信息技术应用更高效、更可靠、更安全。云计算支持用户在任意位置、使用各种终端获取应用服务。使用了数据多副本容错、计算节点同构可互换等措施来保障服务的高可靠性&a…

从RLHF到DPO再到TDPO,大模型对齐算法已经是「token-level」

在人工智能领域的发展过程中&#xff0c;对大语言模型&#xff08;LLM&#xff09;的控制与指导始终是核心挑战之一&#xff0c;旨在确保这些模型既强大又安全地服务于人类社会。早期的努力集中于通过人类反馈的强化学习方法&#xff08;RLHF&#xff09;来管理这些模型&#x…