dockerfile-optimizer by patricio0312rev/skills
npx skills add https://github.com/patricio0312rev/skills --skill dockerfile-optimizer遵循生产环境最佳实践,构建优化、安全且缓存高效的 Docker 镜像。
| 基础镜像 | 大小 | 使用场景 |
|---|---|---|
node:20 | ~1GB | 仅限开发 |
node:20-slim | ~200MB | 通用生产环境 |
node:20-alpine |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| ~130MB |
| 对大小要求严格的生产环境 |
gcr.io/distroless/nodejs20 | ~120MB | 最高安全性 |
# Node.js
FROM node:20-alpine
# Python
FROM python:3.12-slim
# Go
FROM golang:1.22-alpine AS builder
FROM scratch AS runtime # 或 gcr.io/distroless/static
# Rust
FROM rust:1.75-alpine AS builder
FROM alpine:3.19 AS runtime
# Java
FROM eclipse-temurin:21-jdk-alpine AS builder
FROM eclipse-temurin:21-jre-alpine AS runtime
# ==================== 构建阶段 ====================
FROM node:20-alpine AS builder
WORKDIR /app
# 首先安装依赖(缓存层)
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# 复制源代码并构建
COPY . .
RUN npm run build
# 修剪开发依赖
RUN npm prune --production
# ==================== 生产阶段 ====================
FROM node:20-alpine AS production
# 安全性:创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
# 仅复制必要的文件
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
# 安全性:切换到非 root 用户
USER nextjs
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
EXPOSE 3000
CMD ["node", "dist/index.js"]
# ==================== 依赖阶段 ====================
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ==================== 构建器阶段 ====================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 构建期间禁用遥测
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ==================== 运行器阶段 ====================
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# 复制静态资源
COPY --from=builder /app/public ./public
# 为预渲染缓存设置正确的权限
RUN mkdir .next && chown nextjs:nodejs .next
# 复制构建输出
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
# ==================== 构建器阶段 ====================
FROM python:3.12-slim AS builder
WORKDIR /app
# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 创建虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ==================== 生产阶段 ====================
FROM python:3.12-slim AS production
WORKDIR /app
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 从构建器复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 复制应用程序代码
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# ==================== 构建器阶段 ====================
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# 下载依赖
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# 构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s -X main.version=$(git describe --tags --always)" \
-o /app/server ./cmd/server
# ==================== 生产阶段 ====================
FROM scratch AS production
# 为 HTTPS 复制 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# 复制二进制文件
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# ✓ 良好:变更最少 → 变更最多
FROM node:20-alpine
# 1. 系统依赖(很少变更)
RUN apk add --no-cache dumb-init
# 2. 创建用户(很少变更)
RUN adduser -D appuser
# 3. 设置工作目录
WORKDIR /app
# 4. 复制依赖文件(偶尔变更)
COPY package.json package-lock.json ./
# 5. 安装依赖(如果包文件未更改则缓存)
RUN npm ci --production
# 6. 复制源代码(频繁变更)
COPY --chown=appuser:appuser . .
USER appuser
CMD ["dumb-init", "node", "index.js"]
# ✗ 差:源代码在依赖之前
FROM node:20-alpine
WORKDIR /app
COPY . . # 任何文件变更都会使缓存失效
RUN npm install # 每次都必须重新安装
CMD ["node", "index.js"]
# 版本控制
.git
.gitignore
# 依赖项(在容器中重新安装)
node_modules
.pnpm-store
# 构建输出
dist
build
.next
out
# 开发文件
.env*.local
*.log
coverage
.nyc_output
# IDE
.idea
.vscode
*.swp
*.swo
# Docker
Dockerfile*
docker-compose*
.docker
# 文档
*.md
docs
# 测试(除非容器中需要)
__tests__
*.test.ts
*.spec.ts
jest.config.*
# ✓ 良好:在同一层中安装和清理
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# ✗ 差:分开的层(清理不会减小大小)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # 太晚了,已经在前一层中
# ✓ 最小化安装
RUN apt-get install -y --no-install-recommends package-name
# ✗ 安装不必要的推荐包
RUN apt-get install -y package-name
# Alpine 使用 apk,而不是 apt
RUN apk add --no-cache \
curl \
git \
&& rm -rf /var/cache/apk/*
# 在 Debian/Ubuntu 中创建用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# 在 Alpine 中创建用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 设置所有权并切换用户
COPY --chown=appuser:appgroup . .
USER appuser
# 在 docker-compose.yml 或 docker run 中
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
# 为安全扫描添加标签
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.description="应用程序描述"
LABEL org.opencontainers.image.licenses="MIT"
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 仅当绑定到端口 < 1024 时
security_opt:
- no-new-privileges:true
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Node.js
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Python
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# wget (Alpine)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 构建时参数
ARG NODE_ENV=production
ARG APP_VERSION=unknown
# 运行时环境变量
ENV NODE_ENV=$NODE_ENV
ENV APP_VERSION=$APP_VERSION
# 不要在 Dockerfile 中包含密钥
# 使用 docker run --env-file 或密钥管理
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # 多阶段目标
volumes:
- .:/app
- /app/node_modules # node_modules 的匿名卷
ports:
- "3000:3000"
environment:
- NODE_ENV=development
command: npm run dev
app-prod:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
# .github/workflows/docker.yml
name: Docker 构建
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录到容器注册表
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: 扫描漏洞
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
# ✓ 固定版本以确保可重现性
FROM node:20.11.0-alpine3.19
# ✗ 最新标签可能会破坏构建
FROM node:latest
FROM node:20-alpine
# ✓ COPY 更明确且更受推荐
COPY package.json .
# ✗ ADD 具有很少需要的额外功能
ADD package.json . # 仅用于 URL 或 tar 提取
# ✓ 单层,镜像更小
RUN apt-get update && \
apt-get install -y package1 package2 && \
rm -rf /var/lib/apt/lists/*
# ✗ 多层,镜像更大
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
# 查看层历史
docker history image-name
# 使用 dive 分析镜像
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest image-name
# 详细的构建输出
docker build --progress=plain -t myapp .
# 构建特定阶段
docker build --target builder -t myapp:builder .
每个优化的 Dockerfile 都应包含:
每周安装数
141
代码仓库
GitHub 星标数
20
首次出现
2026年1月24日
安全审计
安装于
opencode130
gemini-cli128
codex123
github-copilot118
cursor104
claude-code93
Build optimized, secure, and cache-efficient Docker images following production best practices.
| Base Image | Size | Use Case |
|---|---|---|
node:20 | ~1GB | Development only |
node:20-slim | ~200MB | General production |
node:20-alpine | ~130MB | Size-critical production |
gcr.io/distroless/nodejs20 | ~120MB | Maximum security |
# Node.js
FROM node:20-alpine
# Python
FROM python:3.12-slim
# Go
FROM golang:1.22-alpine AS builder
FROM scratch AS runtime # Or gcr.io/distroless/static
# Rust
FROM rust:1.75-alpine AS builder
FROM alpine:3.19 AS runtime
# Java
FROM eclipse-temurin:21-jdk-alpine AS builder
FROM eclipse-temurin:21-jre-alpine AS runtime
# ==================== Build Stage ====================
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (cache layer)
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Copy source and build
COPY . .
RUN npm run build
# Prune dev dependencies
RUN npm prune --production
# ==================== Production Stage ====================
FROM node:20-alpine AS production
# Security: Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
# Copy only necessary files
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
# Security: Switch to non-root user
USER nextjs
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
EXPOSE 3000
CMD ["node", "dist/index.js"]
# ==================== Dependencies ====================
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ==================== Builder ====================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ==================== Runner ====================
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy static assets
COPY --from=builder /app/public ./public
# Set correct permissions for prerender cache
RUN mkdir .next && chown nextjs:nodejs .next
# Copy build output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
# ==================== Builder ====================
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ==================== Production ====================
FROM python:3.12-slim AS production
WORKDIR /app
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy application code
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# ==================== Builder ====================
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s -X main.version=$(git describe --tags --always)" \
-o /app/server ./cmd/server
# ==================== Production ====================
FROM scratch AS production
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# ✓ GOOD: Least changing → Most changing
FROM node:20-alpine
# 1. System dependencies (rarely change)
RUN apk add --no-cache dumb-init
# 2. Create user (rarely changes)
RUN adduser -D appuser
# 3. Set working directory
WORKDIR /app
# 4. Copy dependency files (change occasionally)
COPY package.json package-lock.json ./
# 5. Install dependencies (cached if package files unchanged)
RUN npm ci --production
# 6. Copy source code (changes frequently)
COPY --chown=appuser:appuser . .
USER appuser
CMD ["dumb-init", "node", "index.js"]
# ✗ BAD: Source code before dependencies
FROM node:20-alpine
WORKDIR /app
COPY . . # Invalidates cache on ANY file change
RUN npm install # Must reinstall every time
CMD ["node", "index.js"]
# Version control
.git
.gitignore
# Dependencies (reinstalled in container)
node_modules
.pnpm-store
# Build outputs
dist
build
.next
out
# Development files
.env*.local
*.log
coverage
.nyc_output
# IDE
.idea
.vscode
*.swp
*.swo
# Docker
Dockerfile*
docker-compose*
.docker
# Documentation
*.md
docs
# Tests (unless needed in container)
__tests__
*.test.ts
*.spec.ts
jest.config.*
# ✓ GOOD: Install and clean in one layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# ✗ BAD: Separate layers (cleanup doesn't reduce size)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # Too late, already in previous layer
# ✓ Minimal installation
RUN apt-get install -y --no-install-recommends package-name
# ✗ Installs unnecessary recommended packages
RUN apt-get install -y package-name
# Alpine uses apk, not apt
RUN apk add --no-cache \
curl \
git \
&& rm -rf /var/cache/apk/*
# Create user in Debian/Ubuntu
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Create user in Alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Set ownership and switch user
COPY --chown=appuser:appgroup . .
USER appuser
# In docker-compose.yml or docker run
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
# Add labels for security scanning
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.description="Application description"
LABEL org.opencontainers.image.licenses="MIT"
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to port < 1024
security_opt:
- no-new-privileges:true
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Node.js
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Python
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# wget (Alpine)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Build-time arguments
ARG NODE_ENV=production
ARG APP_VERSION=unknown
# Runtime environment variables
ENV NODE_ENV=$NODE_ENV
ENV APP_VERSION=$APP_VERSION
# Don't include secrets in Dockerfile
# Use docker run --env-file or secrets management
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # Multi-stage target
volumes:
- .:/app
- /app/node_modules # Anonymous volume for node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
command: npm run dev
app-prod:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
# .github/workflows/docker.yml
name: Docker Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
# ✓ Pinned versions for reproducibility
FROM node:20.11.0-alpine3.19
# ✗ Latest tags can break builds
FROM node:latest
FROM node:20-alpine
# ✓ COPY is explicit and preferred
COPY package.json .
# ✗ ADD has extra features rarely needed
ADD package.json . # Only use for URLs or tar extraction
# ✓ Single layer, smaller image
RUN apt-get update && \
apt-get install -y package1 package2 && \
rm -rf /var/lib/apt/lists/*
# ✗ Multiple layers, larger image
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
# View layer history
docker history image-name
# Analyze image with dive
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest image-name
# Detailed build output
docker build --progress=plain -t myapp .
# Build specific stage
docker build --target builder -t myapp:builder .
Every optimized Dockerfile should include:
Weekly Installs
141
Repository
GitHub Stars
20
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode130
gemini-cli128
codex123
github-copilot118
cursor104
claude-code93
Azure 升级评估与自动化工具 - 轻松迁移 Functions 计划、托管层级和 SKU
99,100 周安装
GMGN Portfolio:命令行工具查询Solana/BSC/Base链钱包投资组合与交易统计
1,100 周安装
UI/UX Pro Max - 50+样式与97配色方案,网页移动端设计指南与无障碍访问规范
1,000 周安装
Laravel架构模式指南:生产级开发模式与最佳实践
1,100 周安装
Kotlin Exposed ORM 模式指南:DSL查询、DAO、事务管理与生产配置
1,100 周安装
免费AI数据抓取智能体:自动化收集、丰富与存储网站/API数据
1,100 周安装
GraphQL 操作最佳实践指南:查询、变更、订阅编写与优化技巧
1,100 周安装