Node.js 安装与使用指南

一、 Node.js 安装

A. Ubuntu 20.04 安装 Node.js

1. 方法一:官网下载安装

此方法允许您安装指定版本的 Node.js。

  • 下载安装包:

    • 访问官方网站 https://nodejs.org/en/download 或中国镜像网站 https://npmmirror.com/mirrors/node/ 选择所需版本(例如 v18.17.0)。
    • 使用 wget 下载(请根据实际版本调整命令):
      • 从官网下载:
        wget https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz
      • 或从中国镜像下载:
        wget https://npmmirror.com/mirrors/node/v18.17.0/node-v18.17.0-linux-x64.tar.xz
  • 创建目标目录并解压:

    # 创建安装目录
    sudo mkdir -p /usr/local/nodejs
    # 解压文件到目标目录,并移除顶层文件夹
    sudo tar -xf node-v18.17.0-linux-x64.tar.xz -C /usr/local/nodejs --strip-components=1
    • --strip-components=1 选项会去除解压出来的顶级文件夹,将文件直接解压到指定目录。
  • 配置环境变量:

    # 将Node.js路径添加到环境变量配置中
    echo 'export NODE_HOME=/usr/local/nodejs' >> ~/.bashrc
    echo 'export PATH=$NODE_HOME/bin:$PATH' >> ~/.bashrc
    echo 'export NODE_PATH=$NODE_HOME/lib/node_modules' >> ~/.bashrc
    # 使环境变量立即生效
    source ~/.bashrc
  • 验证安装:
    在终端中运行以下命令,查看 Node.js 和 npm 的版本号:

    node -v
    npm -v

2. 方法二:使用 apt (通过 NodeSource PPA)

此方法通常安装较新版本,但可能不如手动下载灵活。注意:原文示例中的 setup_6.x 版本非常老旧,建议使用更新的版本,例如 setup_18.x 或 setup_20.x

  • 安装 curl (如果尚未安装):

    sudo apt update
    sudo apt install curl -y
  • 获取 NodeSource 安装脚本:
    (以 Node.js 18.x 为例,请根据需要替换版本号)

    cd ~
    curl -sL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh
  • 运行脚本:

    sudo bash nodesource_setup.sh
  • 安装 Node.js:

    sudo apt-get install nodejs -y
  • 安装构建工具 (推荐,用于编译原生模块):

    sudo apt-get install build-essential -y
  • 验证安装:

    node -v
    npm -v

B. CentOS 安装 Node.js

1. 官网下载安装

此方法与 Ubuntu 手动下载类似。

  • 下载安装包:

    • 访问官网 https://nodejs.org/en/download/ 获取下载链接。
    • 使用 wget 下载(以 v16.14.0 为例):
      wget https://nodejs.org/dist/v16.14.0/node-v16.14.0-linux-x64.tar.xz
  • 解压:

    tar -xvf node-v16.14.0-linux-x64.tar.xz
  • 移动并重命名文件夹 (移动到 /usr/local 并命名为 nodejs):

    # 假设下载和解压在 /home/yys/test/ 目录下
    sudo mv /home/yys/test/node-v16.14.0-linux-x64 /usr/local/nodejs
    # 或者,如果在当前目录解压后:
    # sudo mv node-v16.14.0-linux-x64 /usr/local/nodejs
  • 配置环境变量 (全局生效):

    • 编辑 /etc/profile 文件:
      sudo vi /etc/profile
    • 在文件末尾添加:
      export PATH=$PATH:/usr/local/nodejs/bin
    • 保存退出后,使配置生效:
      source /etc/profile
  • 验证安装:

    node -v
    npm -v

C. 通用配置与问题处理

1. 更换 NPM 国内源

为了提高 npm install 的下载速度,可以更换为国内镜像源。

  • 临时使用:
    npm --registry https://registry.npm.taobao.org install <package-name>
  • 永久更换:
    npm config set registry https://registry.npm.taobao.org
  • 验证当前源:
    npm config get registry
  • 去掉 SSL 证书验证 (如果遇到证书问题,但不推荐在生产环境使用):
    npm config set strict-ssl false

2. 常见问题处理

  • Ubuntu (WSL) apt 报错:

    • 错误信息: E: Could not read response to hello message from hook [ ! -f /usr/bin/snap ] || /usr/bin/snap advise-snap --from-apt 2>/dev/null || true: Success
    • 原因: snapd 包与 WSL 可能存在兼容性问题。
    • 解决方法: 删除相关的 apt 配置文件。
      sudo rm -rf /etc/apt/apt.conf.d/20snapd.conf
      然后重试 apt 命令。
  • Windows 11 PowerShell 运行 npm 报错:

    • 错误信息: npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
    • 原因: PowerShell 的执行策略限制了脚本运行。
    • 解决方法:
      1. 以 管理员身份 打开 PowerShell。
      2. 查看当前执行策略:
        Get-ExecutionPolicy
      3. 更改执行策略(例如改为 RemoteSigned):
        Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
      4. 根据提示输入 Y 确认。
      5. 重新运行 npm 命令。

二、 Node.js 基础使用

A. 运行第一个脚本 (helloworld)

  1. 创建 00-helloworld.js 文件:
    vim 00-helloworld.js
  2. 写入 JavaScript 代码:
    var foo = 'bar';
    console.log(foo);
  3. 使用 node 命令运行:
    node 00-helloworld.js
  4. 预期输出:
    bar

