墨穗app.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v1.0.165 · 墨穗笔记
笔记

Notebase墨穗
静水流深,落墨成穗。

0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →

笔记

0
加载中...

工具

0

此页用于记录用户反馈问题后的每一次改进

关于

笔记用法

“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。

md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。

要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。

工具用法

在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。

要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。

工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。

隐藏笔记

写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。

适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。

不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。

数据安全

你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。

技术

全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。

理念

这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。

原则

不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。

更多

产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。

举报

如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。

趋势

// 点击导航加载发现
归档
// 归档为空
最近浏览
// 暂无浏览记录
发布
// 加载中...
用户发布
// 加载中...
用户管理
// 加载中...
访问统计
// 加载中...
内容审核
// 加载中...
个人信息
// 加载中...
返回首页

第1部分 — 容器化应用并在本地Kubernetes中运行

2026-07-08编程开发

嘿,各位!今天咱们来搞点实际的东西。这篇笔记是我“DevOps 101”系列的第一篇,目标是带你从零开始,把一个空文件夹变成能在本地Kubernetes集群里跑起来的应用。整个过程大概40分钟,跟着我一步步走,保证你学完能自己动手。

先说说你能学到啥:

  • 用Node.js + Express写一个简单的Todo API
  • 把它打成一个Docker镜像(容器化)
  • 用kind在本地建一个Kubernetes集群
  • 手写YAML文件部署应用(不用那些偷懒的快捷命令),真正理解K8s怎么干活
  • 从你本机访问集群里跑着的应用

这个系列叫“DevOps 101 — 从零学K8s、Helm、ArgoCD”,所有部分都用同一个叫todo-ops的应用。后面我们会慢慢加数据库、配置、Ingress、Helm,最后用ArgoCD做GitOps。今天这第一部分就是地基。

准备工作

先把工具装好。你需要这些:

  • Docker(或者Docker Desktop / OrbStack)—— 确保它在运行
  • kind —— 在Docker里创建K8s集群的工具
  • kubectl —— 和Kubernetes对话的命令行工具
  • Node.js 20+ 和 npm —— 本地跑应用用的
  • git —— 版本管理,后面GitOps部分很关键

安装步骤

macOS用户(用Homebrew,没有的话先装一个):

# 装Homebrew(跳过如果你已经有了)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Docker Desktop(或者用OrbStack:brew install --cask orbstack)
brew install --cask docker

# 剩下的CLI工具
brew install kind kubectl node git

装完后打开Docker Desktop(或OrbStack),等它显示“Running”再继续。

Linux用户(Ubuntu/Debian):

# Docker Engine
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"   # 让你不用sudo就能跑docker,记得登出再登入生效

# kubectl
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -m 0755 kubectl /usr/local/bin/kubectl && rm kubectl

# kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
sudo install -m 0755 kind /usr/local/bin/kind && rm kind

# Node.js 20(通过NodeSource) + git
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git

💡 如果你用的是Apple Silicon(M1/M2/M3),把上面Linux命令里的amd64改成arm64。macOS用brew装就不用操心这个。

装完后跑一下这些命令,每个都应该输出版本号,别报“command not found”:

docker --version
kind --version
kubectl version --client
node --version
git --version

为什么要容器化?

这问题其实挺经典的:你写的代码在你自己机器上跑得好好的,但到别人机器上或者服务器上就各种报错。原因无非是Node.js版本不对、缺了个库、环境变量没设对……烦死个人。

容器化就是解决这个痛点的。它把你的应用和它需要的所有东西(运行时、依赖、配置)打包成一个标准化的“盒子”。这个盒子在任何有Docker的地方跑起来都一样。Docker镜像就是那个静态的打包文件(有点像安装包),而容器就是运行起来的镜像(就像进程)。

第一步:创建项目

先建个目录,初始化git仓库:

mkdir todo-ops && cd todo-ops && git init
  • mkdir todo-ops:创建项目文件夹
  • cd todo-ops:进去,之后所有命令都在这个目录里跑
  • git init:把这个目录变成git仓库,方便追踪改动。后面Part 6做GitOps会用到

你应该看到输出:

Initialized empty Git repository in /你的路径/todo-ops/.git/

检查点1:跑pwd,路径应该以/todo-ops结尾。跑ls -a,应该能看到.git目录。如果搞错了(比如在父目录跑了git init),用rm -rf .git删掉那个错误的.git,然后cd到正确目录重来。

第二步:写Node.js应用

我们的Todo API很简单:一个HTTP服务,能查看和添加待办事项。这期先不接数据库,数据存在内存里——重启就没了,故意的,下期我们会换成Postgres。

需要两个文件:package.json(项目配置)和server.js(代码)。

2a. package.json

创建package.json:

{
  "name": "todo-api",
  "version": "1.0.0",
  "description": "Todo API - DevOps 101",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  }
}

解释一下各个字段:

  • name、version、description:项目基本信息
  • main:入口文件
  • scripts.start:定义了npm start等于跑node server.js。这样启动应用就不用记文件名了
  • dependencies.express:声明我们需要Express框架。^4.19.2意思是“4.19.2及以上,但保持在4.x版本内”

2b. server.js

创建server.js:

const express = require("express");
const app = express();
const PORT = 3000;

// 允许Express解析请求体中的JSON(POST请求需要)
app.use(express.json());

// 临时“数据库”:一个内存数组
// 重启应用就丢了——下期我们会换成Postgres
let todos = [
  { id: 1, title: "读DevOps 101第1部分", done: false },
];
let nextId = 2;

// GET /todos → 返回所有待办事项(JSON格式)
app.get("/todos", (req, res) => {
  res.json(todos);
});

