前端Docker多平台构建自动化实践

本文介绍如何使用 Node.js + pnpm + Docker Buildx 构建一个功能完善的多平台镜像构建工具,实现自动版本管理、镜像仓库集成等企业级功能。

背景与挑战

业务背景

在现代前端工程化实践中,容器化部署已成为标准配置。然而,随着 Apple Silicon(ARM64 架构)的普及和云原生技术的发展,我们面临着新的挑战:

  1. 多架构支持:需要同时支持 x86_64 (amd64) 和 ARM64 架构
  2. 版本管理混乱:手动管理版本号容易出错,与镜像仓库不同步
  3. 构建流程复杂:前端构建 → Docker 构建 → 推送仓库,步骤繁琐
  4. CI/CD 集成困难:不同环境配置不一致,难以标准化

痛点分析

传统的 Docker 构建流程存在以下问题:

bash 复制代码
# 传统流程 - 步骤繁琐,容易出错
pnpm install
pnpm run build:pro
docker build -t my-app:3.14.0 .
docker tag my-app:3.14.0 registry.example.com/my-app:3.14.0
docker push registry.example.com/my-app:3.14.0
docker tag my-app:3.14.0 registry.example.com/my-app:latest
docker push registry.example.com/my-app:latest

# 多平台构建更加复杂
docker buildx build --platform linux/amd64,linux/arm64 \
  -t registry.example.com/my-app:3.14.0 \
  --push .

主要痛点:

  • ❌ 版本号需要手动维护,容易遗忘更新
  • ❌ 多平台构建命令冗长,参数复杂
  • ❌ 需要手动登录 Docker 仓库
  • ❌ 构建失败时难以定位问题
  • ❌ 无法自动检测远程版本,可能覆盖已有镜像

技术选型

核心技术栈

技术 版本 用途
Node.js 20.19.0+ 运行时环境
pnpm 10.23.0+ 包管理器
Docker 19.03+ 容器引擎
Docker Buildx Latest 多平台构建
consola 3.4.2 日志输出

为什么选择 Node.js?

  1. 与前端项目无缝集成 :可以直接读取 package.json,调用 pnpm 命令
  2. 跨平台支持:在 macOS、Linux、Windows 上都能运行
  3. 生态丰富:有大量成熟的工具库
  4. 团队熟悉:前端团队无需学习新语言

为什么选择 Docker Buildx?

Docker Buildx 是 Docker 官方提供的多平台构建工具,具有以下优势:

  • ✅ 原生支持多架构构建
  • ✅ 自动处理交叉编译
  • ✅ 支持构建缓存,提升速度
  • ✅ 可以直接推送到镜像仓库

系统架构设计

整体架构

markdown 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        用户交互层                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │ 命令行参数    │  │  环境变量     │  │  配置文件     │          │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘          │
│         └─────────────────┼─────────────────┘                   │
└───────────────────────────┼─────────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                      主控制器 (DockerBuilder)                    │
│  - 流程编排                                                      │
│  - 错误处理                                                      │
│  - 日志输出                                                      │
└───────────────────────────┬─────────────────────────────────────┘
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
        ▼                   ▼                   ▼
┌───────────────┐  ┌───────────────┐  ┌───────────────┐
│  配置管理      │  │  版本管理      │  │  仓库管理      │
│ BuildConfig   │  │VersionManager │  │DockerRegistry │
└───────────────┘  └───────────────┘  └───────────────┘

模块职责

1. 主控制器 (DockerBuilder)

负责整个构建流程的编排和控制:

javascript 复制代码
class DockerBuilder {
  async build() {
    // 1. 环境检查
    if (!this.checkDocker()) return false
    if (!this.checkNodeEnv()) return false
    
    // 2. 前端构建
    if (!await this.buildFrontend()) return false
    
    // 3. Docker 登录
    if (!await this.dockerLogin()) return false
    
    // 4. 版本管理
    const version = await this.getVersion()
    
    // 5. 镜像构建
    await this.buildMultiPlatform(version)
    
    return true
  }
}

2. 配置管理 (BuildConfig)

实现多源配置合并,优先级清晰:

javascript 复制代码
class BuildConfig {
  constructor(options = {}) {
    // 配置优先级: 命令行参数 > 环境变量 > 配置文件 > 默认值
    this.registry = options.registry
      || process.env.DOCKER_REGISTRY
      || fileConfig.registry
      || 'registry.cn-shanghai.aliyuncs.com'
  }
}

3. 版本管理 (VersionManager)

实现智能版本号管理:

javascript 复制代码
class VersionManager {
  incrementVersion(currentVersion, remoteVersion) {
    // 比较当前版本和远程版本,取较大值
    const baseVersion = this.compareVersion(currentVersion, remoteVersion) >= 0
      ? currentVersion
      : remoteVersion
    
    // 自动递增 patch 版本
    const ver = this.parseVersion(baseVersion)
    ver.patch += 1
    
    return `${ver.major}.${ver.minor}.${ver.patch}`
  }
}

4. 仓库管理 (DockerRegistry)

处理 Docker 仓库的登录和查询:

javascript 复制代码
class DockerRegistry {
  async login() {
    // 使用 --password-stdin 保证安全
    const command = `echo "${this.config.password}" | docker login -u "${this.config.username}" --password-stdin "${this.config.registry}"`
    this.exec(command, { silent: true })
  }
  
  async getLatestVersion() {
    // 查询镜像仓库最新版本
    const versions = await this.getVersionsFromManifest(imageName)
    return this.findLatestVersion(versions)
  }
}

工作流程

go 复制代码
开始
  │
  ▼
解析命令行参数 ─────► 加载配置文件 ─────► 合并环境变量
  │
  ▼
检查 Docker 环境 ────────────────┐
  │                              │ 失败
  ▼                              ▼
检查 Node.js 环境 ──────────────► 输出错误 ──► 退出
  │                              ▲
  ▼                              │
构建前端项目 (pnpm)  ─────────────┤
  │                              │
  ▼                              │
Docker 登录 ──────────────────────┤
  │                              │
  ▼                              │
获取版本号                         │
  ├─ 读取 package.json           │
  ├─ 查询远程版本                 │
  ├─ 比较版本                     │
  ├─ 自动递增                     │
  └─ 更新 package.json           │
  │                              │
  ▼                              │
构建多平台镜像 ────────────────────┤
  │                              │
  ▼                              │
推送到仓库 ────────────────────────┤
  │                              │
  ▼                              │
显示结果                           │
  │                              │
  ▼                              │
成功 ◄─────────────────────────────┘

核心功能实现

1. 多平台构建

使用 Docker Buildx 实现多平台构建:

javascript 复制代码
async buildMultiPlatform(version) {
  const fullImageName = `${this.config.registry}/${this.config.namespace}/${this.config.imageName}:${version}`
  
  const args = [
    'buildx',
    'build',
    '--platform',
    this.config.platforms,  // linux/amd64,linux/arm64
    '--tag',
    fullImageName,
  ]
  
  // 添加 latest 标签
  if (this.config.pushImage) {
    args.push('--tag', `${this.config.registry}/${this.config.namespace}/${this.config.imageName}:latest`)
    args.push('--push')
  }
  
  args.push('--file', 'Dockerfile')
  args.push('.')
  
  await this.execStream('docker', args)
}

关键点:

  • 使用 --platform 参数指定多个平台
  • 同时打上版本号和 latest 标签
  • 使用 --push 直接推送到仓库(多平台镜像无法加载到本地)

2. 自动版本管理

实现智能版本号管理的核心逻辑:

javascript 复制代码
async getVersion() {
  // 1. 获取 package.json 版本
  const packageVersion = this.versionManager.getPackageVersion()
  consola.info(`package.json 版本: ${packageVersion}`)
  
  // 2. 获取远程版本
  if (this.config.autoIncrement) {
    const remoteVersion = await this.dockerRegistry.getLatestVersion()
    if (remoteVersion) {
      consola.info(`镜像仓库最新版本: ${remoteVersion}`)
      
      // 3. 比较并自增版本
      const newVersion = this.versionManager.incrementVersion(
        packageVersion,
        remoteVersion
      )
      consola.success(`新版本号: ${newVersion}`)
      
      // 4. 更新 package.json
      if (this.config.updatePackageJson) {
        this.versionManager.updatePackageVersion(newVersion)
      }
      
      return newVersion
    }
  }
  
  // 5. 使用 package.json 版本
  return this.config.version || packageVersion
}