B. 核心概念与常用模块

1. 运行环境差异 (无 BOM, DOM)

Node.js 中的 JavaScript 运行在服务器端,与浏览器环境不同:

  • 没有 浏览器提供的 windowdocument 等 BOM 和 DOM API。
  • 拥有 浏览器环境所没有的服务器端能力,如文件操作、网络通信等。
// 01-没有bom和dom.js
// 在 Node 中,采用 EcmaScript 进行编码
// 没有 BOM、DOM
// 和浏览器中的 JavaScript 不一样
// 下面的代码会报错:ReferenceError: window is not defined
// console.log(window)
// console.log(document)

2. 文件系统 (fs 模块)

Node.js 通过内置的 fs (file-system) 核心模块提供文件操作能力。使用前需要先 require

  • 读取文件 (fs.readFile):

    1. 创建 a.txt 文件,写入内容 (例如: hello nodejs 你好)。
    2. 创建 02-读取文件.js:
      // 1. 加载 fs 核心模块
      var fs = require('fs');
      
      // 2. 异步读取文件
      // 第一个参数:文件路径
      // 第二个参数:回调函数 (error, data)
      fs.readFile('a.txt', function (error, data) {
        if (error) {
          console.log('读取文件失败了:', error);
        } else {
          // data 是 Buffer 对象 (二进制数据)
          // console.log(data);
          // 使用 toString() 转为可读字符串 (默认utf8)
          console.log(data.toString());
        }
      });
    3. 运行 node 02-读取文件.js,输出 a.txt 的内容。
  • 写入文件 (fs.writeFile):

    1. 创建 data 文件夹 (如果需要)。
    2. 创建 2.4 写文件.js (原文文件名,建议修改为更有意义的如 04-writeFile.js):
      var fs = require('fs');
      
      // 第一个参数:文件路径
      // 第二个参数:要写入的内容
      // 第三个参数:回调函数 (error)
      fs.writeFile('./data/你好.md', '大家好,给大家介绍一下,我是Node.js', function (error) {
        if (error) {
          console.log('写入失败:', error);
        } else {
          console.log('写入成功了');
        }
      });
    3. 运行脚本,会在 data 目录下创建 你好.md 文件并写入内容。

3. 路径操作 (path 模块)

path 核心模块用于处理和转换文件路径。

  • 获取文件扩展名 (path.extname):
    // 2.3 路径 (建议命名为 03-path-extname.js)
    const path = require('path');
    
    // 文件路径示例
    const fpath = '/a/b/c/index.html';
    const fext = path.extname(fpath);
    console.log(fext); // 输出: .html
    
    console.log(path.extname('c:/a/b/c/d/hello.txt')); // 输出: .txt

4. HTTP 服务器 (http 模块)