// POST /todos → 添加一个新待办。请求体需要包含{"title": "..."}
app.post("/todos", (req, res) => {
  const title = req.body && req.body.title;
  if (!title) {
    return res.status(400).json({ error: "title is required" });
  }
  const todo = { id: nextId++, title, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

// GET /healthz → 健康检查探针。只要应用活着就返回200
app.get("/healthz", (req, res) => {
  res.status(200).send("OK");
});

// 监听端口3000,绑定所有网络接口(0.0.0.0)——这在容器里很重要
app.listen(PORT, () => {
  console.log(`Todo API listening on port ${PORT}`);
});

几个关键点:

  • express.json()是一个中间件,它在每个请求处理前运行,把JSON格式的请求体解析成req.body。没有这行,POST请求就读不到title
  • todos和nextId是内存状态,简单粗暴,方便学习。数据不持久
  • 三个路由对应三个端点:GET /todos、POST /todos、GET /healthz
  • /healthz返回200 OK——后面Part 4会用这个端点做Kubernetes的健康检查。提前写好,保持一致
  • app.listen(PORT)打开端口等待请求。console.log让我们知道应用已经启动

检查点2:跑ls,应该看到package.json和server.js。跑node -c server.js检查语法——没有输出就是对的。如果报错,看看是哪行语法有问题(漏括号、漏逗号之类的),修好再试。

第三步:本地跑起来看看

在容器化之前,先确认应用在裸机(你本机)上能跑。这是个好习惯:出问题好定位——如果这步就挂了,那问题不在Docker或K8s,在代码本身。

npm install

这行命令会读package.json,下载Express库到node_modules文件夹,并生成package-lock.json锁定版本。输出大概是这样的:

added 63 packages, and audited 64 packages in 2s

然后启动应用:

npm start

你应该看到:

> todo-api@1.0.0 start
> node server.js

Todo API listening on port 3000

好,应用在跑了!现在打开另一个终端窗口,测试一下:

# 获取所有待办事项
curl http://localhost:3000/todos

你应该得到:

[{"id":1,"title":"读DevOps 101第1部分","done":false}]

再试试添加一个:

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "学Kubernetes"}'

返回:

{"id":2,"title":"学Kubernetes","done":false}

再查一次GET /todos,应该能看到两条记录了。最后测健康检查:

curl http://localhost:3000/healthz

输出应该是OK。

搞定了就按Ctrl+C停掉应用。

检查点3:跑完npm install后,node_modules目录和package-lock.json文件应该存在。三个curl命令都应该返回正确的响应。如果有问题,检查是不是端口被占了(试试换端口),或者代码有没有拼写错误。

第四步:写Dockerfile

现在要把应用打包成Docker镜像。Dockerfile就是一个配方文件,告诉Docker怎么构建镜像。

创建Dockerfile(注意没有扩展名):

# 第一阶段:依赖安装
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# 第二阶段:运行
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]

解释一下这个Dockerfile在干嘛:

第一阶段(deps):

  • FROM node:20-alpine AS deps:基于Node.js 20的Alpine Linux镜像(超轻量,才几十MB),并给这个阶段起名叫deps
  • WORKDIR /app:设置工作目录为/app
  • COPY package.json package-lock.json ./:只复制依赖配置文件,不复制代码。这样可以利用Docker的缓存机制——只要依赖没变,这层就不用重跑
  • RUN npm ci --only=production:npm ci和npm install类似,但它严格按package-lock.json装依赖,更快更可靠。--only=production意思是只装生产依赖(不装devDependencies),减小镜像体积

第二阶段(runner):

  • FROM node:20-alpine AS runner:又是一个干净的Node.js Alpine镜像
  • WORKDIR /app:同样设置工作目录
  • COPY --from=deps /app/node_modules ./node_modules:从第一阶段复制装好的node_modules过来。这样最终镜像里只有依赖,没有构建工具
  • COPY server.js ./:复制应用代码
  • EXPOSE 3000:告诉Docker这个容器会监听3000端口(这只是文档说明,不实际打开端口)
  • CMD ["node", "server.js"]:容器启动时执行的命令

这种多阶段构建的好处:最终镜像只包含运行需要的东西,不包含npm、构建工具等,体积小、安全。你可以对比一下——如果只用单阶段,镜像可能200MB+,现在只有120MB左右。

第五步:构建并测试Docker镜像

5a. 构建镜像

docker build -t todo-ops:latest .
  • docker build:构建命令
  • -t todo-ops:latest:给镜像打标签,名字叫todo-ops,版本latest
  • .:构建上下文是当前目录(Dockerfile所在位置)

运行后你会看到类似这样的输出:

[+] Building 15.2s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 308B
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load metadata for docker.io/library/node:20-alpine
 => [deps 1/3] FROM docker.io/library/node:20-alpine
 => [internal] load build context
 => => transferring context: 586B
 => [deps 2/3] COPY package.json package-lock.json ./
 => [deps 3/3] RUN npm ci --only=production
 => [runner 1/3] WORKDIR /app
 => [runner 2/3] COPY --from=deps /app/node_modules ./node_modules
 => [runner 3/3] COPY server.js ./
 => exporting to image
 => => naming to docker.io/library/todo-ops:latest

看到FINISHED就说明成功了。跑一下确认:

docker images

应该能看到todo-ops,大小大概120MB。

5b. 测试容器

先跑起来:

docker run -d --name todo-container -p 3000:3000 todo-ops:latest
  • -d:后台运行(detached模式)
  • --name todo-container:给容器起个名字
  • -p 3000:3000:把主机的3000端口映射到容器的3000端口。左边的3000是主机端口,右边是容器端口
  • todo-ops:latest:要运行的镜像

跑完后,用docker ps确认容器在运行。然后测试:

curl http://localhost:3000/todos

应该和之前一样返回数据。测试完停掉并删除容器:

docker stop todo-container && docker rm todo-container

检查点4:docker images应该显示todo-ops镜像。docker run之后,docker ps应该显示容器在运行。curl请求应该返回正确结果。如果容器起不来,用docker logs todo-container看日志。

第六步:创建本地Kubernetes集群

终于到K8s了!我们用kind(Kubernetes in Docker)在本地创建集群。kind实际上是在Docker容器里跑K8s节点,所以你的Docker Desktop得开着。

kind create cluster --name todo-cluster
  • kind create cluster:创建集群
  • --name todo-cluster:给集群起个名字,方便后面管理

输出应该是:

Creating cluster "todo-cluster" ...
 ✓ Ensuring node image (kindest/node:v1.31.0) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-todo-cluster"
You can now use your cluster with:

kubectl cluster-info --context kind-todo-cluster

检查一下集群状态:

kubectl cluster-info --context kind-todo-cluster

应该看到类似:

Kubernetes control plane is running at https://127.0.0.1:xxxxx
CoreDNS is running at https://127.0.0.1:xxxxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

再看看节点:

kubectl get nodes

输出:

NAME                          STATUS   ROLES           AGE   VERSION
todo-cluster-control-plane   Ready    control-plane   2m    v1.31.0

很好!一个单节点集群已经跑起来了。这个节点其实是一个Docker容器,你可以用docker ps看到它。

检查点5:kubectl cluster-info不报错。kubectl get nodes显示一个Ready状态的节点。如果kubectl连不上,检查一下context对不对:kubectl config current-context应该显示kind-todo-cluster。

第七步:把镜像加载到集群

这里有个坑:我们刚才构建的镜像在本地Docker里,但kind集群跑在Docker容器里——它们是隔离的。所以我们需要把镜像加载到集群节点里。

kind load docker-image todo-ops:latest --name todo-cluster
  • kind load docker-image:加载镜像到集群节点
  • todo-ops:latest:要加载的镜像
  • --name todo-cluster:指定集群名字

输出:

Image: "todo-ops:latest" with ID "sha256:xxxxx" not yet present on node "todo-cluster-control-plane"
Loading image "todo-ops:latest" ...

搞定。现在集群节点里有了这个镜像。

检查点6:命令不报错。你可以用docker exec进到集群节点里确认:docker exec -it todo-cluster-control-plane crictl images | grep todo-ops,应该能看到镜像。

第八步:部署应用到Kubernetes

现在要写真正的Kubernetes部署文件了。我们会创建两个资源:

  1. Deployment:告诉K8s“我要跑一个应用,保持它一直运行”
  2. Service:让集群内外的其他东西能访问这个应用

8a. 创建命名空间

先创建一个专用命名空间,把资源组织起来:

kubectl create namespace todo-ops

输出:namespace/todo-ops created

8b. Deployment YAML

创建k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-api
  namespace: todo-ops
  labels:
    app: todo-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: todo-api
  template:
    metadata:
      labels:
        app: todo-api
    spec:
      containers:
      - name: todo-api
        image: todo-ops:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"

逐行解释:

  • apiVersion: apps/v1:Deployment属于apps/v1API组,这是稳定版
  • kind: Deployment:资源类型
  • metadata.name:Deployment的名字
  • metadata.namespace:所属命名空间
  • metadata.labels:Deployment自己的标签
  • spec.replicas: 2:要跑2个Pod副本(高可用)
  • spec.selector.matchLabels:Deployment通过这个标签选择它管理的Pod
  • spec.template:Pod模板,定义Pod长什么样
    • metadata.labels:Pod的标签,必须匹配上面的matchLabels
    • spec.containers:容器定义
      • name:容器名字
      • image:要跑的镜像
      • imagePullPolicy: IfNotPresent:如果镜像本地没有才去拉。因为我们是用kind load加载的,所以设为这个值
      • ports.containerPort:容器监听的端口
      • resources:资源限制
        • requests:K8s保证至少给这么多资源
        • limits:最多能用这么多
        • 内存单位:Mi=兆字节,Gi=吉字节
        • CPU单位:100m=0.1个CPU核心,200m=0.2个核心

8c. Service YAML

创建k8s/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: todo-api-service
  namespace: todo-ops
spec:
  type: NodePort
  selector:
    app: todo-api
  ports:
  - protocol: TCP
    port: 3000
    targetPort: 3000
    nodePort: 30080

解释:

  • apiVersion: v1:Service是核心API,属于v1
  • kind: Service:资源类型
  • spec.type: NodePort:服务类型。NodePort会在每个集群节点上开一个端口(30080),把外部流量转发到Pod
  • spec.selector:通过标签选择后端Pod(必须匹配Deployment里Pod的标签)
  • spec.ports:端口映射
    • port:Service自己的端口(集群内部访问用)
    • targetPort:转发到Pod的哪个端口
    • nodePort:节点上开的端口(范围30000-32767)。不指定的话K8s会随机分配

8d. 应用YAML

先创建k8s目录:

mkdir k8s

然后把两个YAML文件放进去。现在部署:

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

输出:

deployment.apps/todo-api created
service/todo-api-service created

检查状态:

kubectl get pods -n todo-ops

应该看到两个Pod都在Running状态:

NAME                       READY   STATUS    RESTARTS   AGE
todo-api-xxxxx-xxxxx       1/1     Running   0          30s
todo-api-xxxxx-xxxxx       1/1     Running   0          30s

再看看Service:

kubectl get svc -n todo-ops

输出:

NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
todo-api-service   NodePort   10.96.xxx.xxx   <none>        3000:30080/TCP   30s

检查点7:两个Pod都是Running状态。Service显示NodePort类型,端口映射是3000:30080。如果Pod一直Pending或CrashLoopBackOff,用kubectl describe pod <pod名> -n todo-ops和kubectl logs <pod名> -n todo-ops排查。

第九步:从本机访问Kubernetes里的应用

现在应用跑在集群里了,怎么访问它?因为我们用的是NodePort类型Service,可以通过集群节点的IP加NodePort来访问。

先获取集群节点的IP:

kubectl get nodes -o wide

输出:

NAME                          STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP
todo-cluster-control-plane   Ready    control-plane   5m    v1.31.0   172.18.0.2    <none>

内部IP是172.18.0.2,但这是Docker网络内部的地址,从你的宿主机不一定能直接访问。别急,kind有办法——它会把节点端口映射到宿主机。

实际上,kind创建集群时,默认会把控制平面节点的某些端口映射出来。但更靠谱的方式是用kubectl port-forward:

kubectl port-forward service/todo-api-service 8080:3000 -n todo-ops

这个命令会在本地开一个端口8080,把流量转发到集群里的Service的3000端口。输出:

Forwarding from 127.0.0.1:8080 -> 3000
Forwarding from [::1]:8080 -> 3000

现在打开另一个终端窗口,测试:

curl http://localhost:8080/todos

应该返回:

[{"id":1,"title":"读DevOps 101第1部分","done":false}]

再加一个:

curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "成功部署到K8s"}'