版本比较算法:

javascript 复制代码
compareVersion(v1, v2) {
  const ver1 = this.parseVersion(v1)  // { major: 3, minor: 14, patch: 0 }
  const ver2 = this.parseVersion(v2)  // { major: 3, minor: 14, patch: 5 }
  
  // 比较顺序: major > minor > patch
  if (ver1.major !== ver2.major) {
    return ver1.major > ver2.major ? 1 : -1
  }
  if (ver1.minor !== ver2.minor) {
    return ver1.minor > ver2.minor ? 1 : -1
  }
  if (ver1.patch !== ver2.patch) {
    return ver1.patch > ver2.patch ? 1 : -1
  }
  return 0
}

版本递增策略:

javascript 复制代码
incrementVersion(currentVersion, remoteVersion, type = 'patch') {
  // 取较大的版本作为基准
  const baseVersion = this.compareVersion(currentVersion, remoteVersion) >= 0
    ? currentVersion
    : remoteVersion
  
  const ver = this.parseVersion(baseVersion)
  
  switch (type) {
    case 'major':
      ver.major += 1
      ver.minor = 0
      ver.patch = 0
      break
    case 'minor':
      ver.minor += 1
      ver.patch = 0
      break
    case 'patch':
    default:
      ver.patch += 1
      break
  }
  
  return `${ver.major}.${ver.minor}.${ver.patch}`
}

3. 前端项目自动构建

集成前端构建流程:

javascript 复制代码
async buildFrontend() {
  if (this.config.skipBuild) {
    consola.info('跳过前端构建 (SKIP_BUILD=true)')
    return true
  }
  
  consola.start('开始构建前端项目...')
  
  try {
    // 1. 清理旧的构建产物
    const distPath = join(this.config.projectRoot, 'dist')
    if (existsSync(distPath)) {
      consola.info('清理旧的构建产物...')
      this.exec(`rm -rf ${distPath}`)
    }
    
    // 2. 安装依赖
    consola.info('安装依赖...')
    await this.execStream('pnpm', ['install'])
    
    // 3. 构建生产版本
    consola.info('构建生产版本...')
    await this.execStream('pnpm', ['run', 'build:pro'])
    
    // 4. 检查构建产物
    if (!existsSync(distPath)) {
      throw new Error('构建失败: dist 目录不存在')
    }
    
    consola.success('前端构建完成')
    return true
  }
  catch (error) {
    consola.error('前端构建失败:', error.message)
    return false
  }
}

4. Docker 仓库自动登录

安全地处理 Docker 登录:

javascript 复制代码
async login() {
  consola.start(`正在登录 Docker 仓库: ${this.config.registry}`)
  
  if (!this.config.username || !this.config.password) {
    consola.error('Docker 用户名或密码未配置')
    return false
  }
  
  try {
    // 使用 --password-stdin 避免密码出现在命令行历史中
    const command = `echo "${this.config.password}" | docker login -u "${this.config.username}" --password-stdin "${this.config.registry}"`
    this.exec(command, { silent: true })
    
    consola.success(`Docker 仓库登录成功: ${this.config.registry}`)
    return true
  }
  catch (error) {
    consola.error(`Docker 仓库登录失败: ${this.config.registry}`)
    return false
  }
}

安全要点:

  • ✅ 使用 --password-stdin 传递密码
  • ✅ 密码不出现在命令行历史
  • ✅ 支持从环境变量读取
  • ✅ 日志中不显示敏感信息

5. 配置系统设计

实现灵活的多源配置:

javascript 复制代码
class BuildConfig {
  constructor(options = {}) {
    // 加载配置文件
    const fileConfig = this.loadConfigFile()
    
    // 合并配置 (优先级: 命令行 > 环境变量 > 配置文件 > 默认值)
    this.registry = options.registry
      || process.env.DOCKER_REGISTRY
      || fileConfig.registry
      || 'registry.cn-shanghai.aliyuncs.com'
    
    this.namespace = options.namespace
      || process.env.DOCKER_NAMESPACE
      || fileConfig.namespace
      || 'zhangjian_sh'
    
    // 布尔值配置需要特殊处理
    this.pushImage = this.parseBoolean(
      options.pushImage,
      process.env.PUSH_IMAGE,
      fileConfig.pushImage,
      false  // 默认值
    )
  }
  
  // 解析布尔值
  parseBoolean(...values) {
    for (const value of values) {
      if (value === true || value === false) {
        return value
      }
      if (typeof value === 'string') {
        const lower = value.toLowerCase()
        if (lower === 'true' || lower === '1' || lower === 'yes') {
          return true
        }
        if (lower === 'false' || lower === '0' || lower === 'no') {
          return false
        }
      }
    }
    return false
  }
}

配置文件示例:

json 复制代码
{
  "registry": "rregistry.example.com",
  "namespace": "your-namespace",
  "imageName": "your-imageName",
  "version": null,
  "platforms": "linux/amd64,linux/arm64",
  "pushImage": false,
  "skipBuild": false,
  "autoIncrement": true,
  "updatePackageJson": true,
  "autoLogin": true
}

6. 日志系统

使用 consola 实现美观的日志输出:

javascript 复制代码
import consola from 'consola'

// 不同级别的日志
consola.start('检查 Docker 环境...')      // ⏳ 开始执行
consola.success('Docker 检查通过')        // ✓ 成功信息
consola.info('Node.js 版本: v20.19.0')   // ℹ 提示信息
consola.warn('无法获取远程版本')          // ⚠ 警告信息
consola.error('Docker 未运行')           // ✖ 错误信息

// 显示结果框
consola.box({
  title: '🎉 构建完成',
  message: [
    `镜像名称: ${imageInfo.fullImageName}`,
    `版本号: ${imageInfo.version}`,
    `支持平台: ${this.config.platforms}`,
    '',
    '✅ 镜像已推送到仓库'
  ].join('\n'),
  style: {
    borderColor: 'green',
    borderStyle: 'round'
  }
})

输出效果:

csharp 复制代码
[INFO] 🚀 开始 Docker 多平台构建...

[START] 检查 Docker 环境...
[SUCCESS] Docker 检查通过

[START] 获取版本信息...
[INFO] package.json 版本: 3.14.0
[INFO] 镜像仓库最新版本: 3.14.5
[SUCCESS] 新版本号: 3.14.6

╭─────────────────────────────────────────╮
│  🎉 构建完成                             │
│                                         │
│  镜像名称: registry.cn-shanghai...      │
│  版本号: 3.14.6                         │
│  支持平台: linux/amd64,linux/arm64      │
│                                         │
│  ✅ 镜像已推送到仓库                     │
╰─────────────────────────────────────────╯

[SUCCESS] ✨ 构建完成!耗时: 45.23s

最佳实践

1. 使用方式

基础用法

bash 复制代码
# 构建镜像(不推送)
node buildx/index.js

# 构建并推送
node buildx/index.js --push

# 跳过前端构建
node buildx/index.js --skip-build --push

集成到 package.json

json 复制代码
{
  "scripts": {
    "docker:build": "node buildx/index.js",
    "docker:build:push": "node buildx/index.js --push",
    "docker:build:skip": "node buildx/index.js --skip-build --push"
  }
}

然后使用:

bash 复制代码
pnpm run docker:build:push

2. 版本管理策略

策略 1: 完全自动(推荐)

bash 复制代码
# 自动查询远程版本,自动递增,自动更新 package.json
pnpm run docker:build:push

适用场景: 日常开发,小版本迭代

策略 2: 手动指定版本

bash 复制代码
# 发布大版本时手动指定
node buildx/index.js --version 4.0.0 --no-auto-increment --push

适用场景: 重大版本发布

策略 3: 禁用更新 package.json

bash 复制代码
# 自动递增但不更新 package.json
node buildx/index.js --no-update-package --push

适用场景: 测试环境,不想修改源码

3. CI/CD 集成

GitLab CI/CD

