中秋猜灯谜:基于华为云码道零依赖纯 Canvas 手写一个灯会小游戏

中秋猜灯谜:基于华为云码道零依赖纯 Canvas 手写一个灯会小游戏

项目地址:https://atomgit.com/sdf56g99988/mid-autumn-huadeng-riddle

运行方式:双击 index.html,离线即玩,无需安装、无需构建、无需联网。

中秋少不了猜灯谜。这篇文章记录一个「中秋灯会 · 猜灯谜」单页小游戏的完整实现------纯手写 HTML + CSS + JavaScript(Canvas 2D),零依赖、零构建、零后端、零外链 ,所有视觉(灯笼、夜空、明月、烟花)全部由代码绘制,不加载任何一张图片、一个字体文件、一个 CDN 资源。

先看成品的灯会现场:

夜空渐变底色、漫天闪烁繁星、右上角一轮带月晕的明月,下面是 24 盏红灯笼排成几排,在夜风里轻轻摆动、灯芯火光摇曳。每一盏灯笼背后都挂着一道灯谜。


一、玩法:一场能玩的灯会

整个游戏就是一个「逛灯会、猜灯谜」的闭环:

  1. 灯会场景:Canvas 绘制夜空 + 明月 + 繁星 + 24 盏悬挂灯笼,灯笼带摆动与火光摇曳动画。
  2. 点灯笼出题:点击任意一盏灯笼,对应的灯谜以「卷轴」形式从上方垂落展开(带展开动画)。
  3. 答题 :在卷轴输入框写下谜底,回车或点「提交」。
    • 答对:该灯笼「点亮变金」+ 金光晕 + 烟花粒子特效,显示解析(谜底的扣合逻辑),计入得分。
    • 答错:灯笼轻晃 + 提示「再想想」,不扣分,可重试。
  4. 提示:同一题连续答错 3 次后「提示」按钮解锁,每用一次消耗一次全局提示机会,按「由弱到强」渐进出示 1~3 条提示。
  5. 计分:每题满分 100,按用时阶梯给分(≤10s 满分 / ≤30s 八成 / ≤60s 六成 / >60s 四成),再乘以提示扣分(每用一次提示扣两成)。
  6. 通关:24 题全部答完弹出「灯会通关」结算面板------总分、用时、答对数、评级(灯谜状元 / 榜眼 / 探花 / 再接再厉)+ 星级。

点开灯笼,卷轴垂落展开的样子:

答对一题,灯笼点亮、烟花绽放、解析展示:

答对后回到灯会,那盏灯笼已经变成金色:


二、灯谜引擎:把「判定」做成纯函数

这个小游戏最值得讲的不是画面,而是引擎与界面彻底解耦的设计。整个游戏拆成三层:

复制代码
题库(riddles.js)   → 只提供数据,零逻辑
引擎(engine.js)    → 归一化 + 判定 + 状态演算 + 计分,纯函数,无 DOM
渲染(main.js)      → Canvas 渲染与交互,只读状态

题库和引擎都用 UMD 包装(兼容 CommonJS / AMD / 全局挂载),因此同一份 engine.js 既能在浏览器里 <script> 直接引入跑游戏,也能在 Node 下 require 跑单元测试------这是它能做到「file:// 双击即玩」又能「npm test 全绿」的关键。

1. 答案归一化:让玩家怎么答都不算「冤」

玩家输入谜底的方式千奇百怪:可能带空格、用全角、写繁体、大小写混着来。如果直接 === 比对,「月餅」和「月饼」就会判错,体验很差。引擎的 normalizeAnswer 做四步归一:

js 复制代码
function normalizeAnswer(input) {
  if (input === null || input === undefined) return '';
  var s = String(input);
  s = toHalfWidth(s);   // 全角 → 半角(含全角空格)
  s = trimAll(s);       // 去掉所有空白
  s = t2s(s);           // 常见繁体 → 简体(白名单)
  s = s.toLowerCase();  // 英文 / 拼音大小写容错
  return s;
}

其中「全角转半角」利用的是 Unicode 的编码规律:全角字符 !~~(U+FF01--U+FF5E)与对应半角字符的码点恰好相差 0xFEE0,所以一个 charCodeAt 偏移就能批量转;全角空格(U+3000)单独处理:

js 复制代码
function toHalfWidth(s) {
  return String(s)
    .replace(/[\uFF01-\uFF5E]/g, function (ch) {
      return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0);
    })
    .replace(/\u3000/g, ' '); // 全角空格 → 半角空格
}

function trimAll(s) {
  return String(s).replace(/\s+/g, '');
}

繁简转换不引入任何库,用一张「常见字白名单」映射表逐字替换------只收录谜底里真正会出现的那批字(餅→饼、燈→灯、籠→笼、煙→烟、圓→圆、團→团......),够用就好,避免为了一张全量对照表拖进几百 KB 数据:

js 复制代码
var T2S = {
  '餅': '饼', '燈': '灯', '籠': '笼', '煙': '烟', '吳': '吴', '剛': '刚',
  '圓': '圆', '裡': '里', '個': '个', '們': '们', '這': '这', '對': '对',
  '為': '为', '題': '题', '節': '节', '樂': '乐', '團': '团', '滿': '满',
  // ...只收谜底相关字
};

function t2s(s) {
  return String(s).split('').map(function (c) {
    return T2S[c] || c;
  }).join('');
}

判定时先与标准答案精确比对,再遍历每题可配置的 aliases 同义/俗称白名单。关键在于:判定结果带着原因 返回 { correct, reason },reason ∈ exact | alias | wrong | empty,UI 可以据此给出不同的反馈文案(比如 alias 命中时提示「也算对,标准写法是......」):

js 复制代码
function checkAnswer(input, riddle) {
  var norm = normalizeAnswer(input);
  if (!norm) return { correct: false, reason: 'empty' };
  if (norm === normalizeAnswer(riddle.answer)) {
    return { correct: true, reason: 'exact' };
  }
  var aliases = riddle.aliases || [];
  for (var i = 0; i < aliases.length; i++) {
    if (norm === normalizeAnswer(aliases[i])) {
      return { correct: true, reason: 'alias' };
    }
  }
  return { correct: false, reason: 'wrong' };
}

注意 aliases 在比对前也要过一遍 normalizeAnswer------否则白名单里写「月饼」、玩家输入「月餅」时,归一化只对玩家输入生效,两边还是对不上。这是一个容易漏掉的细节。

2. 状态演算:不可变更新,渲染层只读

游戏状态(每盏灯笼是否已答对、尝试了几次、用了几条提示、总分、起止时间)全部放在一个纯数据对象里,所有变更都通过引擎的纯函数返回新状态 ,而不是原地修改。submitAnswer 是整个引擎的核心,看它一个函数就能看懂全部设计:

js 复制代码
function submitAnswer(state, riddle, input, now) {
  var next = cloneState(state);                 // ① 先克隆,不可变更新
  var l = findLantern(next, riddle.id);
  if (!l)      return { state: next, result: { correct: false, reason: 'notfound', /*...*/ } };
  if (l.solved) return { state: next, result: { correct: true,  reason: 'solved',   /*...*/ } };

  var t = typeof now === 'number' ? now : Date.now();  // ② 时间可注入,便于单测
  l.attempts += 1;
  var check = checkAnswer(input, riddle);
  if (check.correct) {
    l.solved = true;
    var timeMs = l.openTime != null ? Math.max(0, t - l.openTime) : 0;
    l.score = scoreAnswer(timeMs, l.hintsUsed);  // ③ 用时越短、提示越少,分越高
    next.totalScore += l.score;
    if (next.lanterns.every(function (x) { return x.solved; })) {  // ④ 全部答完→通关
      next.finished = true;
      next.endTime = t;
      next.currentId = null;
    }
  }
  return { state: next, result: { correct: check.correct, reason: check.reason,
           score: l.solved ? l.score : 0, attempts: l.attempts, finished: next.finished } };
}

几个设计点:

  1. 不可变更新 :先 cloneState 再改副本,渲染层拿到的 state 永远是只读快照,可预测、可回放、可单测。
  2. 时间可注入 :now 允许从外部传入,单测里可以精确控制「用时」,验证计分阶梯,而不必真的去 sleep。
  3. 重复提交防护 :已答对的题再提交直接返回 reason: 'solved',不会重复加分。
  4. 通关检测内聚 :every(solved) 就地在引擎里判断,UI 只读 finished 标志。

