Node.js新手教程:从零开始学习服务器端JavaScript
大家好,我是小张,一个刚毕业的大专计算机应届生。去年秋招的时候,靠着自己在家自学前端(主要是 Vue + TypeScript)混进了一家做 Web3 工具链的创业公司,现在远程办公中——每天早上8点准时坐到电脑前,咖啡还没泡好,VSCode 就已经开了三个窗口了。
说实话,我一开始只想着写写页面、调调样式,做个“切图仔”也挺香。但入职不到两周,CTO 直接甩给我一个任务:“你不是会 JS 吗?后端接口也顺手搞一下吧。” 当时我内心OS:我特么只会 console.log 啊!但转念一想,这不就是涨薪跳槽的绝佳机会吗?于是咬咬牙,硬着头皮啃起了 Node.js。
今天这篇文章,就是想给和我一样从零起步的兄弟们,分享一段真实的“血泪史”——如何用 Node.js 写出第一个能跑在生产环境的服务端程序。我会结合我们团队最近上线的一个区块链项目运营后台的真实场景,聊聊踩过的坑、熬过的夜,以及为什么 GitHub 上那些 star 很高的模板代码其实根本不能直接抄。
起因:产品经理要一个“实时监控钱包余额”的运营面板
事情是这样的。我们公司主要做 NFT 发行工具,最近老板为了“精细化运营”,要求产品加一个功能:运营同学能在后台实时看到用户钱包里有多少代币,然后针对性推送空投活动。
乍一听很简单——前端轮询接口不就完了?但测试同学一句话把我问懵了:“如果同时有 1000 个运营开着页面,每秒请求一次,你的服务器扛得住吗?” 我当场冷汗直流。
更麻烦的是,这个数据要对接 以太坊节点,每次查询都要走 JSON-RPC,延迟高不说,还容易被限流。运维大哥(人称“K8s 教父”)冷冷地丢下一句:“别让我的集群因为你写的垃圾服务崩了。”
那一刻,我知道:光会 fetch 是不够的。得上 Node.js + WebSocket + 缓存策略 的组合拳。
第一步:别再用 http.createServer 手搓了!
很多新手教程(包括我最早看的)都喜欢从原生 http 模块开始:
const http = require('http');
http.createServer((req, res) => {
res.end('Hello World');
}).listen(3000);
看起来很酷,对吧?但现实是——你不会在真实项目里这么干。就像没人会徒手造轮子去上班一样。
我们团队用的是 Express,轻量、生态成熟、中间件丰富。而且它和前端的 Express Router 风格一致,我这种前端转过来的学起来毫无压力。
初始化一个 Express 项目超级简单:
mkdir wallet-monitor-backend
cd wallet-monitor-backend
npm init -y
npm install express
然后写个最简 server:
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
});
别笑!这个 /health 接口可是 K8s 的救命稻草。运维配置的 liveness probe 就靠它判断服务是不是假死。上周五晚上我就因为忘写这个,导致 Pod 被无限重启,差点通宵。
第二步:对接区块链数据 —— 别把密钥写死在代码里!
接下来要调用以太坊节点。我们用的是 ethers.js,比 web3.js 更现代,API 也更清爽。
但问题来了:节点 URL 和 API Key 怎么管理?
我第一次提交代码,直接把 Infura 的 key 写在 config.js 里,push 到 GitHub 私有仓库。结果 CI 自动触发安全扫描,Slack 瞬间炸锅:“🚨 SECRET LEAK DETECTED”。
运维老哥直接 @ 我:“你是想让我帮你付 10 万刀的账单吗?”
教训惨痛。后来改用 环境变量 + .env 文件(但不提交到 Git):
# .env
INFURA_PROJECT_ID=your_real_id_here
ETHEREUM_NETWORK=mainnet
代码里用 dotenv 加载:
require('dotenv').config();
const { INFURA_PROJECT_ID, ETHEREUM_NETWORK } = process.env;
const provider = new ethers.InfuraProvider(
ETHEREUM_NETWORK,
INFURA_PROJECT_ID
);
记住:.env 必须加到 .gitignore 里!我们团队的 pre-commit hook 还加了脚本,一旦检测到代码里有 infura.io 字样就直接拒绝提交——防呆设计,真香。
第三步:性能优化 —— 别让运营刷新页面搞崩服务器
回到核心问题:如何高效获取钱包余额?
最初我傻乎乎地在每个 /balance/:address 请求里都去查链上:
app.get('/balance/:address', async (req, res) => {
const balance = await provider.getBalance(req.params.address);
res.json({ balance: ethers.formatEther(balance) });
});
结果压力测试一跑,QPS 刚过 50,Infura 就返回 429 Too Many Requests。运营同学还没看到数据,服务先挂了。
解决方案?缓存 + 异步更新。
我们引入了 Redis(运维说:“你要是不用缓存,就滚去用 PHP”),配合 node-cache 做本地内存兜底:
| 缓存层级 | TTL | 用途 |
|---|---|---|
| 内存 (node-cache) | 30 秒 | 防止同一地址高频请求 |
| Redis | 5 分钟 | 跨实例共享,持久化 |
关键代码:
const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 30 });
app.get('/balance/:address', async (req, res) => {
const { address } = req.params;
// 先查内存
let balance = myCache.get(address);
if (balance) {
return res.json({ balance, source: 'cache' });
}
// 再查 Redis(伪代码)
balance = await redis.get(`balance:${address}`);
if (balance) {
myCache.set(address, balance); // 回填内存
return res.json({ balance, source: 'redis' });
}
// 最后查链上
try {
const rawBalance = await provider.getBalance(address);
balance = ethers.formatEther(rawBalance);
// 写入两级缓存
myCache.set(address, balance);
await redis.setex(`balance:${address}`, 300, balance); // 300秒
res.json({ balance, source: 'blockchain' });
} catch (err) {
console.error('Chain query failed:', err);
res.status(500).json({ error: 'Failed to fetch balance' });
}
});
上线后,Infura 调用量下降了 98%,运营同学疯狂点赞。产品经理甚至想把这个“技术亮点”写进周报里邀功(笑)。
第四步:实时性?WebSocket 才是王道!
虽然缓存解决了性能问题,但运营还是抱怨:“我刚转了 ETH 进去,页面怎么还不刷新?”
这时候轮询已经 out 了。我们决定上 WebSocket,用 ws 库(轻量,无依赖):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server }); // 复用 Express server
wss.on('connection', (ws, req) => {
console.log('New client connected');
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'subscribe') {
// 订阅某个地址
subscribeAddress(ws, msg.address);
}
});
});
配合定时任务(用 node-cron)每 30 秒拉一次链上最新余额,如果有变化就推送给订阅的客户端:
const cron = require('node-cron');
cron.schedule('*/30 * * * * *', async () => {
for (const address of subscribedAddresses) {
const newBalance = await getBalanceFromCacheOrChain(address);
if (newBalance !== lastKnownBalance[address]) {
// 广播给所有订阅该地址的客户端
broadcastToSubscribers(address, newBalance);
lastKnownBalance[address] = newBalance;
}
}
});
前端用 useEffect 建立连接,体验丝滑如德芙。运营同学终于不用狂按 F5 了——虽然他们现在的问题变成了:“能不能自动弹窗提醒我?”(产品经理,你听见了吗?)
GitHub 不是终点,而是起点
很多人以为把代码 push 到 GitHub 就完事了。但在我们团队,CI/CD 流程才是真正的试金石。
我们的 GitHub Actions 配置如下:
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npm run lint
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to K8s
run: |
echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
kubectl --kubeconfig=kubeconfig set image deployment/wallet-api \
wallet-api=ghcr.io/our-org/wallet-api:${{ github.sha }}
每次 merge 到 main,自动构建 Docker 镜像并滚动更新 K8s Deployment。运维说:“只要你的镜像能跑起来,我就给你加星。”(当然,他加的是 Slack 表情 😅)
给新手的几条真心话
- 别怕犯错:我第一次部署就把数据库密码写成了明文,被全组围观。但正是这些“社死”时刻让我成长最快。
- 善用社区:GitHub 上搜
awesome-nodejs,一堆高质量资源。但别盲目 copy,先看 issue 区有没有坑。 - 关注安全:永远不要信任前端传来的任何数据。
body-parser解析后记得校验,推荐用zod。 - 日志很重要:用
winston或pino结构化日志,K8s 里查起来快如闪电。 - 你不是一个人在战斗:我们团队每周五下午有“Bug Bash”环节,一起 review 代码、吐槽需求,氛围超好。
写在最后
从那个连 process.env 都不知道是什么的小白,到现在能独立负责一个微服务模块,Node.js 给了我从前端走向全栈的底气。虽然工资还没翻倍(哭),但至少——我不再害怕后端这个词了。
如果你也是大专出身、自学成才,正在焦虑自己的职业路径,我想说:技术栈没有高低贵贱,解决问题的能力才是硬通货。管你是用 Express 还是 NestJS,只要能让运营少骂一句“这破系统又卡了”,你就是团队 MVP。
好了,咖啡喝完了,该去修下一个 Bug 了。如果你觉得这篇文章有点用,欢迎去 GitHub 给我点个 ⭐(我的主页在 bio 里)。咱们下次聊 如何用 Node.js 写一个能抗住双11流量的限流中间件 —— 毕竟,老板已经暗示今年大促要上了 😅
Happy Coding!

评论 0