Bun技术评估 - 18 Bun 1.2(下)

概述

本文是笔者的系列博文 《Bun技术评估》 中的第十八篇。

并且作为下篇,继续上篇: 《Bun技术评估 - 17 Bun 1.2(上)》 的内容。

Packge Manage 包管理

这部分的相关内容比较多,而且也比较重要,笔者将会另行撰文详细探讨。

JS Bundle 新特性

其实笔者对于bun的前端特性并没有太深的研究,坦白讲作为后端开发者而言也没有特别大的兴趣,只是了解到Bun可以做一些前端相关的操作。具体而言,就是内置了支持JavaScript和TypeScript的捆绑器(bundle)、转译器(transpiler)和压缩器(minifier),可以为浏览器、Node.js 和其他平台包装代码。

这里就简单例举一下新版本中相关的内容。

  • HTML imports HTML导入

Bun1.2中增加了对HTML导入的支持,允许将整个前端工具链替换为单个导入语句。下面是一个简单示例:

js 复制代码
// 导入html
import homepage from "./index.html";

Bun.serve({
  static: {
    "/": homepage,
  },

  async fetch(req) {
    // ... api requests
  },
});

// HTML原始内容
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="./reset.css" />
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./sentry-and-preloads.ts"></script>
    <script type="module" src="./my-app.tsx"></script>
  </body>
</html>

// 打包后的HTML
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="/index-[hash].css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index-[hash].js"></script>
  </body>
</html>

从后端应用而言,这个引入显然非常直观和简单,并且能够和后端应用进行集成;而从前端而言,这个过程是透明而自动的。

  • standalone executable 单一可执行文件

Bun提供的构建工具和选项,允许开发者将其应用程序,包括bun本身,构建到一个单一可执行文件中。这种方式在很多应用分发和使用场合非常有用。

在1.2版本中,这个构建是支持构建跨平台(现在支持Linux、MacOS和Windows)的应用的,下面是一个简单的示例:

js 复制代码
// 跨平台构建
bun build --compile --target=bun-windows-x64 app.ts

[8ms]  bundle  1 modules
[1485ms] compile  app.exe bun-windows-x64-v1.2.0

// 设置图标,windows隐藏控制台
bun build --compile --windows-icon=./icon.ico --windows-hide-console app.ts

但笔者的评估,虽然这个特性是非常令人激动的,但实际上的操作可能有很多的限制,可能还处于比较早期的发展阶段(nodejs那边也是类似的情况),还没有到实用的程度,只能处理非常简单的应用。

  • Bytecode caching 字节码缓存

Bun1.2有一个构建选项,可以在应用编译时,同时生成一个字节码的版本。程序启动时,可以提高启动和执行速度,代价时需要更多磁盘空间。

js 复制代码
// 程序构建
bun build --bytecode --outdir=dist app.ts

// 构建目标
ls dist
app.js  app.jsc

这个看起来不错,但是据文档所说,还是需要原始的js文件和配套的字节码文件(jsc)一起才能执行。

  • CommonJS output format, CommonJS输出格式

build的时候,可以选择输出为CommonJS的格式。可能有些古早版本的执行环境需要兼容。

  • Better CommonJS detection, 更好的CommonJS检测

Bun的一个有趣之处,就是它同时支持CommonJS和ESM,也就是说,你可以在一个文件中同时使用import和require。在新的Bun版本中,系统会自动检测合适的环境。

  • Plugin API 插件API

这主要是和N-API插件开发相关的特性,笔者研究不多,可能需要在应用评估后另行探讨。

  • Inject environment variables 环境变量注入

build可以通过编译参数,将环境变量注入编译过程

js 复制代码
bun build --env="PUBLIC_*" app.tsx

// 等同于
import { build } from "bun";

await build({
  entrypoints: ["./app.tsx"],
  outdir: "./out",
  // Environment variables starting with "PUBLIC_"
  // will be injected in the build as process.env.PUBLIC_*
  env: "PUBLIC_*",
});
  • drop

build drop选项,可以在构建时,去除设置的函数调用。

js 复制代码
bun build ./index.tsx --outdir ./out --drop=console --drop=anyIdentifier.or.propertyAccess