createGameState / openRiddle / submitAnswer / requestHint / summarize 这一组函数覆盖了从开局到结算的全部状态流转。

3. 计分与评级

计分公式 score = round(100 × 时长系数 × 提示系数)------答得越快、用提示越少,分越高。时长系数按阶梯取,提示每用一次扣两成、扣到零为止:

js 复制代码
function scoreAnswer(timeMs, hintsUsed) {
  var base = 100;
  var sec = (timeMs || 0) / 1000;
  var timeFactor;
  if (sec <= 10)      timeFactor = 1.0;
  else if (sec <= 30) timeFactor = 0.8;
  else if (sec <= 60) timeFactor = 0.6;
  else                timeFactor = 0.4;
  var hintFactor = Math.max(0, 1 - (hintsUsed || 0) * 0.2);
  return Math.round(base * timeFactor * hintFactor);
}

评级看「答对比例」和「得分比例」两个维度:全对且得分 ≥90% 才够格「灯谜状元」(3 星),光蒙对但拖太久、提示用光也拿不到满分评级:

js 复制代码
function gradeResult(totalScore, maxScore, correctCount, totalCount) {
  var ratio = totalCount > 0 ? correctCount / totalCount : 0;
  var scoreRatio = maxScore > 0 ? totalScore / maxScore : 0;
  if (ratio >= 1 && scoreRatio >= 0.9) return { title: '灯谜状元', stars: 3 };
  if (ratio >= 0.8) return { title: '灯谜榜眼', stars: 2 };
  if (ratio >= 0.6) return { title: '灯谜探花', stars: 1 };
  return { title: '再接再厉', stars: 0 };
}

4. 提示门槛:把「解锁条件」收进引擎

提示不是想用就用------要连续答错 3 次才解锁,且消耗全局提示机会。这个门槛逻辑也放在引擎里,用 canRequestHint 单独判断,UI 只负责拿结果去 disable 按钮:

js 复制代码
function canRequestHint(state, riddle) {
  var l = findLantern(state, riddle.id);
  if (!l || l.solved) return false;         // 已答对不需提示
  if (l.attempts < 3) return false;         // 连错 3 次才解锁
  if (state.hintsRemaining <= 0) return false; // 全局机会耗尽
  var hints = riddle.hints || [];
  if (l.hintsUsed >= hints.length) return false; // 该题提示已用完
  return true;
}

把「为什么不能用提示」拆成四个明确的分支,单测可以逐条断言;requestHint 里再返回 reason ∈ need-more-attempts | no-hint-left | no-more-hint | ok,UI 据此给出精准提示文案,而不是一句笼统的「暂不可用」。

5. 题库:数据驱动,扩充零成本

内置 24 道原创灯谜(字谜 9 / 成语谜 6 / 中秋意象谜 9),每题是一个结构化对象:

js 复制代码
{
  id: 'q01',
  category: '字谜',           // 谜面分类
  answerType: '字',           // 谜底类型:字 / 成语 / 事物
  question: '一月一日非今天',  // 谜面
  answer: '明',               // 谜底
  aliases: [],                // 同义/俗称白名单
  analysis: '"日"与"月"相合为"明"......',  // 解析(扣合逻辑)
  hints: ['...', '...', '...']       // 1~3 条渐进提示,由弱到强
}

想加题?往 riddles 数组里追加一个同结构对象即可,引擎和 UI 一行都不用改。


三、Canvas 渲染:手写每一帧

画面没有任何素材,全是 Canvas 2D 一笔一画画出来的:

  • 夜空:纵向渐变底色 + 随机分布、各自以不同频率闪烁的繁星。
  • 明月:月晕(径向渐变光晕)+ 月体 + 暗斑。
  • 灯笼:悬挂线、顶盖、椭圆主体、竖纹、灯芯火光(摇曳)、流苏、谜字标记;每盏灯有独立的摆动相位与速度,答对后叠加金光晕。
  • 烟花:答对瞬间从灯笼处炸开 72 个粒子,带重力下落与淡出。
  • 卷轴 :木质轴头 + 纸身,展开用 scale + translateY 动画。
  • 高 DPI 适配 :按 devicePixelRatio 缩放画布,Retina 屏不模糊;布局随窗口响应式重排,也支持触摸点灯笼。

