NodeJs入门

NodeJs入门

_

一,什么是NodeJs

Node.js 是一个让 JavaScript 在浏览器之外运行的环境(Runtime)。

它基于 Chrome V8 引擎,让 JS 代码可以像 Python、Java 一样直接运行在操作系统上,而不是依赖网页。

核心本质:Node.js = V8 + libuv

你可以这样理解 Node.js 的构成:

组件作用类比
V8引擎解析和执行JS代码浏览器里的JS引擎
libuv提供异步I/O和事件循环Node独有的"后台管家"
bindings让JS调用C++写的底层功能胶水层

在浏览器里能用的console、setTimeout、Promise,都是 V8 提供的。而 Node.js 额外给你加了一堆浏览器里没有的东西:

  • fs 读写文件
  • http 创建服务器
  • child_process 执行系统命令
  • os 获取系统信息
  • path 处理文件路径

所以:在浏览器里用 JS 控制网页 DOM,在 Node 里用 JS 控制操作系统资源。

二,什么是前端工程化?

前端工程化 = 用软件工程的方法,解决前端开发中的 "规模" 和 "效率" 问题。

说白了就是:当项目变大、人变多、需求变快时,怎么保证代码还能写得爽、跑得稳、上线快。

从 "原始社会" 到 "工程化"

  • 阶段 1:原始时代(2010 年前)

    你写一个网页:

    <!DOCTYPE html>
    <html>
    <head>
      <script src="jquery.js"></script>
      <script src="utils.js"></script>
      <script src="app.js"></script>
    </head>
    <body>
      <!-- 页面内容 -->
    </body>
    </html>
    

    痛点:

    • 所有变量都是全局的(window.$、window.app),命名冲突家常便饭
    • 引用的JS顺序不能错,utils.js必须在app.js前面
    • 要压缩代码?手动去网站粘贴压缩再粘回来
    • 要兼容IE?手动加一堆if判断
    • 上线前要把所有HTML里的图片路径改成CDN?手动改

    这时候你还不需要工程化,因为项目就几个页面、两个人开发。

  • 阶段 2:混乱时代(2013-2016 年)

    项目变大了:

    • 用了React/Vue,每个组件一个.js文件
    • 团队20个人同时开发
    • 需要用到Sass写样式、ES6+语法、TypeScript
    • 代码要压缩、要tree-shaking、要按需加载

    你发现:浏览器根本看不懂这些!

    • 浏览器不认识.vue文件
    • 浏览器不认识import语法(当时)
    • 浏览器不认识.scss文件
    • 需要把ES6转成ES5兼容IE

    于是你急需一个 "翻译官",把开发时写的 "现代代码" 翻译成浏览器能跑的 "古董代码"——这就是前端工程化的起点。

  • 阶段 3:工程化时代(现在)

    你现在使用的工具链:

    工具解决什么问题类比
    Vite/Webpack把.vue、.jsx、.ts、.scss转成浏览器认识的HTML/CSS/JS翻译官
    ESLint代码上线前自动检查语法错误和风格问题校对员
    Prettier自动格式化代码,团队统一风格保洁阿姨
    TypeScript在写代码时提前发现类型错误质检员
    Jest/Vitest自动测试你的代码是否跑得通测试员
    Git + CI/CD多人协作、自动部署流程管理
    npm/yarn/pnpm管理第三方库的版本和依赖仓库管理员

    这整套 "工具 + 流程 + 规范" 的体系,就是前端工程化。

三,前端工程化的四大核心目标

3.1 模块化(代码组织)

问题:几百个 JS 文件怎么组织?怎么避免命名冲突?怎么复用代码?

工程化方案:

  • Node.js的CommonJS规范(require/module.exports)
  • 浏览器的ES Module规范(import/export)
  • 通过打包工具(Vite/Webpack)把零散的模块打包成几个文件

3.2 编译/转译(代码转换)

问题:浏览器不认识 TypeScript、Sass、JSX,怎么让它们跑起来?

工程化方案:

  • TypeScript → JS:tsc编译器
  • Sass → CSS:sass编译器
  • JSX → JS:babel编译器
  • 这些编译器都是用Node.js写的,通过fs模块读写文件

3.3 开发效率(开发体验)

问题:每次改代码都要手动刷新浏览器?手动打包?太慢了!

工程化方案:

  • HMR(热模块替换):改代码后自动更新页面,保留状态
  • Dev Server:本地起一个开发服务器,自动打开浏览器
  • 文件监听:保存文件自动触发重新编译
  • 这些功能底层都是Node.js的fs.watch + http模块实现的

3.4 质量保障(代码可靠)

问题:团队 20 个人,怎么保证代码不坏?怎么保证风格统一?

工程化方案:

  • ESLint:代码提交前自动检查
  • Prettier:保存时自动格式化
  • Git Hooks(husky):在git commit前自动跑测试
  • CI/CD:代码合并到主分支后自动构建、自动部署

3.5 一个"工程化"的完整工作流

把上面的串起来:

你写代码(.vue / .ts / .scss)
    ↓
ESLint 自动检查语法(Node脚本)
    ↓
Prettier 自动格式化(Node脚本)
    ↓
Vite 监听文件变化(fs.watch)
    ↓
Vite 把 .vue 转成 .js(用Node读取文件→转换字符串→写入内存)
    ↓
Dev Server 把编译后的代码发给浏览器(http模块)
    ↓