// 构建将会去除所有console.log调用
  • banner/footer 旗标和脚注

构建时可以指定旗标和脚注的内容,这些内容其实都是注释内容,方便增加版权和说明信息等等:

js 复制代码
- bun build --banner "/* Banner! */" --footer "/* Footer! */" app.ts

// 生成结果
/**
 * Banner!
 */
export default "Hello, world!";
/**
 * 
 Footer!
 */
  • --packages=external

这个选项可以控制构建时,是否包括外部包。这个选项可以基于运行环境,减少构建包大小,或者可以用于构建库文件。

  • CSS Parse

Bun1.2中,基于LightCSS程序,实现了一个新的CSS解析器,来实现和优化前端代码中CSS相关的处理。它可以将多个CSS文件和资产,结合使用如url、@import、@font-face等指令,包装成为一个单一的CC文件。可以简化HTML代码和网络请求。

下面是一个简单的例子:

js 复制代码
// index.css 主引用文件
@import "foo.css";
@import "bar.css";

// 被引用文件 foo.css
.foo {
  background: red;
}

// 被引用文件 bar.css
.bar {
  background: blue;
}

// 构建指令
bun build ./index.css


//dist.css 最终结果
/** foo.css */
.foo {
  background: red;
}

/** bar.css */
.bar {
  background: blue;
}
  • CSS Import

和HTML内容的处理类似,bun提供了CSS自动解析构建的功能,方便开发和维护。

js 复制代码
// index.ts
import "./style.css";
import MyComponent from "./MyComponent.tsx";

// ... rest of your app

// 构建操作,生成最终单一css
bun build ./index.ts --outdir=dist

  index.js     0.10 KB
  index.css    0.10 KB
[5ms] bundle 4 modules
  • Using Bun.build() 构建方法

可以手动编写和控制构建过程,核心是Bun.build构建方法。这个程序相对命令行操作而言,可以整合更多构建内容、参数和选项。

js 复制代码
import { build } from "bun";

const results = await build({
  entrypoints: ["./index.css"],
  outdir: "./dist",
});

console.log(results);

Bun API改进

Bun API是Bun开发系统中的重要组成部分。笔者的感觉,很多核心功能和特性,都直接在这里实现,所以Bun本身作为一个类和根名字空间,本身都是非常庞大和丰富的(有点像浏览器中的Global对象?)。

但是,在1.2版本中的改进并不是很多,下面简单列举一下:

  • Bun.file()

Bun.file增加了delete(unlink别名)方法,可以用于删除文件; stat()方法用于获取文件元数据。

  • Bun.color()

这个功能在前端用的比较多,用于颜色的定义和转换。

  • dns.prefetch()

dns记录预取和缓存,可以在某些情况显著增强基于主机名的网络访问性能。配套方法还有dns.getCacheStats()可以获取dns缓存状态。

  • Bun.inspect.table()

格式化文本表格数据,一般用于美化日志。

  • Bun.randomUUIDv7()

生成随机UUIDv7字符串。

Web API 改进

Bun1.2提供了很多Web API的改进,大部分是和stream操作相关的。下面简单总结一下:

  • TextDecoderStream / TextEncoderStream

在TextEncoder/Decoder上增加了stream机制。可以简化编程,提高数据处理效率,降低资源占用。下面是一个简单的示例:

js 复制代码
// reponse解码流程
const response = await fetch("https://example.com");
const body = response.body.pipeThrough(new TextDecoderStream());

for await (const chunk of body) {
  console.log(chunk); // typeof chunk === "string"
}

// 等价代码
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello, world!");
    controller.close();
  },
});
const body = stream.pipeThrough(new TextEncoderStream());

for await (const chunk of body) {
  console.log(chunk); // chunk instanceof Uint8Array
}
  • bytes() API

Bun1.2支持直接获取二进制响应内容,结果就是标准的Uint8Array(原为arrayBuffer)。更加简单直观,示例代码如下:

js 复制代码
// bytes() 方法
const response = await fetch("https://example.com/");
const bytes = await response.bytes();
console.log(bytes); // Uint8Array(1256) [ 60, 33, ... ]

// 原方法,需要转换过程
const blob = new Blob(["Hello, world!"]);
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);

// 文件也可以使用
import { file } from "bun";

