多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Midway 3.x 单元测试实战指南:从 Jest 配置到 HTTP 与依赖注入测试

Midway 3.x 单元测试实战指南:从 Jest 配置到 HTTP 与依赖注入测试 后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载单元测试是保障 Node.js 应用稳定性的核心防线。Midway 3.x 在框架层面深度集成了 Jest 与midwayjs/mock工具集让开发者可以把精力集中在编写测试用例本身而不是纠结于测试周边工具的选择与组装。本文将基于官方文档与仓库源码系统讲解 Midway 3.x 应用测试的目录约定、运行工具、断言方式、HTTP 服务测试、依赖注入容器中的服务测试、createApp/close参数细节、bootstrap 入口测试以及常见超时、串并行、编辑器调试等实战问题帮助你建立一套完整、可复用的测试方法论。测试目录结构与命名约定Midway 约定test目录为存放所有测试脚本的目录测试所使用到的fixtures用于隔离测试的模拟应用目录和相关辅助脚本都应该放在此目录下。测试脚本文件统一按${filename}.test.ts命名必须以.test.ts作为文件后缀。这样约定有两个直接好处Jest 默认的testMatch规则可以直接识别测试文件Midway 工具链在运行测试时会按\/test\/[^.]*\.test\.ts$/i的规则匹配测试套件运行输出中的Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i.即是证明。一个典型应用的测试目录示例➜ my_midway_app tree . ├── src ├── test │ └── controller │ └── home.controller.test.ts ├── package.json └── tsconfig.json在仓库的各个包中这一约定被严格遵守。例如 packages/web/test/custom.test.ts 就是按describe(/test/custom.test.ts, ...)组织而测试所需的模拟应用全部放在各包的test/fixtures目录下参见 packages/web/test/fixtures 中的base-app-use-custom-egg等项目。测试运行工具midway-bin 与 jestMidway 默认提供midway-bin命令来运行测试脚本。在新版本中Midway 默认将 mocha 替换成了 Jest——Jest 功能更强大、集成度更高让开发者聚焦精力在编写测试代码上而不是纠结选择测试周边工具和模块。只需要在package.json上配置好scripts.test即可有两种方式可选方式一直接使用 jest{ scripts: { test: jest } }方式二使用 midwayjs/climidway-bin{ scripts: { test: midway-bin test --ts } }默认脚手架中都已经提供了上述命令可以开箱即用地运行测试➜ my_midway_app npm run test my_midway_project1.0.0 test /Users/harry/project/application/my_midway_app jest Testing all *.test.ts... PASS test/controller/home.controller.test.ts PASS test/controller/api.controller.test.ts Test Suites: 2 passed, 2 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 3.26 s Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i.注意midway-bin test --ts等价于直接使用 jest 的如下命令$ node --requirets-node/register ./node_modules/.bin/jest即通过ts-node/register让 Jest 在运行前先完成 TypeScript 的即时编译这也是后续编辑器配置中反复出现这条命令的原因。断言库jest 内置的 expectJest 自带了强大的expect断言库可以直接在全局使用它。常用的断言方法包括expect(result.status).toBe(200); // 值是否等于某个值引用相等 expect(result.status).not.toBe(200); expect(result).toEqual(hello); // 简单匹配对象属性相同也为 true expect(result).toStrictEqual(hello); // 严格匹配 expect([lime, apple]).toContain(lime); // 判断是否在数组中其中toBe使用Object.is做引用相等比较适合断言基本类型与引用身份toEqual递归比较对象的结构与属性值toStrictEqual比toEqual更严格还会校验属性的类型与undefined属性等细节。除了expectMidway 官方测试示例中也常常混用 Node 内置的assert模块例如仓库测试中的assert.deepStrictEqual(result.status, 200)两种断言风格可以按团队偏好混用。创建测试midwayjs/mock 的核心方法不同的上层框架的测试方法不同。以最常用的 HTTP 服务举例测试一个 HTTP 服务一般来说需要创建一个 HTTP 服务然后用客户端请求它。Midway 提供了一套基础的midwayjs/mock工具集帮助上层框架进行测试同时提供方便的创建 Framework、App 以及关闭的方法。整个流程方法分为几个部分createApp创建某个 Framework 的 app 对象close关闭一个 Framework 或者一个 app为保持测试简单整个流程目前只透出这两个方法。// create app const app await createAppFramework();这里传入的Framework是用来给 TypeScript 推导类型的这样就能返回主框架 app 实例了。当 app 运行完成后可以使用close方法关闭import { createApp, close } from midwayjs/mock; await close(app);事实上createApp方法中封装了midwayjs/bootstrap的启动逻辑。从 packages/mock/src/creator.ts 的源码可以看到createApp的完整链路是先调用内部createT(baseDir, options)它会设置process.env.MIDWAY_TS_MODE默认true创建带 mock 过滤能力的DynamicMidwayContainer再通过initializeGlobalApplicationContext完成全局应用上下文的初始化然后从容器中取出MidwayFrameworkService调用getMainFramework()拿到主框架实例最后createApp再调用framework.getApplication()返回 app 实例。在 packages/mock/src/index.ts 中midwayjs/mock对外导出了create、close、createApp、createFunctionApp、createLightApp、createBootstrap以及各类客户端方法HTTP、Kafka、RabbitMQ、SocketIO、WS、SSE说明 mock 工具集不仅覆盖 HTTP还覆盖了消息队列与实时通信场景。测试 HTTP 服务除了创建 app 之外midwayjs/mock还提供了简单的客户端方法用于快速创建各种服务对应的测试行为。针对 HTTPMidway 封装了 supertest提供了createHttpRequest方法创建 HTTP 客户端// 创建一个客户端请求 const result await createHttpRequest(app).get(/); // 测试返回结果 expect(result.text).toBe(Hello Midwayjs!);从源码看packages/mock/src/client/http.ts 的实现非常精巧createHttpRequest会优先取 app 的callback2或callback方法生成请求处理器否则直接以 app 实例作为 supertest 的入参。这保证了它可以同时适配 Koa、Express、Egg 等不同底层框架的 app。推荐在一个测试文件中复用 app 实例。完整的测试示例如下import { createApp, close, createHttpRequest } from midwayjs/mock; import { Framework, Application } from midwayjs/koa; import * as assert from assert; describe(test/controller/home.test.ts, () { let app: Application; beforeAll(async () { // 只创建一次 app可以复用 app await createAppFramework(); }); afterAll(async () { // close app await close(app); }); it(should GET /, async () { // make request const result await createHttpRequest(app) .get(/) .set(x-timeout, 5000); // use expect by jest expect(result.status).toBe(200); expect(result.text).toBe(Hello Midwayjs!); // or use assert assert.deepStrictEqual(result.status, 200); assert.deepStrictEqual(result.text, Hello Midwayjs!); }); it(should POST /, async () { // make request const result await createHttpRequest(app) .post(/) .send({id: 1}); // use expect by jest expect(result.status).toBe(200); }); });这里的beforeAll/afterAll是 Jest 的生命周期钩子beforeAll中只创建一次 app 并在afterAll中统一关闭可以大幅降低反复启动框架的开销——这也是仓库各包测试如 packages/web/test/custom.test.ts普遍采用的模式。HTTP 请求的常见参数传递createHttpRequest(app)返回的是 supertest 实例因此 supertest 的全部链式方法都可用创建 get 请求传递 query 参数const result await createHttpRequest(app) .get(/set_header) .query({ name: harry });创建 post 请求传递 body 参数const result await createHttpRequest(app) .post(/user/catchThrowWithValidate) .send({id: 1});创建 post 请求传递 form body 参数const result await createHttpRequest(app) .post(/param/body) .type(form) .send({id: 1})传递 header 头const result await createHttpRequest(app) .get(/set_header) .set({ x-bbb: 123 }) .query({ name: harry });传递 cookieconst cookie [ koa.sesseyJuYW1lIjoiaGFycnkiLCJfZXhwaXIiOjE2MTQxNDk5NDk0NzIsIl9tYXhBZ2UiOjg2NDAwMDAwfQ; path/; expiresWed, 24 Feb 2021 06:59:09 GMT; httponly, koa.sess.sigmMRQWascH-If2-BC7v8xfRbmiNo; path/; expiresWed, 24 Feb 2021 06:59:09 GMT; httponly ] const result await createHttpRequest(app) .get(/set_header) .set(Cookie, cookie) .query({ name: harry });.set()既可以传单个key, value也可以传一个对象批量设置还可以直接传Cookie数组。测试服务从依赖注入容器获取实例在控制器之外有时候需要测试单个服务这时可以从依赖注入容器中获取这个服务。假设需要测试UserService// src/service/user.ts import { Provide } from midwayjs/core; Provide() export class UserService { async getUser() { // xxx } }那么在测试代码中这样写import { createApp, close, createHttpRequest } from midwayjs/mock; import { Framework } from midwayjs/web; import * as assert from assert; import { UserService } from ../../src/service/user; describe(test/controller/home.test.ts, () { it(should GET /, async () { // create app const app await createAppFramework(); // 根据依赖注入 class 获取实例推荐 const userService await app.getApplicationContext().getAsyncUserService(UserService); // 根据依赖注入 Id 获取实例 const userService await app.getApplicationContext().getAsyncUserService(userService); // 传入 class 忽略泛型也能正确推导 const userService await app.getApplicationContext().getAsync(UserService); // close app await close(app); }); });这里app.getApplicationContext()返回应用的依赖注入容器getAsync是异步获取实例的方法。Midway 默认按类名小驼峰生成对象标识UserService→userService所以按 Id 获取与按 class 获取结果一致传入 class 时TypeScript 可以从泛型推断返回类型代码更简洁。如果你的服务和请求相关联即作用域为请求级依赖ctx上下文可以使用请求作用域获取服务——通过createAnonymousContext创建一个匿名上下文来模拟一次请求import { createApp, close, createHttpRequest } from midwayjs/mock; import { Framework } from midwayjs/web; import * as assert from assert; import { UserService } from ../../src/service/user; describe(test/controller/home.test.ts, () { it(should GET /, async () { // create app const app await createAppFramework(); // 根据依赖注入 Id 获取实例 const userService await app.createAnonymousContext() .requestContext.getAsyncUserService(userService); // 也能传入 class 获取实例 const userService await app.createAnonymousContext() .requestContext.getAsync(UserService); // close app await close(app); }); });需要说明的是这里的midwayjs/web对应仓库中的 Egg 上层框架实现而midwayjs/koa等其它框架的createApp用法完全一致——这正是createAppFramework()泛型设计的价值不同框架之间测试代码的骨架可以无缝迁移。createApp 选项参数createApp方法用于创建一个框架的 app 实例通过传入泛型的框架类型使得推断出的 app 能够是该框架返回的 app。比如import { Framework } from midwayjs/grpc; // 这里的 app 能确保是 grpc 框架返回的 app const app await createAppFramework();createApp方法其实是有参数的它的方法签名如下async createApp( appDir process.cwd(), options: IConfigurationOptions {} )第一个参数为项目的绝对根目录路径默认为process.cwd()。注意源码 packages/mock/src/creator.ts 中还有一层特殊处理如果传入的不是绝对路径会被拼接到process.cwd()/test/fixtures/下——这是为了支持直接传 fixtures 目录名如createApp(feature/base-app)的快捷写法如果路径不存在会抛出MidwayCommonError。第二个参数为 Bootstrap 的启动参数比如一些全局行为的配置。从 packages/mock/src/interface.ts 的MockBootstrapOptions定义可以看到它继承自IMidwayBootstrapOptions与ILifeCycle并额外支持参数说明cleanLogsDir关闭后是否清理 logs 日志目录cleanTempDir是否清理临时目录如 egg 生成的 run 目录ssl是否以 HTTPS 启动midwayjs/mock自带ssl/ssl.key与ssl/ssl.pem证书文件见 packages/mock/sslbootstrapTimeoutbootstrap 入口启动超时时间默认 30 秒entryFile入口文件路径如bootstrap.jsbootstrapModefaas或app两种模式moduleLoadTypecommonjs或esm根据package.json的type字段自动推断onReady/onStop/onConfigLoad/onServerReady/onHealthCheck生命周期钩子回调另外create内部还会自动处理MIDWAY_TS_MODE环境变量默认置为true这意味着在 TypeScript 测试环境中源码和测试文件都会走 ts-node 的即时编译通道。close 选项参数close方法用于关闭该 app 实例相关的框架await close(app);其签名如下export declare function close( app: IMidwayApplication | IMidwayFrameworkany, any, options?: { cleanLogsDir?: boolean; cleanTempDir?: boolean; sleep?: number; }): Promisevoid;第一个参数是 app 或者 framework 的实例。第二个参数是个对象在执行关闭时可以执行一些行为cleanLogsDir默认为false控制测试完成后删除日志 logs 目录windows 除外cleanTempDir默认为false清理一些临时目录比如 egg 生成的 run 目录sleep默认为50单位毫秒关闭 app 后的延迟时间防止日志没有成功写入。从源码 packages/mock/src/creator.ts 可以看到close的完整行为它会先调用destroyGlobalApplicationContext销毁全局应用上下文然后仅在isTestEnvironment()即MIDWAY_SERVER_ENV/EGG_SERVER_ENV/NODE_ENV为test或unittest之一时执行清理与休眠逻辑。此外close还兼容了BootstrapAppStarter这类具备close方法但没有getConfig的启动器对象。使用 bootstrap 文件测试一般情况下无需用到bootstrap.js来测试。如果你希望直接使用bootstrap.js入口文件直接测试可以在测试的时候传递入口文件信息。和 dev/test 启动不同的是使用bootstrap.js启动是一个真实的服务会同时运行多个框架创建出多个框架的 app 实例。midwayjs/mock提供了createBootstrap方法做启动文件类型的测试。我们可以将入口文件bootstrap.js作为启动参数传入这样createBootstrap方法会通过入口文件来启动代码it(should GET /, async () { // create app const bootstrap await createBootstrap(join(process.cwd(), bootstrap.js)); // 根据框架类型获取 app 实例 const app bootstrap.getApp(koa); // expect and test // close bootstrap await bootstrap.close(); });从 packages/mock/src/creator.ts 的实现看createBootstrap会先检测工作目录是否安装了midwayjs/faas据此自动选择faas或app模式在app模式下它通过entryFile触发真实的 bootstrap 启动源码中会轮询MIDWAY_BOOTSTRAP_APP_READY全局标志每 200ms 检查一次直到Bootstrap.ready完成或bootstrapTimeout超时最后返回一个BootstrapAppStarter。这个启动器通过MidwayApplicationManager按框架类型如koa取出对应 app关闭时则调用midwayjs/bootstrap的Bootstrap.stop()。运行单个测试和 mocha 的only不同jest 的only方法只针对单个文件生效。方式一直接使用 jest执行单个文件$ jest test/controller/api.ts如果你想运行文件中的特定测试可以使用 jest 的-t或--testNamePattern选项后面跟上你想运行的测试的名称$ jest -t name of your test这将只运行名称匹配的测试。方式二使用 midwayjs/climidway-bin提供运行单个文件的能力$ midway-bin test -f test/controller/api.ts这样可以指定运行某个文件的测试再配合describe.only和it.only可以只运行单个文件中的单个测试方法。自定义 Jest 文件内容一般情况下Midway 工具链内置了 jest 配置用户无需再添加该文件。但有些特殊场景下比如使用 VSCode 或 Idea 等编辑器需要在可视化区域进行开发和测试时可能会需要指定一个jest.config.js的场景此时 Midway 支持创建一个自定义的 jest 配置文件。在项目根目录创建一个jest.config.js文件➜ my_midway_app tree . ├── src ├── test │ └── controller │ └── home.test.ts ├── jest.config.js ├── package.json └── tsconfig.json内容如下配置和标准的 jest 相同module.exports { preset: ts-jest, testEnvironment: node, testPathIgnorePatterns: [rootDir/test/fixtures], coveragePathIgnorePatterns: [rootDir/test/], };关键配置项说明preset: ts-jest让 Jest 使用 ts-jest 完成 TypeScript 编译testEnvironment: node运行环境为 Node而非 jsdom因为 Midway 是纯服务端框架testPathIgnorePatterns: [rootDir/test/fixtures]非常重要fixtures 目录是测试专用的模拟应用必须排除在测试匹配之外否则会被当作测试套件执行coveragePathIgnorePatterns: [rootDir/test/]覆盖率统计时忽略测试目录本身。在仓库中每个包都自带jest.config.js与jest.setup.js例如 packages/web/jest.config.js、packages/core/jest.config.js可以参考其实际写法。常见设置jest.setup.js 与四个经典问题如果需要在单测前执行一些代码可以增加jest.setup.js配置如下const path require(path); module.exports { preset: ts-jest, testEnvironment: node, testPathIgnorePatterns: [rootDir/test/fixtures], coveragePathIgnorePatterns: [rootDir/test/], setupFilesAfterEnv: [rootDir/jest.setup.js], // 预先读取 jest.setup.js };:::caution 注意jest.setup.js只能使用 js 文件。 :::setupFilesAfterEnv中的文件会在每个测试文件执行前被加载适合放置全局的初始化逻辑。下面结合官方文档给出四个最常见的实战问题与解法。示例一测试代码时间较长的问题如果测试出现下面的错误说明你的代码执行时间比较长比如连接数据库、跑任务等。如果确定代码没有问题就需要延长启动时间Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.jest 默认时间为5000ms5 秒钟可以调整到更多。可以通过在package.json启动时修改{ scripts: { test: jest --testTimeout30000 } }或者使用 midway-bin{ scripts: { test: midway-bin test --ts --testTimeout30000 } }这里的testTimeout是 jest 的启动参数。也可以在jest.setup.js文件中写入下面的代码对 jest 超时时间做调整// jest.setup.js jest.setTimeout(30000);示例二全局环境变量同理jest.setup.js也可以执行自定义的代码比如设置全局环境变量// jest.setup.js process.env.MIDWAY_TS_MODE true;这个变量本身是 Midway 识别 TypeScript 测试环境的关键开关——midwayjs/mock的create方法中process.env.MIDWAY_TS_MODE process.env.MIDWAY_TS_MODE ?? true见 packages/mock/src/creator.ts在jest.setup.js中显式设置可以确保即便不经过 mock 工具链环境变量也始终生效。示例三程序无法正常退出的处理有时候由于一些代码定时器、监听等在后台运行导致单测跑完后会无法退出进程。对于这个情况jest 提供了--forceExit参数$ jest --forceExit $ jest --coverage --forceExit使用 midway-bin 时$ midway-bin test --ts --forceExit $ midway-bin cov --ts --forceExit也可以在自定义 jest 配置文件中增加forceExit属性module.exports { preset: ts-jest, testEnvironment: node, testPathIgnorePatterns: [rootDir/test/fixtures], coveragePathIgnorePatterns: [rootDir/test/], forceExit: true, };注意--forceExit是强制退出本质上是绕过未关闭的句柄直接结束进程属于兜底方案。更好的做法是先排查并主动清理定时器、关闭服务或断开数据库连接。示例四并行改串行执行jest 默认为每个测试文件并行处理。如果测试代码中有启动端口等场景并行处理可能会导致端口冲突而报错这个时候需要加--runInBand参数。注意这个参数只能加在命令中无法在jest.config.js中配置$ jest --runInBand $ jest --coverage --runInBand使用 midway-bin 时$ midway-bin test --ts --runInBand $ midway-bin cov --ts --runInBand--runInBand让所有测试文件在当前进程中串行执行虽然会牺牲一些速度但能彻底规避端口与资源竞争问题。也可以与--forceExit组合使用。编辑器配置Jetbrains Webstorm/Idea 配置在 Jetbrains 的编辑器使用需要启用 jest 插件。由于使用了子进程的方式启动依旧需要在启动时指定加载--requirets-node/register。在 Run/Debug Configurations 中新增 Jest 配置Jest options 中填入node --requirets-node/register ./node_modules/.bin/jest这样 Jest 运行子进程时会先经过 ts-node 编译 TypeScript与命令行midway-bin test --ts的效果一致。VSCode 配置先搜索并安装 Jest Runner 插件。打开配置配置 jest 命令路径——在 jest command 处填入node --requirets-node/register ./node_modules/.bin/jest或者在工作区文件夹.vscode里面设置settings.json{ jest.pathToJest: node --requirets-node/register ./node_modules/.bin/jest --detectOpenHandles, jestrunner.jestCommand: node --requirets-node/register ./node_modules/.bin/jest --detectOpenHandles }其中--detectOpenHandles会在测试结束后列出未关闭的句柄定时器、socket 等对排查进程无法退出的问题很有帮助。由于 jest runner 插件的调试使用的是 VSCode 的调试需要单独配置 VSCode 的launch.json。在文件夹.vscode里面设置{ version: 0.0.1, configurations: [ { name: Debug Jest Tests, type: node, request: launch, runtimeArgs: [ --inspect-brk, --requirets-node/register, ${workspaceRoot}/node_modules/.bin/jest, --runInBand, --detectOpenHandles ], console: integratedTerminal, internalConsoleOptions: neverOpen } ] }这里--inspect-brk会在启动时暂停等待调试器附加--runInBand确保串行执行便于单步调试。关于 alias paths需要注意mwtsc工具不支持 Alias Path 功能。因此在使用路径别名如/service/user时需要自行在 jest 配置中增加moduleNameMapper映射或尽量避免在测试代码中依赖路径别名保持相对路径引用才能与mwtsc的编译链路完全兼容。关于 mock 数据模拟数据是一个可以在开发和测试中都通用的能力。midwayjs/mock不仅在启动与请求层面提供支持还封装了完整的 mock 数据 API见 packages/mock/src/mock.tsmockSession(app, key, value)为请求上下文 mock 一个 session 字段mockHeader(app, headerKey, headerValue)mock 请求头mockContext(app, key, value)mock 上下文属性或行为mockClassProperty(clzz, propertyName, value)/mockProperty(obj, key, value)mock 类属性与对象属性restoreAllMocks()/restoreMocks(group)统一恢复被 mock 的内容。这些能力底层的实现统一委托给midwayjs/core的MidwayMockService通过分组group默认default管理可以在afterAll或afterEach中调用restoreAllMocks()恢复现场避免 mock 污染影响后续用例。更多细节可参考仓库的 模拟数据文档。小结Midway 3.x 的测试体系可以用一句话概括约定目录结构 Jest 运行器 midwayjs/mock一站式工具集。在实际项目中建议遵循以下实践清单测试文件统一放在test目录按${name}.test.ts命名fixtures一律置于test/fixtures下并在 jest 配置中排除优先使用脚手架自带的npm test直接使用 jest 或midway-bin test --ts均可二者对 TypeScript 的支持等价每个测试文件通过beforeAll创建一次 app、afterAll统一close复用 app 实例提升效率HTTP 测试用createHttpRequest(app)链式构造请求覆盖 query、body、form、header、cookie 各种参数形态服务测试通过app.getApplicationContext().getAsync()获取单例请求级服务用createAnonymousContext().requestContext获取遇到超时、进程不退出、端口冲突等问题分别用--testTimeout、--forceExit、--runInBand对症解决编辑器场景下配置好 ts-node 启动命令即可获得可视化的测试运行与调试体验。掌握这套方法论后无论是控制器、服务还是多框架混合应用都能写出稳定、高效、可维护的单元测试为应用质量兜底。赞分享后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载相关推荐Midway 测试实战指南基于 Jest 与 midwayjs/mock 的单元测试、HTTP 测试与工程化配置Midway 测试实战指南基于 Jest 与 midwayjs/mock 的单元测试、HTTP 测试与工程化配置 在 Midway 应用开发中测试是保障代后端微服务云原生bark!完全部署教程从Pipewire配置到多设备同步播放bark!完全部署教程从Pipewire配置到多设备同步播放 bark是一款专为本地网络设计的实时同步音频流工具通过UDP multicast技术实现低延迟Android Liquid Glass深度解析构建现代玻璃态UI的实战指南Android Liquid Glass深度解析构建现代玻璃态UI的实战指南 Android Liquid Glass是一个专为Compose MultiplUI组件前端上一篇CodeContracts部署指南从源码构建到生产环境配置下一篇30亿参数逆袭130亿模型阿里WebSailor-3B改写开源智能体格局创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表