你修改代码 → Vite重新编译 → HMR更新页面(循环)
    ↓
git commit → husky触发(child_process)
    ↓
自动跑单元测试(Jest)
    ↓
推送代码 → CI服务器拉取 → 自动构建 → 部署上线

每一步都有 Node.js 的身影! 但它不是在跑后端业务,而是在跑各种工具脚本

四,NodeJs快速入门

4.1 执行NodeJs

安装 Nvm-DeskTop

2953321-20260824150127449-1899126171.png

然后随便新建个 Test.js 文件,内容举例

console.log('hello world')

2953321-20260824150204737-1458963243.png

4.2 fs 模块

FS两种API风格

同步 API

const fs = require('fs');

// 读文件(同步)
const content = fs.readFileSync('./src/app.js', 'utf-8');

// 写文件(同步)
fs.writeFileSync('./dist/app.js', '新内容', 'utf-8');

// 检查文件是否存在
const exists = fs.existsSync('./src/app.js');

// 删除文件
fs.unlinkSync('./temp.js');

工程化中同步是主力

  • 构建过程需要按顺序执行:先读A,处理完,再写B
  • 脚本是运行在开发环境,不需要考虑并发性能
  • 同步代码更清晰,出错了堆栈更容易追踪

典型场景:Vite/Webpack 的构建流程、CLI 工具的文件操作


Promise 风格

const fs = require('fs').promises;

async function build() {
  const content = await fs.readFile('./src/app.js', 'utf-8');
  await fs.writeFile('./dist/app.js', content);
}

什么时候用?

  • 需要并发处理多个文件(比如同时读取10个文件)
  • 你在写一个需要高性能的工具(比如文件监听器)

注意:如果你的工具只是简单的顺序处理,用同步就够了,别为了 "异步高级" 而用异步。

回调风格

fs.readFile('./file.txt', 'utf-8', (err, data) => {
  // 回调地狱,别用了!
});

工程化中几乎不用,除非你在维护 10 年前的旧代码。


常用的FS API

fs.readFileSync() —— 读文件

// 最常用:读取文本文件
const content = fs.readFileSync('./src/index.js', 'utf-8');

// 读取二进制文件(图片等)
const buffer = fs.readFileSync('./assets/logo.png');

工程化场景:读取源码文件、读取配置文件(package.json、vite.config.js)

fs.writeFileSync() —— 写文件

// 写入文本
fs.writeFileSync('./dist/index.js', 'console.log("hello")', 'utf-8');

// 创建目录时自动补全路径(需要先创建目录)
// 注意:writeFileSync不会自动创建不存在的目录!

工程化场景:输出构建产物到dist目录、生成manifest.json

fs.existsSync() —— 检查文件 / 目录是否存在

if (fs.existsSync('./node_modules')) {
  console.log('依赖已安装');
}

// 注意:在Node.js官方文档中,existsSync被认为"过时",但工程化中依然广泛使用
// 替代方案:fs.accessSync(),但用法更繁琐,个人推荐existsSync

工程化场景:检查配置文件是否存在、判断是否要创建目录

fs.mkdirSync() —— 创建目录

// 创建单层目录
fs.mkdirSync('./dist');

// 递归创建多层目录(重要!)
fs.mkdirSync('./dist/js/components', { recursive: true });

工程化场景:构建前创建输出目录、创建临时文件夹

fs.readdirSync() —— 读取目录内容

// 读取目录下所有文件和文件夹
const files = fs.readdirSync('./src');
// 返回:['index.js', 'utils.js', 'components/']

// 读取并获取详细信息
const filesWithStats = fs.readdirSync('./src', { withFileTypes: true });
filesWithStats.forEach(item => {
  if (item.isDirectory()) {
    console.log('文件夹:', item.name);
  } else {
    console.log('文件:', item.name);
  }
});

工程化场景:遍历源码目录、扫描所有.vue文件、查找所有测试文件

fs.statSync() —— 获取文件 / 目录信息

const stats = fs.statSync('./src/index.js');
console.log(stats.isFile());     // true
console.log(stats.isDirectory()); // false
console.log(stats.size);          // 文件大小(字节)
console.log(stats.mtime);         // 最后修改时间

工程化场景:判断是文件还是文件夹、文件监听时判断是否修改

fs.watch() —— 监听文件变化

// 监听整个目录
fs.watch('./src', (eventType, filename) => {
  if (eventType === 'change') {
    console.log(`${filename} 发生了变化,重新构建...`);
    // 重新执行构建逻辑
    build();
  }
});

工程化场景:Vite/Webpack 的 HMR、nodemon自动重启、开发模式下的文件监听

注意要点

  1. 总是处理编码

    // ❌ 错误:不指定编码会返回Buffer
    const data = fs.readFileSync('./file.txt');
    console.log(data); // <Buffer 68 65 6c 6c 6f>
    
    // ✅ 正确:指定utf-8
    const data = fs.readFileSync('./file.txt', 'utf-8');
    console.log(data); // 'hello'
    
  2. 总是先创建目录再写文件

    const fs = require('fs');
    const path = require('path');
    
    function writeFileSafe(filePath, content) {
      const dir = path.dirname(filePath);
      
      // 如果目录不存在,递归创建
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
      
      fs.writeFileSync(filePath, content, 'utf-8');
    }
    
    // 使用:即使dist/js目录不存在,也会自动创建
    writeFileSafe('./dist/js/app.js', 'console.log("hello")');
    