const content = await file("./hello.txt").bytes();
console.log(content); // Uint8Array(1256) [ 60, 33, ... ]
  • fetch stream post

fetch post方法,现在支持stream作为参数:

js 复制代码
await fetch("https://example.com/upload", {
  method: "POST",
  body: async function* () {
    yield "Hello";
    yield " ";
    yield "world!";
  },
});
  • console.group() /groupEnd()

这其实只是一个日志呈现的改进,就是将日志分组缩进呈现:

js 复制代码
console.group("begin");
console.log("indent!");
console.groupEnd();
// begin
//   indent!
  • URL.createObjectURL()

现在可以从一个blob(文件)对象创建一个URL对象,用于很多相关的API。例如下面的示例,可以基于一个文本动态的创建文件对象并用于worker:

js 复制代码
const code = `
  const foo: number = 123;
  postMessage({ foo } satisfies Data);
`;
const blob = new File([code], "worker.ts");
const url = URL.createObjectURL(blob);

const worker = new Worker(url);
worker.onmessage = ({ data }) => {
  console.log("Received data:", data);
};
  • AbortSignal.any()方法

现在可以使用any方法,统一的管理多个AbortSignal。示例代码如下:

js 复制代码
const { signal: firstSignal } = new AbortController();
fetch("https://example.com/", { signal: firstSignal });

const { signal: secondSignal } = new AbortController();
fetch("https://example.com/", { signal: secondSignal });

// Cancels if either `firstSignal` or `secondSignal` is aborted
const signal = AbortSignal.any([firstSignal, secondSignal]);
await fetch("https://example.com/slow", { signal });

这样可以帮助在多个异步操作构建更具有关联性和逻辑的撤销机制。

执行C代码

Bun在1.2版本中,"实验性"的增加了从JavaScript编译和运行C代码的特性,而且这这个过程更加简单,无需构造环节。下面是一个简单的示例:

js 复制代码
// C程序
#include <stdio.h>
#include <stdlib.h>

int random() {
  return rand() + 42;
}


// JS调用
import { cc } from "bun:ffi";
const { symbols: { random } } = cc({
  source: "./random.c",
  symbols: {
    random: {
      returns: "int",
      args: [],
    },
  },
});

console.log(random()); // 42

这个代码,可以直接执行,不需要额外的编译或者配置。

Bun解释说,之所以需要这个特性,是因为在一些高级用例或者性能考量的场景,需要为JavaScript提供系统库的执行能力。当前,在nodejs环境中,最常见的方法是使用node-gyp编译N-API插件,但这个操作通常需要会运行一个 postinstall 脚本。这个脚本的执行并非简单,它需要现代版的Python和C编译器,这些都需要一个相对复杂的开发支撑环境,并且容易遇到编译器或者node-gyp等方面的问题,开发和运维体验是不太好的。

js 复制代码
gyp ERR! command "/usr/bin/node" "/tmp/node-gyp@latest--bunx/node_modules/.bin/node-gyp" "configure" "build"
gyp ERR! cwd /bun/test/node_modules/bktree-fast
gyp ERR! node -v v12.22.9
gyp ERR! node-gyp -v v9.4.0
gyp ERR! Node-gyp failed to build your package.
gyp ERR! Try to update npm and/or node-gyp and if it does not help file an issue with the package author.
error: "node-gyp" exited with code 7 (SIGBUS)

所以,Bun在这方面进行了努力,想要改善相关的问题和体验,1.2就是一个很好的尝试和开始。在代码中,我们可以看到这个实现的基础是bun的ffi(Foreign Function Interface,外部函数接口)模块,还有其内置的TinyCC编译器,这部分的机制和内容比较复杂,这里就不再展开了,笔者有机会会另行著文探讨。

musl support

开发者一般都会了解到,nodejs包括bun,一般都需要标准的glibc的支撑。这个基础在标准的Linux和C环境下是没有问题的。但在很多云计算环境和Docker环境中,它们会基于一些考量(主要是镜像的大小),并不使用标准的glibc库,而是另一种musl libc库。