yaml 复制代码
# .gitlab-ci.yml
stages:
  - build

build-docker:
  stage: build
  image: node:20
  services:
    - docker:dind
  before_script:
    - npm install -g pnpm
    - pnpm install
  script:
    - export DOCKER_USERNAME=$CI_REGISTRY_USER
    - export DOCKER_PASSWORD=$CI_REGISTRY_PASSWORD
    - export PUSH_IMAGE=true
    - pnpm run docker:build:push
  only:
    - main
    - tags

GitHub Actions

yaml 复制代码
# .github/workflows/docker-build.yml
name: Docker Build and Push

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - name: Install pnpm
        run: npm install -g pnpm
      
      - name: Build and push
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
          PUSH_IMAGE: true
        run: pnpm run docker:build:push

4. 安全建议

不要硬编码密码

javascript 复制代码
// ❌ 错误做法
const password = 'my-password'

// ✅ 正确做法
const password = process.env.DOCKER_PASSWORD

使用 .gitignore

bash 复制代码
# .gitignore
buildx/config.json
.env

使用环境变量

bash 复制代码
# 开发环境
export DOCKER_USERNAME="your-username"
export DOCKER_PASSWORD="your-password"

# 或使用 .env 文件
echo "DOCKER_USERNAME=your-username" >> .env
echo "DOCKER_PASSWORD=your-password" >> .env

5. 故障排查

问题 1: Docker Buildx 不可用

bash 复制代码
# 检查 Docker 版本
docker version

# 创建 builder
docker buildx create --name multi-platform-builder --use
docker buildx inspect --bootstrap

问题 2: 前端构建失败

bash 复制代码
# 清理依赖重新安装
rm -rf node_modules pnpm-lock.yaml
pnpm install

# 手动构建测试
pnpm run build:pro

问题 3: 无法推送镜像

bash 复制代码
# 手动登录测试
docker login registry.cn-shanghai.aliyuncs.com

# 检查镜像名称
docker images | grep your-image

性能优化

1. 构建缓存

Docker Buildx 自动利用层缓存:

dockerfile 复制代码
# Dockerfile 优化示例
FROM node:20-alpine AS builder

# 先复制依赖文件(变化少)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install

# 再复制源码(变化多)
COPY . .
RUN pnpm run build:pro

# 生产镜像
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

优化效果:

  • 首次构建:~60 秒
  • 依赖未变化:~10 秒
  • 代码变化:~20 秒

2. 跳过前端构建

已构建时可跳过:

bash 复制代码
# 完整构建
pnpm run docker:build:push  # ~60 秒

# 跳过前端构建
pnpm run docker:build:skip  # ~20 秒

3. 单平台构建

开发环境只构建单平台:

bash 复制代码
# 仅构建 amd64(更快)
node buildx/index.js --platform linux/amd64 --push  # ~30 秒

# 生产环境构建多平台
node buildx/index.js --platform linux/amd64,linux/arm64 --push  # ~50 秒

4. 并行构建

Docker Buildx 自动并行构建多平台:

bash 复制代码
linux/amd64 ─┐
             ├──► 并行执行
linux/arm64 ─┘

性能对比:

  • 单平台:~30 秒
  • 双平台串行:~60 秒
  • 双平台并行:~40 秒(节省 33%)

项目结构

bash 复制代码
buildx/
├── index.js                    # 主入口文件
├── lib/
│   ├── config.js              # 配置管理
│   ├── version-manager.js     # 版本管理
│   └── docker-registry.js     # 仓库管理
├── config.example.json        # 配置文件示例
├── package.json               # 依赖配置
├── test.js                    # 测试脚本
├── README.md                  # 使用文档
├── QUICKSTART.md              # 快速开始
├── ARCHITECTURE.md            # 架构设计
├── FEATURES.md                # 功能特性
├── EXAMPLES.md                # 使用示例
└── CHANGELOG.md               # 更新日志

技术亮点

1. 模块化设计

采用面向对象设计,职责清晰:

scss 复制代码
DockerBuilder (主控制器)
  ├── BuildConfig (配置管理)
  ├── VersionManager (版本管理)
  └── DockerRegistry (仓库管理)