4.3 path模块

为什么需要Path模块?

  • 错误的做法(硬编码路径分隔符)

    //  这样写,在Windows上会炸!
    const filePath = 'src/components/App.js';  // Mac/Linux用 / 
    // Windows上路径应该是:src\components\App.js
    
    //  手动拼接字符串
    const filePath = __dirname + '/src/' + 'index.js';
    // 在Windows上可能变成:C:\project/src/index.js(混用/和\)
    

    __dirname 内置变量(获取当前模块目录 - 绝对路径)

  • 正确的做法

    const path = require('path');
    
    // ✅ 自动适配操作系统
    const filePath = path.join('src', 'components', 'App.js');
    // Mac/Linux: src/components/App.js
    // Windows: src\components\App.js
    

核心认知:Path 模块帮你屏蔽操作系统差异,让路径处理在任何系统上表现一致。

常用Path API

按使用频率从高到低排列:

  1. path.join() —— 拼接路径

    最常用的 API,没有之一。用来拼接多个路径片段。

    const path = require('path');
    
    // 基础用法
    const fullPath = path.join('src', 'components', 'App.js');
    // 结果:'src/components/App.js' (Mac/Linux)
    // 结果:'src\components\App.js' (Windows)
    
    // 配合 __dirname 使用(最常用)
    const filePath = path.join(__dirname, 'src', 'index.js');
    // 结果:'/Users/xxx/project/src/index.js' (Mac)
    // 结果:'C:\Users\xxx\project\src\index.js' (Windows)
    
    // 自动处理多余的斜杠
    path.join('src/', '/components', 'App.js');
    // 结果:'src/components/App.js' (会自动归一化)
    

    工程化场景:构建时拼接输入 / 输出路径

    const fs = require('fs');
    const path = require('path');
    
    // 读取源码
    const srcFile = path.join(process.cwd(), 'src', 'index.js');
    const content = fs.readFileSync(srcFile, 'utf-8');
    
    // 输出到dist
    const distFile = path.join(process.cwd(), 'dist', 'index.js');
    fs.writeFileSync(distFile, content);
    
  2. path.resolve() —— 解析为绝对路径

    resolve和join经常被混淆,它们的区别非常关键:

    特性path.join()path.resolve()
    作用拼接路径片段将路径解析为绝对路径
    是否从根目录开始❌ 只拼接,不处理绝对路径✅ 总是返回绝对路径
    处理..保留..解析..为上一级
    // 看区别:
    console.log(path.join('src', 'index.js'));
    // 输出:'src/index.js' (相对路径)
    
    console.log(path.resolve('src', 'index.js'));
    // 输出:'/Users/xxx/current/working/directory/src/index.js' (绝对路径)
    
    // 处理 .. 的区别:
    console.log(path.join('src/..', 'index.js'));
    // 输出:'src/../index.js' (保留了..)
    
    console.log(path.resolve('src/..', 'index.js'));
    // 输出:'/Users/xxx/current/working/directory/index.js' (解析了..)
    

    什么时候用哪个?

    // 用 path.join:拼接相对路径
    const relativePath = path.join('src', 'components', 'App.js');
    
    // 用 path.resolve:获取绝对路径(最常见)
    const absolutePath = path.resolve('src/index.js');
    // 相当于:path.join(process.cwd(), 'src/index.js')
    
    // 最推荐的写法:明确告诉别人你基于哪个目录
    const filePath = path.resolve(__dirname, '../src/index.js');
    

    工程化场景:几乎所有的文件操作都用path.resolve得到绝对路径后再操作

    // 不推荐:直接用相对路径(容易出错)
    const content = fs.readFileSync('./src/index.js');
    
    // 推荐:解析为绝对路径再操作
    const filePath = path.resolve(__dirname, 'src/index.js');
    const content = fs.readFileSync(filePath);
    
  3. path.basename() —— 获取文件名

    const filePath = '/src/components/App.vue';
    
    path.basename(filePath);
    // 输出:'App.vue'
    
    path.basename(filePath, '.vue');
    // 输出:'App' (去掉扩展名)
    
    // 从完整路径中提取文件名(工程化常用)
    const files = fs.readdirSync('./src');
    files.forEach(file => {
      const fileName = path.basename(file);
      const nameWithoutExt = path.basename(file, path.extname(file));
      console.log(`完整文件名:${fileName},文件名:${nameWithoutExt}`);
    });
    

    工程化场景:处理文件列表时提取文件名、生成路由表

    // 自动生成路由入口文件
    const files = fs.readdirSync('./src/pages');
    files.forEach(file => {
      const name = path.basename(file, '.vue');
      // 生成 import Home from './pages/Home.vue'
    });
    
  4. path.dirname() —— 获取目录路径

    const filePath = '/src/components/App.vue';
    
    path.dirname(filePath);
    // 输出:'/src/components'
    
    // 配合 __dirname 获取当前文件所在目录
    console.log(__dirname);  // 当前文件目录
    console.log(path.dirname(__dirname)); // 上一级目录
    

    工程化场景:从文件路径中提取目录、处理输出路径

    function copyFile(sourcePath, targetDir) {
      const fileName = path.basename(sourcePath);
      const targetPath = path.join(targetDir, fileName);
      
      // 确保目标目录存在
      fs.mkdirSync(targetDir, { recursive: true });
      fs.copyFileSync(sourcePath, targetPath);
    }
    
  5. path.extname() —— 获取文件扩展名

    const filePath = 'src/components/App.vue';
    
    path.extname(filePath);
    // 输出:'.vue' (注意包含点)
    
    // 工程化常用:根据扩展名决定如何处理
    function processFile(filePath) {
      const ext = path.extname(filePath);
      
      switch(ext) {
        case '.js':
        case '.ts':
          return compileJavaScript(filePath);
        case '.vue':
          return compileVue(filePath);
        case '.scss':
          return compileSass(filePath);
        default:
          return copyFile(filePath);
      }
    }
    
  6. path.parse() —— 解析路径为对象

    const filePath = '/Users/xxx/project/src/components/App.vue';
    
    const parsed = path.parse(filePath);
    console.log(parsed);
    // {
    //   root: '/',
    //   dir: '/Users/xxx/project/src/components',
    //   base: 'App.vue',
    //   ext: '.vue',
    //   name: 'App'
    // }
    
    // 方便地提取各种信息
    const { dir, base, name, ext } = path.parse(filePath);
    

