一直在被身边准备入门编程的朋友问同一个问题:"我想学后端,第一门语言和技术栈选什么最合适?"我每次的回答都很一致:先试试Node.js。理由很简单,它上手门槛低、生态足够大、社区资料多,而且从安装环境到写出一个能跑起来的程序,整个过程最快可能不到半小时。这篇文章就是想带你把"编写并运行一个Node.js程序"从头到尾走一遍,包括环境安装、第一个程序的写法、运行机制、调试方法,以及新手最容易踩的坑。
不管你是完全没写过代码的小白,还是已经会一点HTML/CSS想往后端走的前端同学,这篇内容都适合你。我会尽量用大白话解释原理,同时把每一步操作讲清楚,保证你照着做就能跑通。
1. 动手之前,先把Node.js的运行逻辑搞清楚
很多人第一次听到"Node.js"会误以为它是一种框架,类似Vue、React那种。其实不是。Node.js是一个JavaScript运行时环境,它的作用是在浏览器之外执行JavaScript代码。换句话说,以前JavaScript只能乖乖待在浏览器里,有了Node.js之后,它就能在服务器上跑,读写文件、访问网络、处理请求,这些能力它全都有了。
1.1 Node.js到底是什么:把JavaScript从浏览器里解放出来
理解Node.js,最核心的一点就是"运行时环境"这五个字。JavaScript本身只是一门编程语言,语言规定了怎么写代码,但真正让代码执行起来需要有一个环境。浏览器就是最典型的运行环境,它提供了一套API,比如document.getElementById、window.alert,这些能力是浏览器给的。
Node.js则基于Chrome浏览器用的V8引擎,把JavaScript的执行能力搬到了操作系统层面。它提供了一个完全不同的API集合,比如fs模块用来操作文件、http模块用来创建服务器、path模块用来处理文件路径。你可以把它理解成"同一个单词,换了一套词典"——语法还是JavaScript的语法,但能力范围完全不同。
1.2 浏览器里的JavaScript和Node.js里的JavaScript差在哪
为了更直观地感受区别,我列一个简单的对照:
| 维度 | 浏览器JavaScript | Node.js |
|---|---|---|
| 全局对象 | window、document | global、process |
| 如何运行 | 嵌入HTML,靠浏览器解析执行 | node命令直接运行JS文件 |
| 模块加载 | ES Module(import/export)为主 | CommonJS(require/module.exports)为主,也支持ESM |
| 能力范围 | 操作DOM、发请求、动画 | 读写文件、创建服务、连接数据库、管理系统资源 |
| 典型用途 | 页面交互、前端逻辑 | 后端服务、命令行工具、自动化脚本 |
表格最后一行特别关键。现在很多自动化脚本、构建工具、脚手架工具,底层全都是Node.js写的,包括你可能已经用过的Webpack、Vite、ESLint,它们本质都是Node.js程序。
1.3 谁适合走这条路:学习成本与实际回报
如果你已经在写前端代码,那学Node.js几乎没有额外语法负担,因为语言本身你已经会了。如果你是完全零基础,也没关系,JavaScript的语法结构在主流语言里算亲和的,变量、函数、对象、数组这些概念不难理解,而且写出来的代码能立刻在终端里看到结果,这种即时反馈对新手来说特别重要。
从回报角度看,Node.js在中小型项目里出活非常快。做一个简单的文件批量重命名工具、爬虫脚本、接口Mock服务,几十行代码就能搞定。很多公司内部的运维工具、数据同步脚本,也都是用Node.js写的。学它不会白学,哪怕以后不做专职后端,它也能在你日常开发中当个"瑞士军刀"来用。
2. 安装Node.js:不同系统的完整流程与版本陷阱
安装这一步看起来简单,其实里面有几个决定你后续开发是否会顺利的关键细节。很多人装完发现node命令找不到、npm装包特别慢、版本不对导致项目跑不起来,这些问题大部分都能在安装阶段避免。
2.1 版本怎么选:LTS和Current的区别
Node.js官网提供两种版本下载:LTS(Long Term Support,长期支持版)和Current(最新尝鲜版)。新手我强烈推荐直接选LTS。理由很简单,LTS版本会提供长期的安全修复和稳定性保障,大多数第三方依赖库也都优先保证对LTS版本的支持。Current版本虽然包含新特性,但可能有一些不兼容改动,你在学习阶段没必要冒这个险。
另外要注意Node.js版本更新很快,不同大版本之间在语法特性和模块行为上会有差异。我并不建议追求最新,装一个偶数版本且标记为LTS的就好,比如常见的18.x、20.x。装完用node -v可以看到当前版本号,如果看到类似v20.10.0这样的输出,说明安装成功。
2.2 Windows安装:官网下载包与PATH环境变量的坑
Windows平台最简单的安装方式是去Node.js官网下载Windows Installer(.msi文件),然后一直点下一步。但有一个地方要特别注意:安装向导里有一个"Add to PATH"的选项,默认是勾选的,千万不要取消。还有自定义安装路径时,尽量避开带空格和中文的目录,比如C:\Program Files\nodejs\虽然自带空格也能正常工作,但有些第三方工具处理这种路径偶尔会抽风,自己单独建一个D:\nodejs之类的纯英文路径会更省心。
装完之后验证安装,打开命令提示符(CMD)或者PowerShell,输入:
node -v npm -v两个命令都能正常输出版本号,就说明安装成功。
2.3 macOS和Linux:用版本管理器装是最省心的方案
macOS用户如果直接去官网下载pkg安装包也能用,但我更推荐先装一个Homebrew,然后通过它安装,或者直接用nvm来管理版本。Linux用户则强烈建议用nvm,因为系统包管理器(apt、yum)里带的Node.js版本通常比较老,会在后面装某些新依赖时报错。
以macOS配合nvm为例,安装命令是两条:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash然后重新打开终端,执行:
nvm install --lts nvm use --lts这样装的好处是你可以随时切换Node.js版本。比如某个老项目需要Node.js 14,另一个新项目需要Node.js 20,用nvm use 14和nvm use 20就能来回切,不用反复卸载重装。我工作这些年,因为这个功能少踩了不知道多少坑。
2.4 安装完必做的两件事:换官方源以外的镜像要考虑清楚
很多人装完Node.js之后第一件事就是去网上找命令把npm源切换到国内镜像,因为默认源在海外的确慢。这里我给一个中肯的建议:如果只是学习阶段,装包频率不高,用默认源也能忍受;如果你已经频繁装包、感觉到明显卡顿,再去换镜像也不迟。切换命令一般网上一搜就有,我这里不贴具体的了,注意一个原则:个人开发换源没问题,公司项目要按团队规定来,别擅自改共享机器的源。
另外建议安装完成之后顺手执行一下:
npm config get registry看到输出是默认官方地址就继续保持,如果发现是未知的第三方地址,要考虑是不是之前做过什么修改。保持源的可控性,后面排查依赖问题会省很多力气。
3. 第一个Node.js程序:从Hello World到真正用起来
环境装好了,接下来写代码。这是最关键的一步,我会带着你从新建文件夹开始,一步步走到一个稍微有点实际功能的程序。
3.1 创建项目目录和初始化package.json
先在本地新建一个练习项目文件夹。命名上有一个小建议:尽量不要用node-demo、project这种让以后回头看的你一脸茫然的名字,虽然练习无所谓,但养成好习惯从第一天开始总没坏处。我一般会命名为node-learn之类的简短英文,路径同样避免中文和空格。
接着在终端里进入这个目录,执行:
npm init -y这会自动生成一个package.json文件,它是项目的"身份证",记录着项目名称、版本号、依赖包列表、启动命令等信息。手动创建的话,你也可以在文件夹里新建一个文件命名为package.json,但我建议用命令生成,因为格式不会出错。-y的意思是跳过所有交互提问直接生成默认配置,等以后需要自定义了再手动改也不迟。
3.2 编写入口文件:代码逐行拆解
在项目目录下新建一个文件,命名为app.js。用任意编辑器打开(新手推荐VS Code,免费且插件丰富),输入下面这段代码:
// 第一行Node.js代码 const greeting = "Hello, Node.js!"; console.log(greeting);大概解释一下发生了什么:
const greeting = "Hello, Node.js!";:声明一个字符串变量,值是Hello, Node.js!。const表示这个变量不能重新赋值,可以理解成"只读常量"。console.log(greeting);:把变量的值打印到终端。console.log是最常用的输出语句,不仅仅是Node.js有,浏览器里也有,作用一样。
这个文件虽然短,但它完成了一个完整程序的闭环:声明数据、处理数据、输出结果。所有编程的基础都在这三件事里打转了。
在终端里运行:
node app.js如果终端输出:
Hello, Node.js!恭喜你,第一个Node.js程序已经成功跑起来了。这一步虽然看起来简单,但"从0到1"的那个"1"你已经拿到了,后面的路会顺畅很多。
3.3 理解模块系统:require和import到底怎么选
上面的例子太简单,实际开发里代码会拆分成多个文件,一个文件里的代码想被另一个文件复用,就需要模块系统。Node.js原生支持两套模块系统:CommonJS和ES Module。
CommonJS的语法是require和module.exports。我改造一下刚才的例子,新建一个utils.js:
// utils.js const author = "nodejs新人"; function getGreeting(name) { return `Hello, ${name}!`; } module.exports = { author, getGreeting };然后在app.js里这样使用:
// app.js const { author, getGreeting } = require("./utils.js"); console.log(getGreeting(author));运行node app.js,输出同样正常。这里的核心是:module.exports声明了"这个文件对外提供什么",require("./utils.js")声明了"我要引入哪个文件"。文件之间彼此隔离,只有通过module.exports暴露的内容才能被引用,这种设计能有效避免全局变量污染。
ES Module的语法则是import和export,更现代,浏览器原生支持,Node.js从12.17版本开始也支持了。要注意的是,要在package.json里加一个"type": "module"配置,否则Node.js默认按CommonJS处理文件。我建议新手学习阶段先用CommonJS,因为它不需要额外配置、报错更直观,等熟悉之后再接触ESM,理解会更透彻。
3.4 接收命令行参数:让程序开始有点实际用途
纯打印一句话的代码很快就腻了,下一步我们做一个稍微有交互感的程序——从命令行接收参数,根据不同输入输出不同内容。
修改app.js为:
// app.js const args = process.argv.slice(2); if (args.length === 0) { console.log("请输入名字,例如:node app.js 张三"); } else { console.log(`你好,${args[0]}!欢迎来到Node.js世界。`); }然后运行:
node app.js 张三输出:
你好,张三!欢迎来到Node.js世界。这里process.argv是Node.js提供的一个数组,存储着命令行参数。数组前两个元素一个是Node.js执行文件的路径,一个是你运行的程序文件的路径,所以从索引2开始才是我们真正传入的参数。slice(2)就是从索引2开始截取后面的内容。
到这里,你的小工具已经能做"根据输入产生不同响应"的事情了。这就是命令行工具的基本雏形,很多Node.js CLI工具就是这么一点点做出来的。
4. 运行、调试与开发体验:从一次执行到天天开发
程序能跑不算完,现实开发中你会反复改代码、重启程序、观察输出、定位报错。这一节讲的是让你日常开发更顺畅的运行和调试手段。
4.1 node命令的三种常见用法
第一,直接运行一个JS文件,这是最基础的用法:
node app.js第二,直接执行一段代码字符串,省去建文件。适合快速验证某个语法:
node -e "console.log(1 + 1)"第三,进入REPL交互环境。在终端输入node不带任何参数,回车后会进入一个>提示符的交互界面,可以直接一行一行输入JavaScript代码并立即看到结果,类似于浏览器的控制台。想退出就输入.exit或者按两次Ctrl+C。这个交互环境在调试小语法片段时非常有用。
4.2 让程序自动重启:node --watch与nodemon
学习阶段改完代码还要手动重新node app.js,虽然能接受,但改多了就烦。Node.js从18.11版本开始内置了一个--watch模式,用法就是在运行命令后面加这个参数:
node --watch app.js这样你每次保存文件,程序都会自动重启,省去了手动操作。如果你的Node.js版本太老没有这个参数,可以安装一个叫nodemon的第三方工具,命令是:
npm install -g nodemon然后运行:
nodemon app.js它俩实现的热重载效果基本一样,--watch的优势是不依赖额外安装,nodemon的优势是支持更多自定义配置。以我的经验,早期阶段直接用Node.js内置的--watch就完全够了,少装一个全局工具多一份省心。
4.3 控制台输出的进阶用法:别只会console.log
新手很容易把console.log当成唯一的调试工具,用多了就会发现输出混在一起分不清。Node.js给console对象提供了好几个方法,各有用途:
| 方法 | 用途 | 示例输出 |
|---|---|---|
| console.log | 普通打印 | 普通文字 |
| console.info | 信息提示 | 同log,但语义不同 |
| console.warn | 警告 | 黄色高亮 |
| console.error | 错误 | 红色高亮 |
| console.table | 表格化数组/对象 | 结构化表格 |
| console.time / timeEnd | 计时 | 执行时间 |
举个例子,console.time和console.timeEnd用来测一段代码执行时间很好用:
console.time("循环耗时"); let sum = 0; for (let i = 0; i < 1000000; i++) { sum += i; } console.timeEnd("循环耗时");运行后会输出类似循环耗时: 5.234ms的结果。以后再遇到"这段代码怎么这么慢"的疑惑,这个方法可以直接帮你定位性能瓶颈在哪里。
4.4 报错怎么看:读懂堆栈和调试器
程序报错不可怕,可怕的是看不懂报错。Node.js的报错信息会告诉你三件事:错误类型、错误信息和出错位置。
比如我故意把代码改成:
const data = undefined; console.log(data.name);运行后终端会输出类似:
TypeError: Cannot read properties of undefined (reading 'name') at Object.<anonymous> (C:\xxx\app.js:2:17)第一行说明错误类型是TypeError,具体原因是"尝试读取undefined的name属性"。第二行开始的堆栈信息指出了出错文件路径和行列位置app.js:2:17,意思是app.js文件第2行第17列。看到这个信息,直接跳过去看就行。
如果console.log打再多也找不出问题,可以用Node.js内置调试器。在代码里加上一行debugger;,然后运行:
node inspect app.js程序会在debugger那一行暂停,你可以在终端交互式地查看变量。不过说实话,现在多数人更习惯在VS Code里点一下行号设断点再按F5启动调试,可视化的断点调试对新手友好得多。这里我就不展开VS Code调试配置了,后面单独写一篇讲调试环境的搭建。
5. 新手最常见的几个坑,我踩过之后帮你总结
这部分是全文的精华。我见过太多新手在这些地方卡上几个小时甚至一整天,我自己当年也一样。提前告诉你这些坑,能省下大量无意义的时间。
5.1 运行时提示"node不是内部或外部命令"
这是Windows用户高频遇到的问题,基本可以断定是安装时没有把Node.js添加到PATH环境变量,或者安装后没有重启终端。解决方法是重启终端,如果还不行就检查环境变量里是否有一个指向nodejs安装目录的条目。没有的话就手动添加,装过一次之后路径一般是C:\Program Files\nodejs。这一关过了,后续命令就都通了。
5.2 模块找不到:Cannot find module背后的三大原因
Cannot find module 'xxx'这个报错出现频率极高,原因不外乎三种:
第一,拼写错误。这看起来像废话,但require的文件名、路径、大小写都必须完全正确,比如app.js和App.js在Linux/macOS上是两个不同的文件,小写的写错成大写就会报错。
第二,依赖没有安装。如果你require的是一个npm包,比如express,但没执行过npm install express,那肯定找不到。判断方法很简单,看项目里有没有node_modules文件夹,没有就说明依赖还没装。
第三,路径错误。相对路径的理解要清楚:./代表当前目录,../代表上一级目录。新手经常写错,比如require("./utils")在根目录运行没问题,但如果在src目录下运行,就要改成require("../utils")。
5.3 端口被占用:EADDRINUSE到底是什么意思
当你开始写Web服务器,比如用http模块起一个8080端口的服务,运行时报Error: listen EADDRINUSE: address already in use :::8080,意思是8080端口已经被另一个程序占用了。解决方法是换一个端口,或者找到占用端口的进程把它结束掉。
Windows上查看端口占用和结束进程的命令是:
netstat -ano | findstr 8080 taskkill /PID 你的进程号 /FmacOS/Linux上可以用:
lsof -i :8080 kill -9 你的进程号这类问题在写服务之后会频繁遇到,知道在哪查、怎么解决,能让你的心态稳很多。
5.4 异步代码不按顺序执行:理解事件循环的基本逻辑
Node.js的一个显著特点是异步非阻塞。看这段代码,很多人第一次跑会很困惑:
console.log("开始"); setTimeout(() => { console.log("定时器内"); }, 0); console.log("结束");输出顺序是:
开始 结束 定时器内哪怕定时器时间是0毫秒,它也会在"结束"之后才执行。这里不展开讲事件循环的深层机制,你只需要先记住一条经验:在Node.js里,回调函数、Promise这些异步操作不会立刻执行,它们会在当前同步代码跑完之后再排队执行。需要按顺序处理多步异步操作时,推荐用async/await语法,它能让异步代码写起来像同步一样自然:
function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function main() { console.log("开始"); await wait(1000); console.log("一秒钟后执行"); } main();这段代码输出的顺序是"开始"、等待一秒、然后"一秒钟后执行",逻辑清晰多了。async/await是日常开发的基础语法,值得在写接口、操作数据库时反复使用。
5.5 一个影响深远的习惯:用package.json里的scripts管理命令
最后一个建议不是踩坑,是少走弯路。很多新手习惯直接敲node app.js,但项目一多就会忘到底用哪个命令启动。更好的习惯是在package.json的scripts字段里配置命令:
{ "name": "node-learn", "version": "1.0.0", "main": "app.js", "scripts": { "start": "node app.js", "dev": "node --watch app.js" }, "type": "commonjs" }配置之后,运行:
npm start就能启动程序;开发时运行:
npm run dev就能带热重载启动。这样做的好处是命令统一、有迹可循,以后把项目交给别人时,对方只要看package.json就知道怎么启动。这个习惯我从很早就开始养成,现在每个项目都这么干。
我自己在带新手入门时,最常说的一句话是:"先别急着背API,把从写文件到跑起来的流程多走几遍,很多概念自然就通了。"Node.js的学习路径很平缓,安装环境、跑通第一个程序、理解模块系统、会看报错调试,这四个节点走完,就已经具备继续往下深挖的底气了。接下来可以根据兴趣挑方向,比如用express写Web接口、用fs做文件批处理工具、用child_process执行系统命令,每条路都够你研究一阵子。先把这个"从0到1"的过程扎扎实实走完,后面的路会越来越顺。