现实的情况是,在很多企业和互联网应用环境中,将nodejs和bun应用部署在云计算和Docker环境中的需求也越来越多,所以bun1.2提供了相应的支撑musl libc的版本。有一些docker镜像,已经内置了bun的musl版本,当然,这个特性,对于要执行的JS代码而言,是透明的。

js 复制代码
docker run --rm -it oven/bun:alpine bun --print 'Bun.file("/etc/alpine-release").text()'

3.20.5

这样做,也不是无代价的。Bun文档也提到了,musl libc版本的程序,性能可能会略低于标准glibc的版本。开发者需要在性能和运维方便之间进行考虑和平衡。

JavaScript特性

JavaScript是一个持续演进的语言。在Bun1.2中对于JS的支撑也在同步改进。现在的Bun已经支持到一个名为TC39的特性集合。它提供一些新的内容,简单摘要总结如下:

  • Import attributes 导入类型

在导入时可以指定内容的类型,在很多情况下可以简化开发工作,因为可以自动转换。

js 复制代码
import json from "./package.json" with { type: "json" };
typeof json; // "object"

import html from "./index.html" with { type: "text" };
typeof html; // "string"

import toml from "./bunfig.toml" with { type: "toml" };
typeof toml; // "object"

const { default: json } = await import("./package.json", {
  with: { type: "json" },
});
typeof json; // "object"
  • Resource management with using 使用using管理资源

using是一个新引入的JS语法特性,它可以用于自动的控制变量的引用空间,用于显式的资源管理(很多语言都有类似的机制)。笔者觉得可以简单的理解,它就是一个带有指定作用范围的声明(类似let或者var),它可以在作用域结束后自动释放资源。这样的操作,可以显著的避免由于资源没有正常关闭和处理造成的内存泄漏的风险。

下面有一个简单的示例(这个示例并不是原文的示例,因为笔者觉得这个示例能够更好的说明问题):

js 复制代码
class FileHandle {
  constructor(name) {
    this.name = name;
    console.log(`打开文件 ${name}`);
  }

  [Symbol.dispose]() {
    console.log(`关闭文件 ${this.name}`);
  }

  read() {
    return `读取文件 ${this.name}`;
  }
}

function example() {
  using file = new FileHandle("data.txt");

  console.log(file.read());
}

example();

// 结果
打开文件 data.txt
读取文件 data.txt
关闭文件 data.txt

这个示例中,我们显式的打开并且读取了文件,但其实并没有关闭这个文件。但实际上,当程序关闭后(离开了作用区域),仍然可以正确的执行关闭文件的操作。

从上面的例子中还可以看到,要支持这个特性,需要在对象原型定义dispose方法:

js 复制代码
class Resource {
  [Symbol.dispose]() { /* ... */ }
}

using resource = new Resource();

class AsyncResource {
  async [Symbol.asyncDispose]() { /* ... */ }
}

await using asyncResource = new AsyncResource();
  • Promise.withResolvers()

这其实是一个语法糖,可以简化new Promise的写法,无需在业务代码中引入新建的Promise对象,结构更加直观清晰。

js 复制代码
// 新写法
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve(), 1000);
await promise;

// 原始写法
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve(), 1000);
});
await promise;
  • Promise.try()

这个方法,可以通过一致的方式,调用同步或者异步方法!

js 复制代码
const syncFn = () => 1 + 1;
const asyncFn = async (a, b) => 1 + a + b;

await Promise.try(syncFn); // => 2
await Promise.try(asyncFn, 2, 3); // => 6
  • Error.isError()

判断一个对象是否是错误对象的新方法。

  • Uint8Array.toBase64() / fromBase64()

终于,有了一个方便的base64转换程序(标准js程序):

js 复制代码
new Uint8Array([1, 2, 3, 4, 5]).toBase64(); // "AQIDBA=="
Unit8Array.fromBase64("AQIDBA=="); // [1, 2, 3, 4, 5]

有了这个实现,开发者可以实现前后端相同一致的处理代码。不过笔者觉得还是nodejs的buffer实现最优雅。

  • Uint8Array.toHex() / fromHex()

和base64情况类似

  • Iterator helpers 改进的迭代器

改进的迭代器,支持和Array类似的map等遍历方法。

js 复制代码
function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

const result = range(3, 5).map((x) => x * 2);
result.next(); // { value: 6, done: false }