"路径三剑客"

这三个变量在工程化中经常一起出现

  1. __dirname —— 当前文件所在目录

    // 文件位置:/Users/xxx/project/scripts/build.js
    console.log(__dirname);
    // 输出:'/Users/xxx/project/scripts'
    
    // 永远不变,始终是当前JS文件的位置
    
  2. process.cwd() —— 当前工作目录

    // 你在哪个目录执行 `node build.js`
    console.log(process.cwd());
    // 输出:'/Users/xxx/project' (取决于你在哪个目录执行命令)
    
    // 可以变化!如果用户从不同目录启动,值不同
    
  3. path.resolve() —— 基于当前工作目录解析

    // 基于 process.cwd() 解析
    path.resolve('src/index.js');
    // 等同于:path.join(process.cwd(), 'src/index.js')
    

区别

// ❌ 错误:用 __dirname 代替 process.cwd()
const config = require(path.join(__dirname, '..', 'vite.config.js'));
// 如果 scripts/build.js 位置变了,这个路径就错了

// ✅ 正确:用 process.cwd() 获取项目根目录
const config = require(path.resolve('vite.config.js'));
// 始终基于用户当前执行命令的目录

// ✅ 更严谨:从当前文件位置向上找
const config = require(path.resolve(__dirname, '../vite.config.js'));
// 假设 scripts 在项目根目录下,向上找一级

工程化中的黄金法则:

  • 访问项目文件(源码、配置)→ 用 process.cwd()
  • 访问工具自身的文件(npm包里的资源)→ 用 __dirname

4.4 结合FS和Path写一个"文件遍历器"

这是工程化中最常见的任务,把所有知识串起来:

const fs = require('fs');
const path = require('path');

// 递归遍历目录,找所有 .vue 文件
function findVueFiles(dir, result = []) {
  // 1. 读取目录
  const items = fs.readdirSync(dir);
  
  items.forEach(item => {
    const fullPath = path.join(dir, item);
    const stats = fs.statSync(fullPath);
    
    if (stats.isDirectory()) {
      // 2. 如果是目录,递归遍历
      findVueFiles(fullPath, result);
    } else if (stats.isFile() && path.extname(item) === '.vue') {
      // 3. 如果是 .vue 文件,记录
      result.push(fullPath);
    }
  });
  
  return result;
}

// 使用:从 src 目录开始查找
const srcDir = path.resolve('src');
const vueFiles = findVueFiles(srcDir);

console.log(`找到 ${vueFiles.length} 个 Vue 文件`);
vueFiles.forEach(file => {
  const relativePath = path.relative(process.cwd(), file);
  const fileName = path.basename(file);
  console.log(`- ${relativePath} (${fileName})`);
});

"路径原则"

  1. 永远使用path.join()或path.resolve(),别用字符串拼接
  2. 文件操作前先用path.resolve()转成绝对路径
  3. 区分__dirname(文件位置)和process.cwd()(执行位置)

4.5 http模块

Http模块是什么?

Node.js 的http模块是用来创建 HTTP 服务器或发起 HTTP 请求的核心模块。它让你能用 JavaScript 实现 HTTP 协议的全部功能。

const http = require('http');

核心能力:

  1. 做服务器:接收浏览器的请求,返回响应
  2. 做客户端:向其他服务器发起请求(类似浏览器里的fetch)

Http模块的核心概念

请求(Request)和响应(Response)

HTTP 协议就是 "一问一答":

浏览器 ---------- 请求(Request) ----------> 服务器
浏览器 <--------- 响应(Response) ----------- 服务器

在 Http 模块中,这两个对象是核心:

  • req(请求对象):包含浏览器发送的所有信息(URL、方法、请求头、数据)
  • res(响应对象):用来设置返回给浏览器的所有信息(状态码、响应头、数据)

创建服务器(最基础的用法)

const http = require('http');

// 创建服务器
const server = http.createServer((req, res) => {
  // 这个回调函数会在每次有请求时执行
  
  // 1. 设置响应头(状态码 + 内容类型)
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  
  // 2. 返回响应内容
  res.end('Hello World');
});

// 监听端口
server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

运行后:浏览器访问http://localhost:3000,会看到 "Hello World"。

深入理解请求对象(req)

req包含了浏览器发送的所有信息:

const server = http.createServer((req, res) => {
  // 1. 请求方法(GET、POST、PUT等)
  console.log('方法:', req.method);
  
  // 2. 请求URL(路径 + 查询参数)
  console.log('URL:', req.url);
  // 访问 http://localhost:3000/user?id=123
  // 输出:'/user?id=123'
  
  // 3. 请求头(浏览器信息、Cookie等)
  console.log('请求头:', req.headers);
  // 输出:{ host: 'localhost:3000', 'user-agent': '...' }
  
  // 4. 请求体(POST/PUT时携带的数据)
  let body = '';
  req.on('data', chunk => {
    body += chunk.toString(); // 接收数据片段
  });
  req.on('end', () => {
    console.log('请求体:', body);
    res.end('收到数据');
  });
});

深入理解响应对象(res)

res用来控制返回给浏览器的内容:

const server = http.createServer((req, res) => {
  // 1. 设置状态码和响应头(两种方式)
  
  // 方式一:writeHead一次性设置
  res.writeHead(200, { 
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*' 
  });
  
  // 方式二:分开设置
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  
  // 2. 返回JSON数据
  const data = JSON.stringify({ name: 'Node.js', version: '20' });
  res.end(data);
  
  // 3. 返回HTML
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end('<h1>Hello</h1>');
  
  // 4. 返回文件(结合FS模块)
  const fs = require('fs');
  const content = fs.readFileSync('./index.html');
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(content);
});

处理不同的请求路径(路由)

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  // 解析URL,获取路径和查询参数
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;
  const query = parsedUrl.query;
  
  // 路由处理
  if (pathname === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>首页</h1>');
    
  } else if (pathname === '/user') {
    // 获取查询参数:/user?id=123
    const userId = query.id;
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id: userId, name: '张三' }));
    
  } else if (pathname === '/api/data' && req.method === 'POST') {
    // 处理POST请求
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      const data = JSON.parse(body);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ received: data }));
    });
    
  } else {
    // 404
    res.writeHead(404);
    res.end('404 Not Found');
  }
});

server.listen(3000);

解析请求体的多种格式

const server = http.createServer((req, res) => {
  if (req.method === 'POST') {
    let body = '';
    
    req.on('data', chunk => {
      body += chunk.toString();
    });
    
    req.on('end', () => {
      // 根据Content-Type解析不同格式
      const contentType = req.headers['content-type'];
      
      if (contentType === 'application/json') {
        const jsonData = JSON.parse(body);
        console.log('JSON数据:', jsonData);
        
      } else if (contentType === 'application/x-www-form-urlencoded') {
        // 表单数据:name=张三&age=25
        const params = new URLSearchParams(body);
        console.log('表单数据:', Object.fromEntries(params));
        
      } else if (contentType === 'text/plain') {
        console.log('纯文本:', body);
      }
      
      res.end('收到数据');
    });
  }
});

发送HTTP请求(作为客户端)

Http 模块也可以主动请求其他服务器:

const http = require('http');

// 方式1:使用 http.request
const options = {
  hostname: 'api.example.com',
  port: 80,
  path: '/users',
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  }
};

const request = http.request(options, (response) => {
  let data = '';
  
  response.on('data', chunk => {
    data += chunk.toString();
  });
  
  response.on('end', () => {
    console.log('响应数据:', JSON.parse(data));
  });
});

request.on('error', (error) => {
  console.error('请求失败:', error);
});

request.end(); // 发送请求

简单版(使用http.get):

http.get('http://api.example.com/users', (res) => {
  let data = '';
  res.on('data', chunk => data += chunk);
  res.on('end', () => console.log(JSON.parse(data)));
});

常见状态码和Content-Type

常用状态码

res.writeHead(200); // OK - 成功
res.writeHead(201); // Created - 创建成功
res.writeHead(301); // Moved Permanently - 永久重定向
res.writeHead(302); // Found - 临时重定向
res.writeHead(400); // Bad Request - 请求错误
res.writeHead(401); // Unauthorized - 未认证
res.writeHead(403); // Forbidden - 禁止访问
res.writeHead(404); // Not Found - 未找到
res.writeHead(500); // Internal Server Error - 服务器错误

常用 Content-Type

// 文本
'text/plain'
'text/html'
'text/css'
'text/javascript'

// 应用数据
'application/json'
'application/xml'
'application/x-www-form-urlencoded'
'application/octet-stream' // 二进制

// 图片
'image/jpeg'
'image/png'
'image/gif'

// 文件下载
'application/zip'
'application/pdf'

注意事项

  1. 总是处理错误
    server.on('error', (error) => {
      if (error.code === 'EADDRINUSE') {
        console.error('端口被占用,请换一个端口');
      }
    });
    
  2. 设置超时
    server.timeout = 30000; // 30秒超时
    
  3. 限制请求体大小(防止攻击)
    let body = '';
    req.on('data', chunk => {
      body += chunk;
      if (body.length > 1e6) { // 1MB限制
        res.writeHead(413);
        res.end('请求体太大');
        req.destroy();
      }
    });
    
  4. 使用url.parse()解析查询参数
    const url = require('url');
    const parsed = url.parse(req.url, true);
    console.log(parsed.query.id); // 获取查询参数
    

五,NodeJs模块化

5.1 什么是模块化?

一句话定义:模块化就是把一个大的程序拆分成独立、可复用的小文件,每个文件负责一个特定的功能。