http 核心模块用于创建 HTTP 服务器和客户端。

  • 创建基本服务器:

    // 2.5 http.js (建议命名为 05-http-server.js)
    // 1. 加载 http 核心模块
    var http = require('http');
    
    // 2. 创建 Web 服务器实例
    var server = http.createServer();
    
    // 3. 注册 request 事件监听器
    // 当有客户端请求时触发回调函数
    server.on('request', function (request, response) { // request: 请求对象, response: 响应对象
      console.log('收到客户端的请求了,请求路径是:' + request.url);
      console.log('请求客户端地址:', request.socket.remoteAddress, request.socket.remotePort);
    
      // 4. 发送响应
      response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); // 设置响应头,解决中文乱码
      response.write('你好,世界!');
      response.write(' Hello Node.js');
      response.end(); // 必须调用 end() 结束响应
    });
    
    // 5. 绑定端口号,启动服务器
    server.listen(3000, function () {
      console.log('服务器启动成功,可以通过 http://127.0.0.1:3000/ 来进行访问');
    });

    运行 node 05-http-server.js,然后在浏览器访问 http://127.0.0.1:3000/

  • 根据 URL 响应不同内容:

    // 2.7 http-url-res.js (建议命名为 06-http-routing.js)
    var http = require('http');
    
    var server = http.createServer();
    
    server.on('request', function (req, res) {
      var url = req.url; // 获取请求路径
    
      // 设置响应头 Content-Type
      res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    
      if (url === '/') {
        res.end('首页 index page');
      } else if (url === '/login') {
        res.end('登录 login page');
      } else if (url === '/products') {
        var products = [
          { name: '苹果 X', price: 8888 },
          { name: '菠萝 X', price: 5000 },
          { name: '小辣椒 X', price: 1999 }
        ];
        // 响应 JSON 数据需要先序列化,并设置正确的 Content-Type
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.end(JSON.stringify(products));
      } else {
        res.setHeader('Content-Type', 'text/html; charset=utf-8'); // 可以返回HTML
        res.statusCode = 404; // 设置状态码
        res.end('<h1>404 Not Found.</h1>');
      }
    });
    
    server.listen(3000, function () {
      console.log('服务器启动成功,可以访问了...');
    });
  • Content-Type (MIME 类型):

    • Content-Type 响应头用于告诉浏览器服务器发送的数据类型。
    • text/plain: 普通文本。
    • text/html: HTML 文档。
    • application/json: JSON 数据。
    • image/jpegimage/png: 图片。
    • text/css: CSS 文件。
    • application/javascript: JavaScript 文件。
    • 重要: 对于文本类型 (text/*application/jsonapplication/javascript 等),务必指定 charset=utf-8 以避免中文乱码。图片等二进制文件不需要指定字符集。
    • 常见 MIME 类型可参考: http://tool.oschina.net/commons
    // 2.11 http-content-type.js (示例已整合到上方代码中)
    // ...
    // 根据请求的 url 设置不同的 Content-Type
    if (url === '/plain') {
        res.setHeader('Content-Type', 'text/plain; charset=utf-8');
        res.end('hello 世界');
    } else if (url === '/html') {
        res.setHeader('Content-Type', 'text/html; charset=utf-8');
        res.end('<p>hello html <a href="">点我</a></p>');
    }
    // ...

5. 结合 http 与 fs 提供文件服务

可以读取文件内容并将其作为 HTTP 响应发送给客户端。

  1. 创建 resource 目录,并放入 index.htmlmain.cssmain.jsab2.jpg 等资源文件。
  2. 创建 2.12 http-fs.js (建议命名为 07-http-serve-files.js):
    var http = require('http');
    var fs = require('fs');
    var path = require('path'); // 引入 path 模块
    
    var server = http.createServer();
    
    var resourceDir = path.join(__dirname, 'resource'); // 资源目录路径
    
    server.on('request', function (req, res) {
      var url = req.url;
      var filePath = path.join(resourceDir, url); // 默认请求路径对应文件名
    
      // 如果请求根路径,则默认返回 index.html
      if (url === '/') {
        filePath = path.join(resourceDir, 'index.html');
      }
    
      fs.readFile(filePath, function (err, data) {
        if (err) {
          // 文件不存在或读取错误,返回 404
          res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
          res.end('<h1>404 Not Found</h1>');
        } else {
          // 根据文件扩展名设置 Content-Type
          var ext = path.extname(filePath);
          var contentType = 'text/plain'; // 默认类型
          switch (ext) {
            case '.html': contentType = 'text/html; charset=utf-8'; break;
            case '.css': contentType = 'text/css; charset=utf-8'; break;
            case '.js': contentType = 'application/javascript; charset=utf-8'; break;
            case '.jpg': case '.jpeg': contentType = 'image/jpeg'; break;
            case '.png': contentType = 'image/png'; break;
            // 添加更多类型...
          }
          res.writeHead(200, { 'Content-Type': contentType });
          res.end(data); // 发送文件内容 (Buffer 或 String)
        }
      });
    });
    
    server.listen(3000, function () {
      console.log('Server is running at http://127.0.0.1:3000/');
    });
    运行后,访问 http://127.0.0.1:3000/ 会显示 index.html,访问 http://127.0.0.1:3000/ab2.jpg 会显示图片。

6. 操作系统信息 (os 模块)

os 核心模块提供与操作系统相关的方法。

// 2.8 node中的js-核心模块.js (建议命名为 08-os-module.js)
// 用来获取机器信息的
var os = require('os');

// 获取 CPU 信息
console.log('CPU Info:', os.cpus());

// 获取总内存大小 (字节)
console.log('Total Memory (bytes):', os.totalmem());

// 获取操作系统平台 ('darwin', 'freebsd', 'linux', 'sunos', 'win32')
console.log('Platform:', os.platform());

7. IP 地址和端口号

  • IP 地址: 用于在网络中定位计算机。
  • 端口号: 用于定位计算机上运行的具体应用程序。每个需要网络通信的应用都会监听一个或多个端口号。
// 2.10 ip地址和端口号.js (内容已在 05-http-server.js 中体现)
// 在 http server 的 request 事件回调中可以获取客户端 IP 和端口
// console.log('请求我的客户端的地址是:', req.socket.remoteAddress, req.socket.remotePort)

C. 模块化 (requireexportsmodule.exports)

Node.js 采用了 CommonJS 模块规范。每个 .js 文件都是一个独立的模块,拥有自己的作用域。

  • require: 用于加载模块。
    • 加载核心模块: require('fs')require('http')
    • 加载文件模块: require('./b') (相对路径,./ 不能省略,.js 后缀可省略)。
    • 加载第三方模块 (npm 安装的): require('art-template')
  • exports: 每个模块内部都有一个 exports 对象。可以将需要暴露给外部的成员挂载到 exports 对象上。
  • module.exportsexports 是 module.exports 的一个引用。可以直接给 module.exports 赋值来导出单个成员(如函数或类),或者直接导出一个新的对象。通常推荐使用 exports.xxx = ... 方式,或者直接 module.exports = ...

示例:

  1. 创建 b.js:
    // b.js
    console.log('b.js start');
    
    var foo = 'bbb';
    
    function add(x, y) {
      return x + y;
    }
    
    exports.foo = 'hello from b'; // 导出字符串
    exports.add = add;           // 导出函数
    exports.readFile = function (path) { // 导出匿名函数
      console.log('Simulating reading file:', path);
    };
    const age = 18;
    exports.age = age;             // 导出变量
    
    // module.exports = { // 或者直接导出整个对象
    //   foo: 'hello from b',
    //   add: add,
    //   readFile: function(path){...},
    //   age: age
    // }
    
    require('./c'); // 加载 c.js
    
    console.log('b.js end');
  2. 创建 c.js:
    // c.js
    console.log('c.js executed');
  3. 创建 a.js (加载 b.js):
    // a.js (对应原文 2.9 简单的模块化 部分)
    console.log('a.js start');
    var fs = require('fs'); // 加载核心模块
    
    // 加载自定义模块 b.js,require 返回 b.js 的 exports 对象
    var bExports = require('./b');
    
    console.log('Accessing exports from b.js:');
    console.log('bExports.foo:', bExports.foo);           // 输出: hello from b
    console.log('bExports.add(10, 30):', bExports.add(10, 30)); // 输出: 40
    console.log('bExports.age:', bExports.age);             // 输出: 18
    bExports.readFile('./a.js');                          // 输出: Simulating reading file: ./a.js
    
    // 模块内的变量 foo 不会被导出,这里访问的是 a.js 自己的 foo
    var foo = 'aaa';
    console.log('foo in a.js:', foo); // 输出: aaa
    
    console.log('a.js end');
    
    // 示例:使用核心模块 fs
    fs.readFile('./a.js', function (err, data) {
      if (err) {
        console.log('读取 a.js 失败');
      } else {
        // console.log(data.toString()); // 打印 a.js 自身内容
      }
    });
  4. 运行 node a.js,观察输出顺序和导出成员的访问。
    a.js start
    b.js start
    c.js executed
    b.js end
    Accessing exports from b.js:
    bExports.foo: hello from b
    bExports.add(10, 30): 40
    bExports.age: 18
    Simulating reading file: ./a.js
    foo in a.js: aaa
    a.js end

三、 包管理 (NPM) 与项目配置

A. NPM 安装第三方包

NPM (Node Package Manager) 是 Node.js 的包管理器。

  • 本地安装 (Local Installation):
    • 命令: npm install <package-name>
    • 作用: 将包安装到当前项目目录下的 node_modules 文件夹中,并记录到 package.json 的 dependencies。只对当前项目有效。
    • 使用: 在项目代码中通过 require() 或 import 使用。
  • 全局安装 (Global Installation):
    • 命令: npm install <package-name> -g (或 --global)
    • 作用: 将包安装到系统全局路径下,通常用于安装命令行工具(如 pm2nodemonhttp-server)。
    • 使用: 在命令行中直接使用该工具。
  • 开发依赖安装 (Development Dependencies):
    • 命令: npm install <package-name> --save-dev (或 -D)
    • 作用: 将包安装到 node_modules,并记录到 package.json 的 devDependencies。这些包只在开发和构建过程中需要(如测试框架、构建工具、代码检查工具)。
    • 例如: npm install jest --save-dev

B. package.json 文件

package.json 是 Node.js 项目的配置文件,通常位于项目根目录。

  • 作用:

    • 项目元数据: 记录项目名称、版本、描述、作者、许可证等。
    • 依赖管理dependencies (运行时依赖) 和 devDependencies (开发时依赖) 字段列出项目所需的第三方包及其版本范围。运行 npm install 时会根据此文件安装依赖。
    • 脚本命令scripts 字段可以定义常用的项目命令,如启动 (start)、测试 (test)、构建 (build) 等。可以通过 npm run <script-name> 执行。
    • 版本控制与共享: 将 package.json 纳入版本控制(如 Git),方便团队成员共享和恢复项目环境。
    • 项目初始化: 通过 npm init 或 npm init -y 命令可以快速生成一个基本的 package.json 文件。
  • npm init 报错处理:

    • 如果在运行 npm install <package-name> 时遇到 ENOENT: no such file or directory, open '.../package.json' 错误,说明当前目录缺少 package.json 文件。
    • 解决方法: 在项目根目录下运行 npm init,根据提示填写信息,或直接运行 npm init -y 使用默认配置快速生成。

C. 使用模板引擎 (示例: art-template)

模板引擎用于将数据和 HTML 模板结合生成最终的 HTML 页面。

  1. 安装: 在项目目录下执行:
    npm install art-template
    (如果尚未初始化项目,请先运行 npm init -y)
  2. 创建 HTML 模板文件 (tpl.html):
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>{{ title }}</title> <!–– 使用模板变量 -->
    </head>
    <body>
      <p>大家好,我叫:{{ name }}</p>
      <p>我今年 {{ age }} 岁了</p>
      <h1>我来自 {{ province }}</h1>
      <p>我喜欢:
        {{each hobbies}} <!–– 遍历数组 -->
          {{ $value }} <!–– 当前项的值 -->
        {{/each}}
      </p>
    </body>
    </html>
  3. 创建 Node.js 脚本 (05-在node中使用模板引擎.js,建议命名为 09-template-engine.js):
    // 1. 加载 art-template 模块
    var template = require('art-template');
    var fs = require('fs');
    var path = require('path');
    
    // 模板文件路径
    var tplPath = path.join(__dirname, 'tpl.html');
    
    // 读取模板文件内容
    fs.readFile(tplPath, function (err, data) {
      if (err) {
        return console.log('读取模板文件失败了:', err);
      }
    
      // data 是 Buffer,需要转为字符串
      var tplString = data.toString();
    
      // 准备数据
      var viewData = {
        name: 'Jack',
        age: 18,
        province: '北京市',
        hobbies: [
          '写代码',
          '唱歌',
          '打游戏'
        ],
        title: '个人信息'
      };
    
      // 3. 使用 template.render(模板字符串, 数据对象) 渲染模板
      var renderedHtml = template.render(tplString, viewData);
    
      // 输出渲染后的 HTML 字符串
      console.log(renderedHtml);
    
      // 实际应用中,可以将 renderedHtml 通过 http response 发送给浏览器
      // res.setHeader('Content-Type', 'text/html; charset=utf-8');
      // res.end(renderedHtml);
    });
  4. 运行脚本node 09-template-engine.js,会输出渲染好的 HTML 内容。

四、 进程管理 (PM2)

PM2 是一个流行的 Node.js 应用程序进程管理器,可以简化应用的部署、监控和管理。

A. 安装 PM2

  • 使用 npm 全局安装:
    sudo npm install -g pm2
    • 如果遇到权限问题,可能需要 sudo,或者配置 NPM 全局安装路径权限。

B. 启动和管理应用

  1. 创建 Node.js 应用 (app.js):

    // app.js - 一个简单的 HTTP 服务器示例
    const http = require('http');
    
    http.createServer(function(req, res) {
      res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
      res.end('你好,PM2 管理的应用!\n');
    }).listen(8080, '127.0.0.1'); // 监听本地 8080 端口
    
    console.log('服务器运行在 http://127.0.0.1:8080/');
  2. 使用 PM2 启动应用:
    在 app.js 所在目录下运行:

    pm2 start app.js
    • 输出会显示应用的状态信息,包括 app name, id, status, memory usage 等。
  3. 常用 PM2 命令:

    • pm2 list: 列出所有由 PM2 管理的应用。
    • pm2 stop <app_name|id>: 停止指定应用。
    • pm2 restart <app_name|id>: 重启指定应用。
    • pm2 delete <app_name|id>: 从 PM2 列表中移除应用(并停止)。
    • pm2 logs [<app_name|id>]: 查看应用日志。
    • pm2 monit: 监控所有应用的 CPU 和内存使用情况。
    • pm2 startup: 生成开机自启动脚本 (需要按提示执行生成的命令)。
    • pm2 save: 保存当前 PM2 进程列表,以便重启后恢复。

五、 使用 Nginx 作为反向代理

当 Node.js 应用监听在非 80 端口(如 8080)时,通常使用 Nginx 作为反向代理,将来自公网 80 端口的请求转发给 Node.js 应用。

A. 配置 Nginx

  1. 安装 Nginx (如果尚未安装):

    • Ubuntu: sudo apt update && sudo apt install nginx -y
    • CentOS: sudo yum update && sudo yum install nginx -y
  2. 编辑 Nginx 配置文件:
    通常是 /etc/nginx/sites-available/default (Ubuntu) 或 /etc/nginx/nginx.conf 或 /etc/nginx/conf.d/default.conf (CentOS)。

    sudo nano /etc/nginx/sites-available/default # Ubuntu 示例
  3. 在 server 块中配置 location /:
    找到 server { ... } 块,修改或添加 location / { ... } 部分:

    server {
        listen 80 default_server;
        listen [::]:80 default_server;
    
        # ... 其他配置 ...
    
        server_name your_domain_or_ip; # 替换为你的域名或服务器 IP
    
        location / {
            # 将请求转发给本地监听 8080 端口的 Node.js 应用
            proxy_pass http://127.0.0.1:8080;
    
            # 设置必要的头信息,以便后端应用获取真实信息
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade; # 支持 WebSocket
            proxy_set_header Connection 'upgrade'; # 支持 WebSocket
            proxy_set_header Host $host; # 传递原始 Host 头
            proxy_set_header X-Real-IP $remote_addr; # 传递真实客户端 IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme; # 传递协议 (http/https)
            proxy_cache_bypass $http_upgrade;
        }
    
        # 可以添加其他 location 块代理到不同端口的应用
        # location /secondapp {
        #     proxy_pass http://127.0.0.1:8081;
        #     # ... 其他 proxy_set_header 等配置 ...
        # }
    
        # ... 其他配置 ...
    }
  4. 检查 Nginx 配置语法:

    sudo nginx -t

    如果显示 syntax is ok 和 test is successful,则配置无误。

  5. 重启 Nginx 服务:

    sudo systemctl restart nginx

    或者 sudo service nginx restart

现在,假设你的 Node.js 应用(如上面 app.js 示例)正在运行并监听 8080 端口,你应该可以通过访问服务器的 IP 地址或域名(默认 80 端口)来访问该应用了。

六、 Node.js 编码风格与实践

A. 通用编码规范建议

  • 缩进: 使用 2 个空格进行缩进。
  • 字符串: 优先使用单引号 ('),除非字符串内包含单引号需要转义。
  • 变量: 避免声明未使用或冗余的变量。
  • 分号:
    • 可以选择不加分号(ASI - Automatic Semicolon Insertion 会在多数情况下自动处理)。
    • 注意: 如果不加分号,行首不要以 ([, 或 ` (模板字符串) 开头。如果必须这样写,请在该行 前面加上分号 ;,例如 ;(function(){...})()。这是省略分号最容易出问题的地方。
  • 空格:
    • 关键字后加空格: if (condition) { ... }
    • 函数名后加空格 (函数声明): function name (arg) { ... } (匿名函数表达式通常不加: var fn = function() {})
  • 比较: 坚持使用全等 (===) 和不全等 (!==),避免使用 (==) 和 (!=),除非是判断 null 或 undefined (obj == null)。
  • 错误处理一定要处理 Node.js 异步回调中的 err 参数。
  • 浏览器全局变量: 在 Node.js 中避免直接使用 windowdocumentnavigator 等浏览器全局变量(除非你在写特定环境的代码如 Electron 渲染进程)。
  • 避免全局变量冲突: 避免使用常见的、易冲突的全局变量名,如 openlengtheventname

B. 无分号风格的注意事项示例

// 00-代码无分号问题.js (建议命名 10-semicolon-issues.js)
function say() {
  console.log('hello world')
}

// 如果上一行没有分号,下一行以 ( 开头,会被解析为函数调用
// say() // 正确调用
// (function () { ... })() // IIFE

// 正确写法(如果省略分号):
say() // 上一行有换行符,ASI 会插入分号

;(function () { // 在 IIFE 前加分号,避免与上一行连接
  console.log('IIFE executed')
})()

;['苹果', '香蕉'].forEach(function (item) { // 在数组字面量前加分号
  console.log(item)
})

;`hello`.toString() // 在模板字符串前加分号

// 结论:
// 无论是否使用分号,都建议在一行代码是以 (、[、` 开头的,
// 并且可能与前一行产生语法歧义时,在其前面加上分号 ;

七、 补充脚本与示例

A. 自定义日志打印函数 (带脚本名、行号、时间)

// print.js
const path = require('path');

function customPrint(...args) {
  // 获取调用堆栈信息
  const stack = (new Error()).stack.split('\n');
  // 尝试匹配调用者的信息 (通常是 stack[2],stack[0]是Error, stack[1]是customPrint自身)
  const callerFrame = stack[2] || ''; // 获取调用 customPrint 的那一行信息
  const match = callerFrame.match(/\s*at.*?\((.*?):(\d+):(\d+)\)|\s*at\s*(.*?):(\d+):(\d+)$/); // 兼容不同格式

  let scriptPath = '';
  let lineNumber = '';
  let columnNumber = '';
  let scriptName = 'unknown';

  if (match) {
    scriptPath = match[1] || match[4] || ''; // 文件路径
    lineNumber = match[2] || match[5] || ''; // 行号
    columnNumber = match[3] || match[6] || ''; // 列号
    if (scriptPath) {
      scriptName = path.basename(scriptPath); // 文件名
    }
  }

  // 获取当前时间并格式化
  const now = new Date();
  const formattedDate = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;

  // 构造前缀
  const printStr = `[${scriptName}:${lineNumber}:${columnNumber} ${formattedDate}] `;

  // 处理参数,对象转为 JSON 字符串
  const argStrings = args.map(arg => {
    if (typeof arg === 'object' && arg !== null) {
      try {
        return JSON.stringify(arg);
      } catch (e) {
        return '[Object]'; // 处理循环引用等无法序列化的情况
      }
    } else {
      return String(arg); // 其他类型转为字符串
    }
  });

  // 输出日志
  console.log(printStr + argStrings.join(' '));
}

// 导出函数,以便其他模块使用
module.exports = {
  customPrint
};

// --- 使用示例 (在另一个文件,如 main.js) ---
// const myLogger = require('./print');
// myLogger.customPrint('Hello,', 'world!', { key: 'value' });
// 输出类似: [main.js:2:10 2023/08/24 10:30:00] Hello, world! {"key":"value"}

B. 发送邮件 (使用 nodemailer)

  1. 安装 nodemailer:

    npm install nodemailer
  2. 发送基本文本邮件:

    // sendTextEmail.js
    const nodemailer = require('nodemailer');
    
    async function sendEmail(to, subject, text) {
      try {
        // 创建一个 SMTP 传输器 (以 QQ 邮箱为例)
        let transporter = nodemailer.createTransport({
          host: 'smtp.qq.com', // QQ邮箱的 SMTP 服务器
          port: 465,          // SSL 端口
          secure: true,       // 使用 SSL
          auth: {
            user: 'your_qq_email@qq.com', // 你的 QQ 邮箱地址
            pass: 'your_qq_smtp_password' // 你的 QQ 邮箱 SMTP 授权码 (不是邮箱密码)
          }
        });
    
        // 设置邮件选项
        let mailOptions = {
          from: '"Your Name" <your_qq_email@qq.com>', // 发件人显示名称和地址
          to: to,           // 收件人地址 (可以是多个,用逗号分隔)
          subject: subject, // 邮件主题
          text: text        // 纯文本邮件内容
          // html: '<b>Hello world?</b>' // 或者使用 HTML 内容
        };
    
        // 发送邮件
        let info = await transporter.sendMail(mailOptions);
        console.log('邮件发送成功: %s', info.messageId);
        // console.log('预览 URL: %s', nodemailer.getTestMessageUrl(info)); // 仅用于 Ethereal 测试账户
      } catch (error) {
        console.error('邮件发送失败:', error);
      }
    }
    
    // 调用函数发送邮件
    sendEmail('recipient@example.com', '来自 Node.js 的问候', '这是一封通过 Node.js 发送的测试邮件!');
    • 注意: 需要将 your_qq_email@qq.com 和 your_qq_smtp_password 替换为你的实际邮箱和生成的 SMTP 授权码。
  3. 发送带嵌入图片的 HTML 邮件:

    // sendHtmlEmailWithImages.js
    const nodemailer = require('nodemailer');
    const fs = require('fs');
    const path = require('path');
    
    // 创建邮件传输对象 (同上)
    const transporter = nodemailer.createTransport({
      host: 'smtp.qq.com',
      port: 465,
      secure: true,
      auth: {
        user: 'your_qq_email@qq.com',
        pass: 'your_qq_smtp_password'
      }
    });
    
    // 生成带嵌入图片的 HTML 内容
    function generateHTMLContent(images) {
      let attachments = []; // 用于存放附件信息 (嵌入图片)
      let imageTagsHTML = ''; // 用于存放 <img> 标签
    
      images.forEach((imagePath, index) => {
        const cid = `image${index}@nodemailer.com`; // 为每张图片生成唯一的 CID
        const filename = path.basename(imagePath);
    
        attachments.push({
          filename: filename,
          path: imagePath,
          cid: cid // 关键:指定 CID
        });
    
        // 在 HTML 中使用 cid 引用图片
        imageTagsHTML += `<p><img src="cid:${cid}" alt="${filename}" style="max-width: 100%; height: auto;"/></p>`;
      });
    
      // 完整的 HTML 内容
      let emailHTML = `
        <h1>这是一封带嵌入图片的HTML邮件</h1>
        <p>下面是嵌入的图片:</p>
        ${imageTagsHTML}
        <p>邮件结束。</p>
      `;
    
      return { html: emailHTML, attachments: attachments };
    }
    
    // 封装发送邮件函数
    async function sendEmailWithImages(to, subject, imagePaths) {
      try {
        const { html, attachments } = generateHTMLContent(imagePaths);
    
        // 邮件选项
        const mailOptions = {
          from: '"Your Name" <your_qq_email@qq.com>',
          to: to,
          subject: subject,
          html: html,         // HTML 内容
          attachments: attachments // 附件 (包含嵌入图片)
        };
    
        // 发送邮件
        let info = await transporter.sendMail(mailOptions);
        console.log('带图片邮件发送成功: %s', info.messageId);
      } catch (error) {
        console.error('带图片邮件发送失败:', error);
      }
    }
    
    // 使用示例
    const recipientEmail = 'recipient@example.com';
    const emailSubject = '带多张图片的 HTML 邮件测试';
    const imagePaths = [
      path.join(__dirname, 'resource/ab2.jpg'), // 图片文件的实际路径
      // path.join(__dirname, 'path/to/another_image.png')
    ];
    
    sendEmailWithImages(recipientEmail, emailSubject, imagePaths);

C. Electron 倒计时关闭窗口示例

Electron 用于构建跨平台的桌面应用程序。

  1. 创建项目并安装 Electron:
    mkdir electron-countdown-app
    cd electron-countdown-app
    npm init -y
    npm install electron --save-dev
  2. 创建 main.js (主进程):
    // main.js
    const { app, BrowserWindow, ipcMain } = require('electron');
    const path = require('path');
    const url = require('url');
    
    let mainWindow;
    
    function createWindow() {
      mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
          nodeIntegration: true, // 允许在渲染进程中使用 Node.js API (为了 require)
          contextIsolation: false, // 关闭上下文隔离 (为了 require, 不推荐在生产环境直接这样用)
          // preload: path.join(__dirname, 'preload.js') // 更安全的做法是使用 preload 脚本
        },
      });
    
      // 加载 index.html
      mainWindow.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
      }));
    
      // mainWindow.webContents.openDevTools(); // 打开开发者工具
    
      mainWindow.on('closed', () => {
        mainWindow = null;
      });
    }
    
    app.whenReady().then(createWindow);
    
    // 监听渲染进程发送的 'close-window' 事件
    ipcMain.on('close-window', () => {
      console.log('收到关闭窗口请求,执行关闭...');
      if (mainWindow) {
        mainWindow.close();
      }
    });
    
    // 其他应用生命周期事件处理...
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit();
      }
    });
    
    app.on('activate', () => {
      if (mainWindow === null) {
        createWindow();
      }
    });
  3. 创建 index.html (渲染进程):
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>倒计时关闭窗口</title>
      <style>
        body { font-family: sans-serif; text-align: center; padding-top: 50px; }
        #countdown { font-size: 72px; color: firebrick; margin: 20px 0; }
        button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
      </style>
    </head>
    <body>
      <h1>窗口将在倒计时结束后自动关闭</h1>
      <div id="countdown">10</div>
      <button id="cancelButton">取消关闭</button>
      <button id="closeNowButton">立即关闭</button>
    
      <script>
        const { ipcRenderer } = require('electron'); // 引入 ipcRenderer
    
        let remainingTime = 10; // 初始倒计时秒数
        const countdownElement = document.getElementById('countdown');
        const cancelButton = document.getElementById('cancelButton');
        const closeNowButton = document.getElementById('closeNowButton');
        let intervalId = null;
    
        function updateCountdown() {
          countdownElement.textContent = remainingTime;
        }
    
        function startCountdown() {
          updateCountdown();
          intervalId = setInterval(() => {
            remainingTime--;
            updateCountdown();
            if (remainingTime <= 0) {
              clearInterval(intervalId);
              // 发送事件到主进程请求关闭窗口
              ipcRenderer.send('close-window');
            }
          }, 1000); // 每秒执行一次
        }
    
        cancelButton.addEventListener('click', () => {
          if (intervalId) {
            clearInterval(intervalId);
            countdownElement.textContent = '已取消';
            console.log('倒计时已取消');
            // 可以在这里禁用按钮等
            cancelButton.disabled = true;
            closeNowButton.disabled = true;
          }
        });
    
        closeNowButton.addEventListener('click', () => {
           if (intervalId) clearInterval(intervalId);
           ipcRenderer.send('close-window');
        });
    
    
        // 页面加载完成后开始倒计时
        startCountdown();
      </script>
    </body>
    </html>
  4. 在 package.json 中添加启动脚本:
    {
      "name": "electron-countdown-app",
      "version": "1.0.0",
      "description": "",
      "main": "main.js", // 确认入口文件是 main.js
      "scripts": {
        "start": "electron .", // 添加 start 脚本
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "electron": "^..." // 版本号可能不同
      }
    }
  5. 运行应用:
    npm start
    或者直接运行 npx electron .

D. MySQL 数据库操作 (使用 mysql2)

mysql2 是一个流行的 Node.js MySQL 驱动库,性能较好,支持 Promise。

  1. 安装 mysql2:

    npm install mysql2
  2. 连接数据库并执行插入操作:

    // mysqlInsert.js
    const mysql = require('mysql2/promise'); // 使用 promise 版本的 mysql2
    
    async function main() {
      let connection; // 将 connection 声明在 try 外部,以便 finally 中访问
      try {
        // 创建数据库连接池 (推荐)
        const pool = mysql.createPool({
          host: 'localhost',       // 数据库主机名
          user: 'your_db_user',    // 数据库用户名
          password: 'your_db_password', // 数据库密码
          database: 'your_db_name',  // 数据库名
          waitForConnections: true,
          connectionLimit: 10,    // 连接池最大连接数
          queueLimit: 0           // 排队请求限制 (0表示不限制)
        });
    
        console.log('连接池创建成功');
    
        // 从连接池获取一个连接
        connection = await pool.getConnection();
        console.log('获取连接成功');
    
        // 准备要插入的数据
        const newRecord = {
          name: 'Alice', // 假设表中有 name 和 email 字段
          email: `alice_${Date.now()}@example.com`
        };
    
        // 执行插入 SQL 语句
        // 使用占位符 (?) 防止 SQL 注入
        const [results, fields] = await connection.query('INSERT INTO your_table_name SET ?', newRecord);
    
        console.log('插入成功,记录 ID:', results.insertId);
    
        // 查询刚插入的数据 (示例)
        const [rows] = await connection.query('SELECT * FROM your_table_name WHERE id = ?', [results.insertId]);
        console.log('查询到的新记录:', rows[0]);
    
      } catch (error) {
        console.error('数据库操作出错:', error);
      } finally {
        // 无论成功或失败,最后都要释放连接回连接池
        if (connection) {
          connection.release();
          console.log('连接已释放');
        }
        // 如果不再需要连接池,可以关闭它 (通常在应用退出时)
        // pool.end();
      }
    }
    
    main(); // 执行主函数
    • 注意:
      • 将 your_db_useryour_db_passwordyour_db_nameyour_table_name 替换为你的实际配置。
      • 确保数据库和表已存在,且表结构包含 nameemailid (假设 id 是自增主键) 字段。
      • 使用 async/await 处理 Promise,代码更简洁。
      • 使用连接池 (createPool) 比每次创建连接 (createConnection) 更高效。
      • 必须 在 finally 块中释放连接 (connection.release())。
  3. mysql vs mysql2:

    • 性能mysql2 通常性能更好,尤其在高并发下。
    • APImysql2 提供了更好的 Promise 支持 (通过 require('mysql2/promise')),使得 async/await 使用更方便。mysql 主要基于回调。
    • 连接池mysql2 内建了连接池支持。mysql 需要额外库或手动实现。
    • Prepared Statementsmysql2 对预处理语句的支持更好,有助于提升性能和安全性。
    • 推荐: 对于新项目或追求性能和现代语法的项目,推荐使用 mysql2

E. 导入自定义函数

正如前面 模块化 部分和 自定义日志函数 部分所示,Node.js 通过 module.exports 或 exports 导出模块成员,通过 require() 导入和使用这些成员。

导出 (例如 print.js):

// print.js
function customPrint(text) {
  console.log('My Custom Print:', text);
}

function anotherUtil() {
  // ...
}

// 导出需要被外部使用的函数
module.exports = {
  customPrint: customPrint, // 可以重命名导出的接口
  anotherUtil             // 如果导出名和内部名相同,可以简写
};

导入和使用 (例如 main.js):

// main.js
// 使用 require 导入 print.js 导出的对象
const myUtils = require('./print'); // 假设 print.js 在同级目录

// 调用导出的函数
myUtils.customPrint('Hello from main.js!');
// myUtils.anotherUtil();

这份整理后的文档应该更清晰、更有条理。希望对您有帮助!