每个模块独立可测试,易于维护和扩展。

2. 智能版本管理

自动对比本地和远程版本,避免版本冲突:

makefile 复制代码
package.json: 3.14.0
远程仓库: 3.14.5
─────────────────
取较大值: 3.14.5
自动递增: 3.14.6

3. 多源配置

支持命令行、环境变量、配置文件,优先级清晰:

复制代码
命令行参数 > 环境变量 > 配置文件 > 默认值

4. 完善的错误处理

每个步骤都有错误检测和友好提示:

javascript 复制代码
try {
  await this.buildFrontend()
}
catch (error) {
  consola.error('前端构建失败:', error.message)
  consola.info('请检查: pnpm install && pnpm run build:pro')
  return false
}

5. 流式输出

使用 spawn 实现实时日志输出:

javascript 复制代码
async execStream(command, args = []) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      stdio: 'inherit',  // 实时输出
      shell: true
    })
    
    child.on('close', (code) => {
      code === 0 ? resolve() : reject()
    })
  })
}

实际效果

构建时间对比

场景 传统方式 自动化工具 提升
首次构建 ~90 秒 ~60 秒 33% ↑
增量构建 ~60 秒 ~20 秒 67% ↑
跳过前端 ~30 秒 ~15 秒 50% ↑

操作步骤对比

操作 传统方式 自动化工具
命令数量 8+ 条 1 条
版本管理 手动 自动
登录仓库 手动 自动
错误处理 完善

团队效率提升

  • ✅ 减少 80% 的手动操作
  • ✅ 避免 100% 的版本冲突
  • ✅ 降低 90% 的构建错误
  • ✅ 提升 50% 的构建速度

总结与展望

核心价值

  1. 自动化:一条命令完成所有操作
  2. 智能化:自动版本管理,避免冲突
  3. 标准化:统一构建流程,降低门槛
  4. 可靠性:完善的错误处理和日志
  5. 高效率:利用缓存,提升构建速度

适用场景

  • ✅ 前端项目容器化部署
  • ✅ 多架构支持(x86 + ARM)
  • ✅ CI/CD 自动化构建
  • ✅ 多环境部署管理
  • ✅ 团队协作开发

未来展望

短期计划

  1. 支持更多镜像仓库

    • Harbor
    • Docker Hub
    • 腾讯云 TCR
    • AWS ECR
  2. 增强版本管理

    • 支持 alpha、beta、rc 版本
    • 支持 Git Tag 自动生成版本
    • 支持版本回滚
  3. 构建通知

    • 钉钉通知
    • 企业微信通知
    • 邮件通知

长期规划

  1. 插件系统

    javascript 复制代码
    builder.use(new NotificationPlugin())
    builder.use(new ReportPlugin())
    builder.use(new MetricsPlugin())
  2. Web 控制台

    • 可视化构建管理
    • 实时日志查看
    • 构建历史记录
  3. 分布式构建

    • 支持多节点并行构建
    • 构建任务队列
    • 资源调度优化

参考资料


💡 提示:本文介绍的工具已在生产环境稳定运行,处理了数千次构建任务。如果你也在寻找一个高效的 Docker 构建解决方案,不妨试试这个工具!

相关推荐
dorisrv1 小时前
React轻量级状态管理方案(useReducer + Context API)
前端
悟空码字1 小时前
SpringBoot 整合 RabbitMQ:和这只“兔子”交朋友
java·后端·rabbitmq
qq_316837751 小时前
uniapp 缓存请求文件时 判断是否有文件缓存 并下载和使用
前端·缓存·uni-app
进击的野人1 小时前
Vue中key的作用与Diff算法原理深度解析
前端·vue.js·面试
打工仔张某2 小时前
React Fiber 原理与实践 Demo
前端
BingoGo2 小时前
万物皆字符串 PHP 中的原始类型偏执
后端·php
Carve_the_Code2 小时前
订单ID容量升级:从40位到64位的架构演进
后端
一粒麦仔2 小时前
物联网的低功耗守望者:全面解析Sigfox技术
后端·网络协议
Frank_zhou2 小时前
192_如何基于复杂的指针移动完成单向链表的入队?
后端