没有模块化的时代(痛点)

<!-- 2010年前的写法 -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>

问题:

  • 所有变量都是全局的,utils.js里的formatDate会污染全局
  • 脚本加载顺序必须正确,app.js依赖utils.js,顺序错了就报错
  • 无法复用:想在其他项目用utils.js?只能复制粘贴

有了模块化之后

// utils.js - 导出功能
function formatDate(date) {
  return date.toLocaleDateString();
}
module.exports = { formatDate };

// app.js - 导入使用
const { formatDate } = require('./utils.js');
console.log(formatDate(new Date()));

优势:

  • 每个文件有独立作用域,不会污染全局
  • 显式声明依赖关系(谁导入谁)
  • 一个文件可以被多个地方复用

5.2 Node.js的两种模块系统

Node.js 同时支持两套模块规范,这是历史原因造成的,也是新手最容易混淆的地方。

特性CommonJSES Module
标准Node.js原生ECMAScript官方标准
语法require() / module.exportsimport / export
文件扩展名.js (默认).mjs 或 .js (配置"type":"module")
加载时机运行时加载编译时静态分析
是否异步同步异步

5.3 CommonJS

导出

  • 方式 1:module.exports(最常用,最推荐)

    你可以把module.exports理解为一个空盒子,你把东西放进去,别人就能拿走。

    放函数

    // 文件:math.js
    
    // 放一个函数进去
    module.exports = function add(a, b) {
      return a + b;
    };
    

    放对象

    // 文件:person.js
    
    // 放一个对象进去
    module.exports = {
      name: '张三',
      age: 25,
      sayHello: function() {
        console.log('你好,我是' + this.name);
      }
    };
    

    放多个东西

    // 文件:utils.js
    
    // 放多个东西进去
    module.exports = {
      formatDate: function(date) {
        return date.toLocaleDateString();
      },
      generateId: function() {
        return Math.random().toString(36).substr(2, 9);
      }
    };
    

    关键点:module.exports是一个容器,你把什么赋给它,别人就只能拿到什么。

  • 方式 2:exports(简写方式,有陷阱)

    exports是module.exports的 "快捷方式",你可以往里面添加属性。

    // 文件:math.js
    
    // 往 exports 里添加属性
    exports.add = function(a, b) {
      return a + b;
    };
    
    exports.subtract = function(a, b) {
      return a - b;
    };
    
    // 最终效果等同于:
    // module.exports = {
    //   add: function(a, b) { return a + b; },
    //   subtract: function(a, b) { return a - b; }
    // };
    

    注意要点:

    // ❌ 错误写法:给 exports 重新赋值
    exports = {
      add: function(a, b) { return a + b; }
    };
    // 这样不行!exports 现在指向了一个新对象,但 module.exports 还是原来的空盒子
    
    // ✅ 正确写法:修改 exports 的属性
    exports.add = function(a, b) { return a + b; };
    // 这样可以!因为 exports 和 module.exports 指向同一个对象
    
    // ✅ 正确写法:直接改 module.exports
    module.exports = {
      add: function(a, b) { return a + b; }
    };
    // 这样可以!直接替换了盒子里的东西
    

    记住:exports只是module.exports的 "别名",最终真正导出的永远是module.exports。

导入

require()就是用来拿别人导出的东西。

  • 基础用法

    // 文件:app.js
    
    // 导入 math.js 导出的内容
    const math = require('./math.js');
    
    console.log(math.add(1, 2)); // 3
    console.log(math.subtract(5, 3)); // 2
    
  • 拿到不同的导出类型

    // math.js - 导出一个函数
    module.exports = function add(a, b) {
      return a + b;
    };
    
    // app.js
    const add = require('./math.js');
    console.log(add(1, 2)); // 3(直接调用函数)
    
    // person.js - 导出一个对象
    module.exports = {
      name: '张三',
      age: 25
    };
    
    // app.js
    const person = require('./person.js');
    console.log(person.name); // '张三'
    console.log(person.age); // 25
    
    // utils.js - 导出多个函数
    module.exports = {
      formatDate: function(date) { /* ... */ },
      generateId: function() { /* ... */ }
    };
    
    // app.js
    const utils = require('./utils.js');
    // 方式1:直接用
    utils.formatDate(new Date());
    
    // 方式2:解构赋值(更方便)
    const { formatDate, generateId } = require('./utils.js');
    formatDate(new Date());
    

导入不同类型的内容

// 1. 导入核心模块(Node.js自带的)
const fs = require('fs');
const path = require('path');

// 2. 导入第三方包(npm安装的)
const lodash = require('lodash');
const express = require('express');

// 3. 导入自己写的文件(注意路径)
const utils = require('./utils.js'); // 同一目录
const config = require('../config.js'); // 上级目录
const helper = require('/absolute/path/helper.js'); // 绝对路径

// 4. 导入JSON文件(自动解析为对象)
const config = require('./config.json');
console.log(config.port); // 3000

// 5. 导入目录(自动找 index.js)
const routes = require('./routes');
// 等价于:require('./routes/index.js')

缓存机制

同一个文件多次导入,只会执行一次,返回同一个对象。

// counter.js
console.log('counter.js 被执行了');
let count = 0;
module.exports = {
  increment: function() {
    count++;
    return count;
  }
};
// app.js
const counter1 = require('./counter.js');
const counter2 = require('./counter.js');