除了map之外,改进的迭代器还支持flatmap(展平映射)、filter(过滤)、take(范围)、drop(丢弃)、reduce(缩减)、toArray(数组)、foreach(遍历)、find(查找)等多种处理和操作。

  • Float16Array

实现和支持了新的Float16数据的数组,这算是跟上AI时代的潮流吗?

Bun行为

在版本1.2中,有很多有趣的关于bun行为的改进。

  • 工作目录

如果笔者没有理解错误的话,在Bun1.2中,默认的应用启动的工作文件夹,将会固定的指定到package.json所在的文件夹(即项目根文件夹),而非原来的启动命令执行时的当前文件夹。这样可以保证规范和一致性。

  • Uncaught errors in bun test 测试执行时未捕获的错误

简单而言,在新版中,bun会作为程序错误,抛出未捕获的错误,原来的版本不会:

js 复制代码
import { test, expect } from "bun:test";

// 老版本会执行完成
test("should have failed, but didn't", () => {
  setTimeout(() => {
    throw new Error("Oops!");
  }, 1);
});


// 新版本执行报错
# Unhandled error between tests
-------------------------------
1 | import { test, expect } from "bun:test";
2 |
3 | test("should have failed, but didn't", () => {
4 |   setTimeout(() => {
5 |     throw new Error("Oops!");
              ^
error: Oops!
      at foo.test.ts:5:11
-------------------------------
  • server.stop() returns a Promise

Bun1.2中,server.stop()会返回一个promise帮助开发者进行后续处理。原来的版本只会返回一个void。

  • Bun.build() rejects when it fails

build方法会返回一个promise,原有的处理在出错的时候,这个promise也会resolve,需要访问result.logs才能检查错误信息,改进后的版本直接reject,更直观规范。

  • bun -p is an alias for bun --print

这个改进是为了和nodejs一致,现有的-p,就是--print的别名,用于打印信息。而非原来的指定端口。

  • bun build --sourcemap

原有的版本中,如果选择sourcemap标签,构建后的sourcemap是"inline"的,就是被嵌入到js代码当中。新版本中,会处理到新生成的map文件当中,和一些如esbuild等外部工具保持一致。

js 复制代码
bun build --sourcemap ./index.ts --outfile ./index.js

// 原模式
console.log("Hello Bun!");
//# sourceMappingURL=data:application/json;base64,...

// 新方式 index.ts.map
{
  "version": 3,
  "sources": ["index.ts"],
  // ...
}

性能,更快

性能一直是Bun技术发展的一个主要目标和诉求。在1.2版本中,Bun在很多方面都有了一些性能改进,其实笔者对这些性能改进取得的提升并没有太大的兴趣,更加关注这些提升或者优势是如何取得的,所以才会和读者一起进行讨论和思考,这些改进背后的系统规划、设计和实现的技术细节。

这些性能提升包括:

  • node:http2 is 2x faster

node:http2的性能提升了2倍。在1.2中,node:http2就是一个原生实现的新特性。相比nodejs系统而言,有大约两倍的性能优势。我们也一般性的认为bun相对nodejs有这个级别的性能优势。

  • node:http is 5x faster at uploading to S3

当上传文件到S3时,node:http的性能提升5倍。这个测试使用了@aws-sdk/client-s3。按照他们的说法就是修复了一个node:http模块实现。

  • path.resolve() is 30x faster

路径字符串解析性能提升30倍。原因在文档中有简单的解释,原来的实现有两个问题: 首先是Bun没有缓存当前的工作文件夹,在getcwd()进行系统调用的时候,每次都要实际操纵一遍;还有就是在原来的实现有几个不必要的临时堆分配。这些都是可以优化的地方。

  • fetch() is 2x faster at DNS resolution

fetch方法,在DNS解析方法上的性能提升了2倍。

bun --hot uses 2x less memory

在使用bun --hot 执行标签时,程序执行和重启节省了大量内存占用。

  • fs.readdirSync() is 5% faster on macOS

fs.readdirSync方法,在MacOS系统上,提升了5%.

String.at方法提升了44%。作为一个基础应用,这个改进应该也是比较重要的。

  • atob() is 8x faster

atob方法,性能提升8倍。atob(ASCII to binary)是一个比较基础的系统方法,这个改进的影响可能会比较大。改进的原因在于应用了一个SIMDUTF库,就是应用SIMD(单指令多数据)技术改进UTF数据的处理性能。

  • fetch() decompresses 30% faster

fetch方法,解压缩的性能提升了30%。应该是应用了libdeflate库的成果。还有人报告他在MacOS系统上的提升是一倍。

  • Buffer.from(String, "base64") is 30x faster

base64字符串转换为buffer的性能,提高了30倍。同样是SIMFUTF的功劳。

  • JSON.parse() is up to 4x faster

JSON.parse方法性能提升了4倍,这应该是一个很重要的特性。其原因在于引入了SIMDE。

"SIMD Everywhere",是一个 C 语言的跨平台头文件库,用于在不支持特定 SIMD 指令集的平台上模拟 SIMD(单指令多数据)操作。它可以提供在JSON.parse方法中更快的字符串扫描,和JSON.stringify方法中更快的扫描和复制。

  • Bun.serve() has 2x more throughput

Bun.serve吞吐量提升了一倍。

  • Error.captureStackTrace() is 9x faster

captureStackTrace应该是一个错误捕获操作,快了9倍,应当能够显著提升开发调试的工作流程。

  • fs.readFile() is 10% faster

对于小型文件,fs.readFile()的性能提升了大约10%。

  • console.log(String) is 50% faster

console.log性能提升了50%。

  • JavaScript is faster on Windows

在版本1.2中,通过在Windows版本上开启JIT,实现了很多性能的改进。在此之前,JIT只能在MacOS和Linux系统中开启。JIT本身是JSC的核心特性之一(通过FTL JIT,Fourth Tier LLVM Just-In-Time 第四层LLVM即时编译器),但FTL JIT在Windows上曾被禁用,主要原因是由于调用约定差异、MSVC编译器限制和维护成本高等。但随着WebKit转向 clang-cl 并统一调用约定,FTL JIT于2024年在Windows端口默认启用,从而解决了这个历史遗留问题。这使JSC在Windows上的运行性能得以恢复和优化,包括Object.entries()提升20%,Array.map()提升50%等等。

简单总结一下,这里面并没有什么黑科技和魔法,性能优化和改进,就是那么几个方面:

  • 应用更新更先进的硬件特性,如SIMD指令集
  • 减少不必要和重复性操作
  • 修复可能影响性能的缺陷
  • 更新引用库,特别是基础库,它们一般本身就包括了性能改进和提升
  • 为特定平台和环境进行优化
  • 对于重要的模块和功能,使用更底层的实现,对于Bun,就是多使用zig语言和代码
  • 业务流程和数据优化

小结

本文作为《Bun技术评估 - 16 Bun 1.2(上)》 一文的下篇,接续上篇一起讨论了Bun1.2版本的新功能特性。并且着重在下篇探讨了如包管理、Bundle、Bun API、Web APPI、C代码执行、JavaScript新特性,Bun行为和性能等方面的问题。

相关推荐
LaoZhangAI10 分钟前
FLUX.1 API图像尺寸设置全指南:优化生成效果与成本
前端·后端
Kookoos18 分钟前
ABP VNext + EF Core 二级缓存:提升查询性能
后端·.net·二级缓存·ef core·abp vnext
哑巴语天雨19 分钟前
Cesium初探-CallbackProperty
开发语言·前端·javascript·3d
月初,24 分钟前
SpringBoot集成Minio存储文件,开发图片上传等接口
java·spring boot·后端
JosieBook36 分钟前
【前端】Vue 3 页面开发标准框架解析:基于实战案例的完整指南
前端·javascript·vue.js
KubeSphere39 分钟前
全面升级!WizTelemetry 可观测平台 2.0 深度解析:打造云原生时代的智能可观测平台
后端
Frank_zhou40 分钟前
Tomcat - 启动过程:类加载机制详解
后端
薄荷椰果抹茶41 分钟前
前端技术之---应用国际化(vue-i18n)
前端·javascript·vue.js
bobz9651 小时前
kubevirt 使用图和节点维护计算|存储|网络
后端
屠龙少年想成恶龙1 小时前
jenkins的CICD部署
后端