返回:

{"id":2,"title":"成功部署到K8s","done":false}

再查健康检查:

curl http://localhost:8080/healthz

输出:OK

牛逼!你的应用成功跑在Kubernetes集群里了!而且有两个副本,如果一个挂了,另一个还能服务。

检查点8:kubectl port-forward命令在后台运行,不报错。三个curl请求都返回正确结果。如果连不上,检查port-forward是不是还在跑,或者试试curl -v看详细错误。

清理

测试完了,关掉port-forward(按Ctrl+C)。然后可以删掉集群:

kind delete cluster --name todo-cluster

输出:Deleted clusters: ["todo-cluster"]

这会删掉整个集群,包括所有Pod、Service等资源。本地镜像还在,下次可以直接用。

小结

今天你做了这些事:

  1. 写了一个Node.js Todo API应用
  2. 用Dockerfile容器化了它
  3. 在本地用kind创建了Kubernetes集群
  4. 手写YAML文件部署了Deployment和Service
  5. 通过port-forward从本机访问了集群里的应用

这些是K8s最基础但最核心的概念。下一期我们会加入Postgres数据库,用ConfigMap和Secret管理配置,让应用真正有状态。

有什么问题随时留言讨论!

相似推荐
uBlock Origin 开发版被 Chrome 商店拒绝:一场关于扩展单一用途政策的争议逆向工程实战:我如何绕过亚马逊Kindle网页端的DRM加密Heroku的丑陋秘密:云之王如何背离Rails并欺骗客户Firefox 成为最后一个支持 uBlock Origin 的主流浏览器:广告拦截的终局之战GitHub Codespaces 深度解读:云端开发环境如何重塑编码工作流认知负荷才是关键:软件设计的根本度量
编写使用方法
Markdown 格式 · Ctrl+Enter 确定
0 字新建笔记
欢迎回来
登录你的墨穗笔记账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属墨穗笔记
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

隐私提醒

取消
编辑工具
受控分享
为这篇笔记生成限时 / 带密码的临时链接
关闭