主循环是标准的 requestAnimationFrame 驱动,update(dt) 推进物理(摆动、火光、烟花粒子),render(t) 重绘每一帧。dt 做了封顶(Math.min(60, ...)),防止切后台再切回来时物理量暴跳:

js 复制代码
function loop(t) {
  if (!lastFrame) lastFrame = t;
  var dt = Math.min(60, t - lastFrame);  // 封顶,避免切后台后 dt 过大
  lastFrame = t;
  update(dt, t);
  render(t);
  requestAnimationFrame(loop);
}

1. 高 DPI 适配:一个 setTransform 解决模糊

Retina 屏上 Canvas 默认会糊。解法是把画布像素尺寸放大 devicePixelRatio 倍,再用 setTransform 把坐标系缩回去------之后所有绘制代码照常按 CSS 像素写,不用管物理像素:

js 复制代码
function resize() {
  dpr = window.devicePixelRatio || 1;
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width  = Math.floor(W * dpr);   // 物理像素放大
  canvas.height = Math.floor(H * dpr);
  canvas.style.width  = W + 'px';        // CSS 尺寸不变
  canvas.style.height = H + 'px';
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 坐标系缩放,之后按 CSS 像素绘制
  layoutStars();
  layoutMoon();
  if (state) layoutLanterns();
}

2. 画一盏灯笼:摆动 + 摇曳 + 点亮

一盏灯笼由悬挂线、顶盖、椭圆灯身、竖纹、灯芯火光、流苏、谜字拼成,全部即时绘制。核心是两个随时间变化的量------摆动 (正弦)和火光摇曳(正弦扰动半径)。答对后灯身从红色渐变切换为金色渐变:

js 复制代码
function drawLantern(l, t) {
  var ls = findLanternState(l.riddleId);
  var solved = ls && ls.solved;
  var swingX = Math.sin(t * 0.001 * l.swingSpeed + l.swing) * 5;   // 夜风摆动
  var shakeX = l.shake > 0 ? Math.sin(t * 0.05) * l.shake * 9 : 0; // 答错时的抖动
  var x = l.x + swingX + shakeX, y = l.y;
  var w = LANTERN_W, h = LANTERN_H;

  // 灯身:横向线性渐变,答对切金色
  var grad = ctx.createLinearGradient(x - w / 2, y, x + w / 2, y);
  if (solved) {
    grad.addColorStop(0, '#9c6f12');
    grad.addColorStop(0.5, '#ffd76a');
    grad.addColorStop(1, '#9c6f12');
  } else {
    grad.addColorStop(0, '#7a1f15');
    grad.addColorStop(0.5, '#e74c3c');
    grad.addColorStop(1, '#7a1f15');
  }
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2);
  ctx.fill();

  // 灯芯火光:半径随正弦摇曳,未答对才画
  if (!solved) {
    var flicker = 0.82 + 0.18 * Math.sin(t * 0.02 + l.swing);
    var fireR = w * 0.20 * flicker;
    var fg = ctx.createRadialGradient(x, y, 0, x, y, fireR * 2.1);
    fg.addColorStop(0, 'rgba(255,242,190,0.9)');
    fg.addColorStop(0.5, 'rgba(255,180,80,0.45)');
    fg.addColorStop(1, 'rgba(255,120,40,0)');
    ctx.fillStyle = fg;
    ctx.beginPath();
    ctx.arc(x, y, fireR * 2.1, 0, Math.PI * 2);
    ctx.fill();
  }
  // ...顶盖 / 竖纹 / 流苏 / 谜字略
}

每盏灯用 Math.random() 生成独立的 swing(相位)与 swingSpeed(速度),所以 24 盏灯不会整齐划一地摆,而是错落有致------这是让画面「活」起来的关键。答错时的抖动复用了同一个 x 偏移:给 l.shake 置 1,再在 update 里随时间衰减回 0,就得到了一次「晃一下就停」的反馈。

3. 烟花:一个极简粒子系统

答对瞬间从灯笼处炸开 72 个粒子,每个粒子有初速度、重力加速度、空气阻尼和生命值。没有引入任何物理引擎,就是几条运动学公式:

js 复制代码
function spawnFireworks(x, y) {
  var colors = ['#FFD700', '#FF8C00', '#FF4500', '#FFB347', '#FFF8DC', '#FF6B6B', '#FFE066'];
  for (var i = 0; i < 72; i++) {
    var a = Math.random() * Math.PI * 2;          // 随机方向
    var sp = 1 + Math.random() * 4.6;             // 随机速率
    fireworks.push({
      x: x, y: y,
      vx: Math.cos(a) * sp,
      vy: Math.sin(a) * sp - 1.6,                 // 整体向上偏一点
      life: 1,
      color: colors[Math.floor(Math.random() * colors.length)]
    });
  }
}

function updateFireworks() {
  for (var i = fireworks.length - 1; i >= 0; i--) {
    var p = fireworks[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.08;     // 重力
    p.vx *= 0.99;     // 阻尼
    p.life -= 0.012;  // 生命值衰减
    if (p.life <= 0) fireworks.splice(i, 1);      // 死了就移除
  }
}

function drawFireworks() {
  for (var i = 0; i < fireworks.length; i++) {
    var p = fireworks[i];
    ctx.globalAlpha = Math.max(0, p.life);        // 用生命值做透明度 → 淡出
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, 2.2, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

vy 初始减 1.6 让烟花先向上蹿,再被重力 vy += 0.08 拉回来,形成抛物线;透明度直接绑在 life 上,粒子越接近消亡越淡。整套系统三个函数、不到 40 行,就是一朵像样的烟花。


四、工程化:5 个 commit,全程可验证

整个项目严格按语义化分 5 个 commit 推进,每个 commit 都能独立检出运行:

# Commit 内容 验证
1 chore 仓库初始化(.gitignore / package.json / README / 目录骨架) ---
2 feat(domain) 题库 riddles.js + 判定引擎 engine.js(UMD 纯函数) Node 验证通过
3 test 引擎单测 test/engine.test.js(node:test + assert,零依赖) npm test 50/50 全绿
4 feat(ui) Canvas 灯会三件套 index.html + style.css + main.js file:// 实测 8/8,控制台零报错
5 docs 完善 README(玩法 / 操作 / 题库 / 引擎 / 技术要点 / 运行 / 测试) ---

单测:50 个用例全绿

测试用 Node 内置的 node:test + node:assert,不引入任何测试框架,覆盖:题库完整性(题数 / 字段 / 类型 / id 唯一 / 分类齐全)、归一化(空白 / 全角 / 繁简 / 大小写 / 空值)、判定(精确 / 别名 / 繁体 / 答错 / 空答案)、计分阶梯、评级、渐进提示、洗牌可复现、状态演算(不可变 / 重复答 / 重复开题)、提示阶梯与机会耗尽、通关结算。

随机洗牌是单测的老大难------带 Math.random 的函数没法断言确定结果。引擎的解法是允许注入随机源 :shuffle(arr, random) 接受一个可选的 random 函数,游戏运行时默认用 Math.random,测试时传入一个固定序列,就能断言「同样的输入必然得到同样的打乱结果」:

js 复制代码
function shuffle(arr, random) {
  var rnd = typeof random === 'function' ? random : Math.random; // 可注入
  var a = arr.slice();                          // 不改原数组
  for (var i = a.length - 1; i > 0; i--) {
    var j = Math.floor(rnd() * (i + 1));        // Fisher-Yates
    var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
  }
  return a;
}

于是测试可以这样写:注入一个永远返回 0 的「随机」源,Fisher-Yates 的行为就完全确定,断言它既不打乱元素集合、又确实改变了顺序:

js 复制代码
const test = require('node:test');
const assert = require('node:assert');
const E = require('../src/engine.js');

test('shuffle 可复现:注入固定随机源', () => {
  const input = ['a', 'b', 'c', 'd', 'e'];
  const out = E.shuffle(input, () => 0);        // 注入确定性随机源
  assert.deepStrictEqual([...out].sort(), [...input].sort()); // 元素不重不漏
  assert.notDeepStrictEqual(out, input);        // 顺序确实变了
  assert.deepStrictEqual(input, ['a', 'b', 'c', 'd', 'e']);   // 原数组未被修改
});

test('答对计分:越快越高,提示扣分', () => {
  assert.strictEqual(E.scoreAnswer(5 * 1000, 0), 100);  // 5 秒内、无提示 → 满分
  assert.strictEqual(E.scoreAnswer(20 * 1000, 0), 80);  // 30 秒内 → 八成
  assert.strictEqual(E.scoreAnswer(5 * 1000, 2), 60);   // 满分但用 2 次提示 → 六成
});

本地克隆下来跑一遍:

复制代码
ℹ tests 50
ℹ pass 50
ℹ fail 0
ℹ duration_ms 401.31

file:// 实测

file:// 协议下没有模块系统,所以领域脚本全部用 UMD 挂到全局(window.MidAutumnRiddles / window.MidAutumnEngine),main.js 直接用全局引用。端到端实测 8/8 步骤全绿:渲染、点灯笼开题、答错反馈、请求提示、答对特效、重新开局,控制台零报错、零 pageerror。


五、过程剪影

这次开发由 AI 编程助手在云端沙箱里完成,从理解需求到推送上线一气呵成。几个关键节点:

任务下发后,助手先复述确认理解,并就「远端仓库推送方式」「仓库可见性」做了澄清:

设计题库时逐题校验扣合逻辑(比如「一月一日非今天 → 明」「半部春秋 → 秦」),确保每道题都经得起推敲:

五个 commit 全部完成、本地验证通过后,汇总了完整进度:

推送阶段还踩了两个有意思的坑:

坑一:lantern 是平台敏感词。 原计划仓库名叫 mid-autumn-lantern-riddle,创建远端时被拒("项目名称存在违规内容")。助手做了对照实验,确认 autumn、mid 都能正常创建、唯独 lantern 触发拦截,最后改用 huadeng(花灯)------语义上反而更贴切,中秋赏的本就是花灯。

坑二:云端沙箱的环境重置。 推送时发现沙箱在工具调用之间会重置 .git 的 refs,把 main 分支打回只有 1 个 commit。解法是先把 5 个 commit 的引用固定下来,再用一条原子命令 git reset --hard <完整commit> && git push origin main 在同一个 shell 调用内完成「恢复 + 推送」,避免中途再被重置:

最终 git push 输出 a16f4ed..5602b82 main -> main,远端 main 指向包含全部 5 个 commit 的最新节点,仓库公开可访问:

六、结语

这个项目没有用到任何框架和构建工具,却把「可玩性」和「工程质量」都做了出来:

  • 对玩家,它是一场能点亮、会放烟花、有评级的中秋灯会;
  • 对开发者,它是一次「纯函数引擎 + 数据驱动 + UMD 模块 + 零依赖单测」的练手------证明了不依赖任何第三方库,也能写出结构清晰、可测试、可扩展的前端代码。

中秋将至,祝你猜谜连中,个个状元。

相关推荐
闲云自留地7 天前
华为云计算云服务
华为云
云老大-阿里云国际站代理商9 天前
华为云国际站服务代理商:NAT网关怎么配?多台ECS共用公网IP要注意什么
服务器·华为云·云计算
闲云野鹤在人间10 天前
docker 入门 | 第5章 网络详解
网络·docker·架构·华为云·php
SatanII10 天前
容器运行时Containerd完整学习笔记|原理、安装、ctr/nerdctl/crictl实操全梳理
运维·docker·华为云·containerd
月落汀兰13 天前
从需求到上线:华为云搭建高可用 Web 站点,ECS/RDS/ELB/AS 组件协同实践
华为云·云计算
月落汀兰13 天前
云上故障怎么排查?华为云 IAM 权限、CES 监控、LTS 日志、CTS 审计完整指南
华为云·云计算
云老大-阿里云国际站代理商14 天前
华为云国际站渠道代理商:Flexus服务器网站打不开怎么办?80/443端口、安全组和Web服务怎么排查
华为云·云计算
.冰块.15 天前
华为云计算服务学习笔记|ECS + 镜像 IMS + 弹性伸缩 AS 完整踩坑实录
linux·华为云·ims·as·ecs·弹性云服务器·计算类云服务
Lysander.Jovian16 天前
存储类服务实践
华为云