console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 2(注意是2,不是1)

console.log(counter1 === counter2); // true(同一个对象)

输出:

counter.js 被执行了  (只打印一次!)
1
2
true

5.4 ES Module

ES Module 是 JavaScript 语言自带的模块化规范,用import和export关键字实现模块导入导出。

对比 CommonJS(Node.js 自创的),ES Module 是官方标准,未来所有 JavaScript 环境都会统一用这个。

如何启用 ES Module?

Node.js 默认使用 CommonJS,想用 ES Module 有三种方式:

  1. 文件扩展名用 .mjs

    # 文件命名
    utils.mjs  # ES Module
    app.mjs    # ES Module
    
    # 运行
    node app.mjs
    
  2. 在 package.json 中配置

    {
      "type": "module"  // 所有 .js 文件都按ES Module处理
    }
    
  3. 同时支持两种(不推荐)

    {
      "type": "module",
      "exports": {
        "import": "./index.mjs",  // import时使用
        "require": "./index.cjs"  // require时使用
      }
    }
    

导出

ES Module 有两种导出方式:命名导出和默认导出。

  1. 命名导出(Named Export)
    可以导出多个,导入时需要用相同的名字。

    // utils.js
    // 方式1:分别导出
    export const name = '工具库';
    export function formatDate(date) {
      return date.toLocaleDateString();
    }
    export const add = (a, b) => a + b;
    
    // 方式2:统一导出(先定义,最后一起导出)
    const name = '工具库';
    function formatDate(date) {
      return date.toLocaleDateString();
    }
    const add = (a, b) => a + b;
    
    export { name, formatDate, add };
    
    // 方式3:导出时重命名(对外暴露的名字变了)
    const name = '工具库';
    export { name as libName };
    // 导入时只能使用 libName,不能使用 name
    
  2. 默认导出(Default Export)

    每个文件只能有一个默认导出,导入时可以任意取名。

    // utils.js
    
    // 方式1:导出函数
    export default function add(a, b) {
      return a + b;
    }
    
    // 方式2:导出对象
    export default {
      name: '工具库',
      formatDate: function(date) { /* ... */ },
      add: function(a, b) { return a + b; }
    };
    
    // 方式3:导出类
    export default class Calculator {
      add(a, b) { return a + b; }
    }
    

命名导出 vs 默认导出

特性命名导出默认导出
每个文件数量多个最多1个
导入时的名字必须和导出时相同可以任意取名
语法export const name = ...export default ...
导入语法import { name } from ...import anyName from ...
使用场景工具函数库主要功能/组件

导入

  1. 导入命名导出

    // app.js
    
    // 导入单个
    import { formatDate } from './utils.js';
    
    // 导入多个
    import { formatDate, add } from './utils.js';
    
    // 导入时重命名(避免命名冲突)
    import { formatDate as dateFormat } from './utils.js';
    
    // 导入全部(不推荐,会失去tree-shaking优化)
    import * as utils from './utils.js';
    utils.formatDate();
    utils.add(1, 2);
    
  2. 导入默认导出

    // app.js
    
    // 默认导出可以随便取名
    import add from './utils.js';
    import myAdd from './utils.js';  // 换个名字也行
    import anything from './utils.js'; // 完全不同的名字也行
    
    // 调用
    console.log(add(1, 2)); // 3
    console.log(myAdd(1, 2)); // 3
    console.log(anything(1, 2)); // 3
    
  3. 混合导入(默认 + 命名)

    // utils.js
    export default function add(a, b) { return a + b; }
    export const version = '1.0.0';
    export const name = '工具库';
    
    // app.js
    // 默认导入放前面,命名导入放后面
    import add, { version, name } from './utils.js';
    
    console.log(add(1, 2)); // 3
    console.log(version); // '1.0.0'
    console.log(name); // '工具库'
    

ES Module的独特特性

  1. 静态加载(编译时)

    ES Module 的导入导出在代码执行前就已经确定,所以必须写在顶层。

    // ✅ 正确:import必须写在顶层
    import { add } from './math.js';
    
    // ❌ 错误:不能写在条件语句里
    if (true) {
      import { add } from './math.js'; // 报错!
    }
    
    // ❌ 错误:不能写函数里
    function test() {
      import { add } from './math.js'; // 报错!
    }
    

    好处:可以进行静态分析,实现 tree-shaking(打包时去掉没用到的代码)。

  2. 导入是只读的

    // utils.js
    export let count = 0;
    export function increment() {
      count++;
    }
    
    // app.js
    import { count, increment } from './utils.js';
    
    console.log(count); // 0
    increment();
    console.log(count); // 1
    
    // ❌ 不能直接修改导入的变量
    count = 10; // 报错!TypeError
    
  3. 动态导入(运行时加载)

    当需要按需加载时,可以使用import()函数。

    // app.js
    
    // 动态导入返回Promise
    const mathModule = await import('./math.js');
    console.log(mathModule.add(1, 2)); // 3
    
    // 条件加载
    if (condition) {
      const module = await import('./moduleA.js');
    } else {
      const module = await import('./moduleB.js');
    }
    
    // 路由懒加载(Vue/React中常见)
    const Home = () => import('./pages/Home.vue');
    

ES Module vs CommonJS 详细对比

特性CommonJSES Module
标准Node.js私有JavaScript官方
语法require() / module.exportsimport / export
加载时机运行时加载编译时加载(静态)
是否异步同步异步
是否支持tree-shaking❌✅
导入是否只读否(可修改)是(只读)
在浏览器使用❌✅
__dirname / __filename✅ 有❌ 没有
支持动态导入❌✅(import())

ES Module中的路径处理

在 ES Module 中,__dirname和__filename不可用,需要自己构造:

// CommonJS(可以直接用)
console.log(__dirname); // /Users/xxx/project
console.log(__filename); // /Users/xxx/project/app.js

// ES Module(需要构造)
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname); // /Users/xxx/project
console.log(__filename); // /Users/xxx/project/app.js

import.meta.url 是 ES Module 中获取当前文件路径的标准方式。

5.5 CommonJS vs ES Module

  1. 加载时机不同

    // CommonJS - 运行时加载
    const fs = require('fs'); // 代码执行到这里才会加载
    console.log('这行先执行');
    
    // ES Module - 编译时加载
    import { readFile } from 'fs'; // 代码执行前就已经加载完成
    console.log('这行后执行'); // 实际上import会提升到顶部
    
  2. 循环依赖的处理

    // a.js
    const b = require('./b.js');
    console.log('a.js 加载完成');
    module.exports = { name: 'A' };
    
    // b.js
    const a = require('./a.js');
    console.log('b.js 加载完成');
    module.exports = { name: 'B' };
    
    // 执行顺序:
    // 1. 加载a.js,发现require('b'),暂停a的执行
    // 2. 加载b.js,发现require('a'),但a正在加载中,返回不完整的a
    // 3. b继续执行,导出
    // 4. a继续执行,导出
    // 结果:b中获取的a是不完整的!
    

    ES Module 的循环依赖(有保护):

    // a.mjs
    import { b } from './b.mjs';
    export const a = 'A';
    
    // b.mjs
    import { a } from './a.mjs';
    export const b = 'B';
    
    // 不会报错,但a在b中可能是undefined(直到a完全加载)
    
  3. this 的指向不同

    // CommonJS
    console.log(this); // 指向当前模块的exports对象
    
    // ES Module
    console.log(this); // undefined
    
  4. 文件扩展名规则

    // CommonJS (.js默认)
    require('./file');      // 自动找 file.js
    require('./file.json'); // 自动解析JSON
    
    // ES Module (.mjs或配置后.js)
    import './file.mjs';    // 必须写完整扩展名
    // 如果package.json中设置 "type": "module",.js文件也按ES Module处理
    

5.6 Node.js中两种模块的互操作

这是实际项目中最常遇到的坑。

1. ES Module 导入 CommonJS

// commonjs-module.js
module.exports = { name: 'CommonJS' };

// es-module.mjs
import cjs from './commonjs-module.js';
console.log(cjs.name); // 'CommonJS'

// 导入整个对象
import * as cjs from './commonjs-module.js';
console.log(cjs.default.name); // 'CommonJS'

2. CommonJS 导入 ES Module

// es-module.mjs
export default { name: 'ES Module' };
export const version = '1.0';

// commonjs.js
const es = require('./es-module.mjs');
// ❌ 报错!CommonJS不能直接require ES Module

// 只能用动态import
async function load() {
  const es = await import('./es-module.mjs');
  console.log(es.default.name); // 'ES Module'
  console.log(es.version); // '1.0'
}

3. package.json中的配置

{
  "type": "commonjs", // 默认,.js文件按CommonJS处理
  // 或
  "type": "module"    // .js文件按ES Module处理
}

最佳实践:

  • 新项目用ES Module("type": "module")
  • 旧项目保持CommonJS
  • 工具类脚本用.mjs扩展名明确标识

5.7 模块解析路径的规则

当你说require('xxx')或import 'xxx'时,Node.js 按以下顺序查找:

1. 核心模块

require('fs'); // Node.js内置模块,直接返回

2. 相对路径模块

require('./utils.js'); // 相对当前文件
require('../lib/index.js');

3. 绝对路径模块

require('/Users/xxx/project/utils.js');

4. 第三方模块(从node_modules查找)

require('lodash');
// Node.js会从当前目录开始,逐级向上查找node_modules
// /project/node_modules/lodash
// /node_modules/lodash
// /Users/node_modules/lodash

5. 目录自动查找index

require('./routes');
// 依次查找:
// ./routes.js
// ./routes.json
// ./routes/index.js
// ./routes/index.json

5.8 常见问题和陷阱

  1. 循环依赖导致的 undefined

    // a.js
    const b = require('./b.js');
    console.log(b); // 可能是不完整的
    
    // 解决方案:延迟require
    module.exports = {
      run: () => {
        const b = require('./b.js');
        // 使用时再加载
      }
    };
    
  2. 导出对象的修改

    // utils.js
    exports.count = 0;
    exports.increment = function() { this.count++; };
    
    // app.js
    const utils = require('./utils.js');
    utils.increment();
    console.log(utils.count); // 1
    
    // 共享状态!多个地方修改会影响彼此
    
  3. __dirname 和 __filename 在 ES Module 中不可用

    // CommonJS
    console.log(__dirname); // 当前文件目录
    console.log(__filename); // 当前文件路径
    
    // ES Module
    import { fileURLToPath } from 'url';
    import { dirname } from 'path';
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    
Axios详解 2026-08-24
NPM 2026-08-24

© 2026 日志记录