knowledge

Node.js学习笔记

目录

  1. 安装

    1. nvm更新Node.js版本
    2. n更新Node.js版本
  2. npm

    1. 一个项目中同时安装、使用同个依赖包的不同版本
  3. CommonJS规范
  4. 原理机制

    1. Node.js的运行机制
    2. 特性
    3. Node.js核心模块(需要require引入)
    4. Node.js全局变量
    5. Tips
  5. 工具使用

    1. Koa
    2. express
    3. pm2

安装

nvm更新Node.js版本

  1. macOS或Linux的nvm

     nvm list-remote
     nvm install v新版本号
     nvm use v新版本号
     nvm alias default v新版本号
    
     nvm list
     nvm uninstall v旧版本号     # 若无法删除,则用管理员权限按要求设置文件夹权限,还可以去目录删除 /Users/「用户名」/.nvm/versions/node/v「版本号」
    

    安装的全局软件包位置:/Users/「用户名」/.nvm/versions/node/v「版本号」/lib/node_modules

  2. Windows的nvm-windows

    安装nvm-windows时,需要删除原本安装在电脑上的Node.js。

     nvm node_mirror https://npm.taobao.org/mirrors/node/    # 设置node源
     nvm npm_mirror https://npm.taobao.org/mirrors/npm/      # 设置npm源
    
     nvm list available
     nvm install 新版本号
     nvm use 新版本号
    
     nvm list
     nvm uninstall 旧版本号      # 还可以再去目录中删除 C:\Users\「用户名」\AppData\Roaming\nvm\v「版本号」
    

切换版本之后需重装Node.js的全局模块包。

n更新Node.js版本

不支持Windows。

  1. 安装

     npm install -g n
    
     # 或:brew、curl
    
  2. 切换版本

     n ls
    
    
     # 安装最新的长期支持正式发布版本
     n lts
    
     # 安装最新正式发布版本
     n latest
    
     # 安装指定版本
     n 新版本号
    
    
     # 展示已经安装的版本,并选择使用哪个版本
     n
    
    
     # 删除指定版本
     n rm 旧版本号
    
     # 删除当前版本之外的所有版本
     n prune
    
     # 删除当前版本安装的node和npm
     n uninstall
    

npm

npm(Node Package Manager)。

  1. npmCI

    在任意命令后添加-h--help查看当前命令的所有参数。

    1. 制作

      1. 登录

        npm login

      2. 初始化package.json

        npm init

        • 修改初始化信息

            npm set init.author.name "名字"
            npm set init.author.email "邮箱"
            npm set init.license "MIT"
          

          初始化信息会存放在~/.npmrc文件里。

      3. 调试开发

        npm linknpm unlink

        1. 仅本地

          1. 引用本地模块的仓库:npm link 本地模块的路径

            • 取消:

              引用本地模块的仓库:npm unlink 模块名

        2. 全局

          1. 本地模块:npm link

            也支持bin。

            • 取消:

              本地模块:npm unlink

          2. 引用本地模块的仓库:npm link 模块名

            • 取消:

              引用本地模块的仓库:npm unlink 模块名

      4. 发布(默认:发布至latest标签)

        npm publish [--tag <tag>]

        1. 除了latest,其他标签都不会默认被安装。最后推送的latest版本会显示在npm官网。
        2. 注意:设置源为npm的网站(https://registry.npmjs.org/)才可以推送到npm。
      5. 「下线」

        npm unpublish [<@scope>/]<pkg>[@<version>]只能下线24小时内发布的版本。

        npm deprecate <pkg>[@<version>] <message>

      6. 打印登录名

        npm whoami

      7. 登出

        npm logout

    2. 查看信息

      1. 查看模块官方信息

        npm view [<@scope>/]<pkg>[@<version>] [<field>[.subfield]...]

        • npm view <pkg> versions:展示所有版本
      2. 查看已安装的模块和依赖

        npm list [[<@scope>/]<pkg> ...]

        仅查看顶层依赖:npm list --depth=0

      3. 查看已安装模块是否需要升级

        npm outdated [[<@scope>/]<pkg> ...]

      4. 查看、添加、删除仓库标签的最后版本

        每个标签仅保留最后一个版本;latest标签无法删除。

         npm dist-tag ls [<pkg>]
         npm dist-tag add <pkg>@<version> [<tag>]
         npm dist-tag rm <pkg> <tag>
        
    3. 安装

      改变安装包的顺序会影响安装包的内容和依赖。

      • 手动安装npm自己

        1. 在本地Node.js同目录下创建目录node_modules\npm
        2. 下载并解压https://github.com/npm/cli/releases至本地Node.js同目录下的node_modules\npm
        3. 拷贝node_modules\npm\bin下面的npmnpm.cmd(、npxnpx.cmd)到本地Node.js同目录下。
      1. 安装包

        自动选择范围内最后发布的版本,安装到本地或全局的node_modules。全局安装会额外创建系统命令。

        1. 安装方式

          1. npm install安装所在目录的package.json文件描述内容

            1. 最新(主版本号最新,第一个数):

              *x

            2. 次版本号最新(第二个数):

              11.x^1.2.3

            3. 补丁号最新(第三个数):

              1.21.2.x~1.2.3

            4. 确定的版本:

              1.2.3

            主版本号.次版本号.补丁号,详细定义查看Semantic

          2. npm install [<@scope>/]<name>[@<tag>]

            1. 没有后缀,则最后发布的latest版本。
            2. @内容

              1. 具体版本号
              2. 标签
              3. 版本范围:>>=<<= + 版本号。范围中最后发布的版本。

                e.g. npm install npm-devil@">=0.0.1 <0.0.5"

          3. 参数

            1. --force-f:强制重新安装。

              当目录中已经存在指定模块,默认:不会重新安装已经安装的模块。或删除node_modules目录再重新安装。

            2. --save-S:安装信息保存到package.jsondependencies(执行时依赖插件)。
            3. --save-dev-D:安装信息保存到package.jsondevDependencies(开发时依赖插件)。
            4. --save-optional-O:安装信息保存到package.jsonoptionalDependencies
        2. 作用域

          1. 本地:在本地被require引入后使用。
          2. 全局:在命令行中使用,或被全局命令引用。
      2. 升级

        npm update [-g] [<pkg>...]

        升级成功会把升级版本号自动写入package.json

        只更新顶层模块,而不更新依赖的依赖。可以使用npm --depth 9999 update更新依赖的依赖。

        • 升级npm自己

          1. macOS:npm install -g npm
          2. Windows:

            来自:npm-windows-upgrade

            1. 以管理员身份运行PowerShell(Windows 7 SP1以上的系统自带)
            2. 在PowerShell内执行

               Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force
               npm install -g npm-windows-upgrade
               npm-windows-upgrade
              
      3. 卸载

        npm uninstall [<@scope>/]<pkg>[@<version>]... [--save-prod|--save-dev|--save-optional] [--no-save]

      4. 重装npm

        curl -L https://www.npmjs.org/install.sh | sh

        若还是无法使用npm,建议重装Node.js。

      5. 验证缓存(垃圾收集不需要的数据、验证缓存的完整性)

        npm cache verify

        • 除非回收磁盘空间,否则不要使用以下清空npm缓存

          npm cache clean -f

      • package-lock.json

        更新成功(npm update)或第一次安装(npm install)时生成,用以记录当前状态下实际安装的各个npm包的具体来源和版本号。在存在此文件的根目录进行npm install,会按照完全相同的依赖关系进行安装。

        1. 若使用lock机制,则应该将package-lock.json提交到版本控制。
        2. 若不使用lock机制,则应该把package-lock=false加入.npmrc,并把.npmrc提交到版本控制。
        1. version:版本号
        2. resolved:软件包位置
        3. integrity:校验码
    4. 执行脚本

      1. npm run 「package.json中scripts字段的命令」 -- 「添加脚本后面的参数」

        -开头的参数可以忽略--而传递。e.g. npm run gulp runCss等价于:npm run gulp -- runCss

        执行的命令,优先找本地node_modules/.bin的命令,然后才找$PATH的命令(暂时未找到绕过先找本地的方法)。

      2. npx

        1. node_modules/.bin路径检查命令是否存在,找到之后执行;
        2. 找不到,就去环境变量$PATH里,检查命令是否存在,找到之后执行;
        3. 还是找不到,自动下载一个临时的依赖包最新版本在一个临时目录,然后再运行命令,运行完之后删除,不污染全局环境。
  2. package.json字段

    包描述、说明文件。

    1. name

      仓库名。

      组成:小写、无空格、字母数字下划线中划线。

    2. version

      版本号x.x.x

      npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]更新version

    3. dependencies

      生产环境依赖。

      node_modules 困境

      1. 安装依赖(npm install)是从(项目根目录)最外层往(引用处)最里层安装,若本层已经有同名但不同版本的库文件夹,则往里层尝试安装;
      2. 引用依赖是从(引用处)最里层往(项目根目录)最外层引用,一旦找到库目录就检索完毕。
      e.g. 1. 项目依赖逻辑: ```text . └── package.json .dependencies ├── A.v1 | .dependencies | ├── B.v1 | ├── C.v1 | └── D.v1 └── B.v2 .dependencies ├── A.v2 └── C.v2 .dependencies ├── A.v3 | .dependencies | └── D.v1 └── D.v2 ``` 2. `npm install`后的文件结构: ```text . ├── node_modules/ | ├── A.v1/ | | └── node_modules/ | | └── B.v1/ | ├── B.v2/ | | └── node_modules/ | | ├── A.v2/ | | ├── C.v2/ | | | └── node_modules/ | | | └── A.v3/ | | | └── node_modules/ | | | └── D.v1/ # 注意,D.v1又安装了一遍 | | └── D.v2/ # 注意,是C.v2引用D.v2,但安装在这里 | ├── C.v1/ | └── D.v1/ | └── package.json ```
    4. devDependencies

      开发、测试依赖。

      • 何时不被安装:

        1. 项目不会安装依赖库的devDependencies
        2. NODE_ENV值为production时,项目不会安装自己的devDependencies

          export NODE_ENV=production;

        3. npm install --production不会安装自己的devDependencies
    5. main

      代码入口,默认:index.js

      引用仓库的代码入口,不在引用链路内的仓库文件最终不会被使用到。

      • 其他非官方的代码入口(若不存在则退回main):

        1. module:ES6 Module
        2. unpkg<script>引用(针对unpkg.com
        3. jsdelivr<script>引用(针对www.jsdelivr.com
    6. scripts

      可执行脚本,用npm run 脚本名执行。

      • pre脚本名会在执行脚本名之前自动执行。
    7. files

      将仓库作为依赖项安装时要包含的路径、文件的数组。

    8. bin

      { 新增的命令: 对应的可执行文件路径 }

    9. engines

      该仓库在哪个版本的Node.js(、npm、yarn、等)上运行。

    10. browserslist

      支持哪些浏览器(及其版本)。

    11. description

      描述,也作为在npm官网被搜索的内容。

    12. repository

      仓库远程版本控制,可以是github等。

      e.g. ```json "repository": { "type": "git", "url": "git@github.com:用户名/仓库名.git" } // 针对Monorepo: "repository": { "type": "git", "url": "https://github.com/用户名/仓库名.git", "directory": "packages/文件夹名" } ```
    13. keywords

      在npm官网被搜索的关键字。

    14. author

      仓库作者。

    15. contributors

      仓库贡献者。

    16. license

      证书。

    17. homepage

      主页。

    18. bugs

      链接到软件包的问题跟踪器,最常用的是GitHub的issues页面。

    19. private

      设置为true,则无法npm publish,用于避免不小心公开项目。

    20. 命令特有的属性

      某些命令特有的(如:ESLint的eslintConfig、Babel的babel、等),可以在相应的命令/项目文档中找到如何使用它们。

    21. 其他

  3. 包的制作-使用

    1. 制作:

      按照CommonJS规范编写代码。

    2. 使用:

      1. 在Node.js环境下使用(require

        如:vue-cli

      2. 在浏览器环境下使用

        1. 用打包工具(webpackbrowserifyrollup)打包成能够在浏览器运行的JS代码。
        2. 直接制作可兼容在浏览器环境运行的代码。

        如:Vue.js。 - 作用域的包

      @scope/project-name

  4. .npmrc

    npm的配置文件,或全局修改:npm config set xxx=yyynpm config get xxx查看.npmrc->全局;npm config get xxx -g查看全局)。

    npm config ls -l查看所有已有配置和默认配置。

    • 常用:package-lockregistry
    • 注释:;#

项目中使用某个开源库时,要考虑它的License和文件大小(若使用webpack打包,则可以使用webpack-bundle-analyzer进行分析)。

一个项目中同时安装、使用同个依赖包的不同版本

很容易导致ts类型的冲突,需要额外处理全局类型问题。

  1. npm aliases(npm:6.9.0+)

    pnpm aliases也类似。

    1. 安装带别名的指定仓库:

      npm install 「别名」@npm:「[<@scope>/]<pkg>[@<version>]」 --save

    2. 会写入package.json的依赖:

       "dependencies": {
         "「别名」": "npm:「[<@scope>/]<pkg>[@<version>]」"
       }
      
    3. 使用时,根据引用名进入指定的版本中

  2. 发布一个新依赖包,包含引用指定版本的目标依赖包,然后通过 新依赖包+相对路径 引用

    安装依赖(npm install)是从(项目根目录)最外层往(引用处)最里层安装,若本层已经有同名但不同版本的库文件夹,则往里层尝试安装。

    "「新依赖包」/node_modules/「目标依赖包」"(若没有不同版本的同个依赖包,则路径错误)

  3. (仅适合全新项目)Monorepo

    各子项目中的依赖互相隔离。


CommonJS规范

参考:阮一峰:require() 源码解读CommonJS 详细介绍阮一峰:JavaScript 模块的循环加载

一个模块就是一个Node.js文件。

主模块:通过命令行参数传递给Node.js以启动程序的模块,负责调度组成整个程序的其它模块完成工作。如:node 文件名package.jsonmain


原理机制

Node.js的运行机制

  1. V8引擎解析应用程序输入的JS脚本。

    V8处理JS,除了 正则表达式JSON的处理 之外,对于大部分操作都很快。

    1. 使用正则表达式时注意ReDoS(Regular expression Denial of Service,正则表达式拒绝服务攻击)风险。
    2. JSON.parse/stringify随着输入参数线性增加执行时间。
  2. 解析后的代码,能够与操作系统进行交互。

    代码 -> Node.js内置模块 -> V8(internalBinding) -> C/C++ -> 操作系统

  3. libuv负责Node.js的API的执行。将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  4. V8引擎再将结果返回给应用程序。

JS本身的throw-try-catch异常处理机制并不会导致内存泄漏,也不会让程序的执行结果出乎意料,但Node.js并不是存粹的JS。Node.js里大量的API内部是由C/C++实现,因此Node.js程序的运行过程中,代码执行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏等问题。

Node.js的事件循环图

特性

  1. 单线程

    尽量不用同步方法,尽量不阻塞进程 高开销的同步方法: 1. `crypto`加密 1. `crypto.randomBytes`(同步版本) 2. `crypto.randomFillSync` 3. `crypto.pbkdf2Sync` - 同时你应当非常小心对加密和解密给予大数据输入的情况。 2. `zlib`压缩 1. `zlib.inflateSync` 2. `zlib.deflateSync` 3. `fs`文件系统 4. `child_process`子进程 1. `child_process.spawnSync` 2. `child_process.execSync` 3. `child_process.execFileSync`

    不为单个客户端连接创建一个新的线程,而仅仅使用单一线程支持所有客户端连接。通过非阻塞I/O和事件驱动机制,让Node.js程序宏观上并行。

    1. 优点:没有线程创建、销毁的时间开销;不需维护多个线程耗费的内存,可同时处理更多的客户端连接;运行中的单线程CPU利用率饱和。
    2. 缺点:若单一客户端连接造成线程的阻塞或奔溃,则影响所有客户端连接。

    事件循环不同于许多其他语言的模型,其它语言创建额外线程来处理并发工作:Java、PHP或.NET等服务器语言,会为每一个客户端连接创建一个新的线程或使用协程。

  2. 非阻塞I/O

    回调函数异步执行,通过事件循环检查已完成的I/O进行依次处理。

    1. 阻塞I/O、非阻塞I/O的区别:系统接收输入再到输出期间,能不能接收其他输入。
    2. I/O主要指由libuv支持的,与系统磁盘和网络之间的交互。
    3. 大多数Node.js核心API所提供的异步方法都遵从惯例:错误信息优先的回调模式(Error-first Callback,第一个参数是错误信息,若不报错则其值为null)。

      EventEmitter类事件函数不属于此范畴。

  3. 事件驱动

    用事件驱动(事件循环)来完成服务器的任务调度。

  1. Node.js开发应用程序:不善于计算,善于I/O(任务调度)。如:长连接的实时交互应用程序。
  2. Node.js服务器:没有根目录概念,没有web容器。URL通过顶层路由设计,呈递静态文件。
  3. 事件循环逻辑和触发JS时机都是由系统(C/C++)控制,在需要时才开启某个具体js调用栈。

只有打通和后端技术的桥梁、实现互联互通,Node.js才能在公司业务中有更长远的发展。

Node.js核心模块(需要require引入)

核心模块/内置模块 定义在源代码的lib/文件。同名加载时,核心模块优先级高于路径加载或自定义模块。

  1. http:HTTP请求相关API

    1. 接口永远不会缓冲整个请求或响应,所以用户可以流式地传输数据。
    2. 为了支持所有可能的HTTP应用程序,Node.js的HTTP API都是非常底层的。

      它仅进行流处理和消息解析。它将消息解析为消息头和消息主体,但不会解析具体的消息头或消息主体。

    e.g. 1. Node.js原生处理POST/GET请求 ```javascript const http = require("http"); http .createServer((req, res) => { let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { // 接受完成 const text = `收到${req.method}请求\nurl是:${req.url}${req.method === "POST" ? `\nbody是: ${body}` : ""}\n`; console.log(text); res.end(text); }); }) .listen(3000, "0.0.0.0"); ``` 2. Node.js原生发起POST请求 ```javascript const http = require("http"); const req = http.request( "路径", { method: "POST", headers: { "Content-Type": "application/json; charset=UTF-8", }, }, (res) => { res.setEncoding("utf8"); let body = ""; res.on("data", (chunk) => { body += chunk || ""; }); res.on("end", () => { // 响应接受完成 console.log(body); // body是GET/POST的响应内容 }); } ); req.on("error", (e) => { console.warn(e); }); req.write(JSON.stringify({ a: "发起的请求body" })); // 保证:发起请求的body和请求头匹配 req.end(); ``` 3. Node.js原生发起GET请求 ```javascript const http = require("http"); const req = http.request( "路径", { method: "GET", }, (res) => { res.setEncoding("utf8"); let body = ""; res.on("data", (chunk) => { body += chunk || ""; }); res.on("end", () => { // 响应接受完成 console.log(body); // body是GET/POST的响应内容 }); } ); req.on("error", (e) => { console.warn(e); }); req.end(); ```
  2. http2
  3. https
  4. fs:以标准POSIX函数为模型的方式与文件系统进行交互

    提供版本:异步、同步(+Sync)、基于Promise(require("fs").promises)。其他模块也类似。

    e.g. ```javascript // 异步(结果在回调;若出错,则回调函数第一个参数不为null或undefined) require('fs').rename('before.json', 'after.json', err => { if (err) { return console.error(err) } //完成 }) // 同步(阻塞线程,直到文件操作结束。结果在执行语句返回。需要try-catch等方式处理错误) try { require('fs').renameSync('before.json', 'after.json') //完成 } catch (err) { console.error(err) } // Promise require("fs").promises .rename("before.json", "after.json") .then((data) => { console.log('succeeded', data); }) .catch((error) => { console.error('failed' ,error); }); ```
    • 尽量选择使用读写文件的内容

      (不使用流的)读操作都会在返回数据之前将文件的全部内容读取到内存中,这意味着大文件会对内存的消耗和程序执行的速度产生重大的影响;(不使用流的)写操作都是在将全部内容写入文件之后才会将控制权返回给程序(在异步的版本中,这意味着执行回调)。

      • 导出文件需求:

        1. 后端返回json数据(无法利用流技术,只能完整保存在服务端内存中),前端根据数据生成文件并创建下载
        2. 后端直接返回文件,前端请求下载文件(get请求)

          1. 利用流技术,创建并保存文件在服务端;
          2. 利用流技术,把刚刚保存的文件传递给前端进行下载。
  5. events:事件触发器

    EventEmitter类:require('events')

    1. 所有能触发事件的对象都是EventEmitter类的实例。
    2. EventEmitter以注册的顺序同步地调用所有监听器。
  6. path:处理文件路径

    e.g. 当前文件所在目录的相对位置:require('path').resolve(__dirname, '../../xx/xxx.txt')

  7. stream:数据流

    流是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。

    e.g. 在传统的方式中,当告诉程序读取文件或通信时,这会将文件或信息从头到尾读入内存,然后进行处理。若使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中),能够一边读取一边处理更加高效。

    1. 所有的流都是EventEmitter类的实例。
    2. Node.js提供了多种流对象(fshttpprocess等都有流操作方式),除非要创建新类型的流实例,否则极少需要直接使用stream
    3. 流类型:可读(Readable)、可写(Writable)、可读可写(Duplex)、可修改或转换数据(Transform)。

    stream不需要手动创建缓冲区(buffer),在Node.js的流中将会自动创建。

  8. readline:用于一次一行地读取可读流中的数据

Buffer是数据以二进制形式临时存放在内存中的物理映射,stream为搬运数据的传送带和加工器,有方向、状态、缓冲大小。

  1. buffer:缓冲区

    Buffer用于表示固定长度的字节序列。是JSUint8Array的子类,并使用涵盖额外用例的方法对其进行扩展。

    由于Buffer需要处理的是大量的二进制数据,假如用一点就向系统去申请,则会造成频繁的向系统申请内存调用,所以Buffer所占用的内存不再由V8分配,而是在Node.js的C++层面完成申请,在JavaScript中进行内存分配。 因此,这部分内存我们称之为堆外内存。

  2. string_decoder:字符串解码器

    提供了用于将Buffer对象解码为字符串(以保留编码的多字节UTF-8和UTF-16字符的方式)的API。

  3. os:提供了与操作系统相关的实用方法和属性
  4. util:提供常用函数的集合,用于弥补核心JavaScript的功能过于精简的不足
  5. process:进程

    require("node:process") === process

  6. child_process:衍生子进程

    衍生的Node.js子进程独立于父进程,但两者之间建立的IPC通信通道除外。每个进程都有自己的内存,带有自己的V8实例。

    由于需要额外的资源分配,因此不建议衍生大量的Node.js子进程。

  7. cluster:集群(运行多个Node.js实例)

    创建共享服务器端口的子进程。为了充分利用多核系统,有时需要启用一组Node.js进程去处理负载任务。

  8. worker_threads:工作线程(单个Node.js实例中并行运行多个应用程序线程)
  9. crypto:加密

    OpenSSL的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。

  10. zlib:提供Gzip、Deflate/Inflate、Brotli的压缩功能

    压缩和解压缩是围绕stream流API构建的。

  11. dgram:数据报

    对UDP datagram sockets(UDP的数据报套接字)的一层封装。

  12. net:用于创建基于流的TCP或IPC的服务器(net.createServer)与客户端(net.createConnection
  13. dns:解析域名
  14. url:解析URL。
  15. querystring:解析和格式化URL查询字符串
  16. tls:实现安全传输层(TLS)及安全套接层(SSL)协议,建立在OpenSSL的基础上
  17. module:模块

    提供几个对象属性。

  18. timers

    Node.js中的定时器函数实现了与网络浏览器提供的定时器API类似的API,但使用的是围绕Node.js事件循环构建的不同的内部实现。

    1. 定时器函数是全局的,所以不需要调用 require('node:timers') 来使用该API。
    2. 提供了一组可返回Promise对象的可供选择的定时器函数:require('node:timers/promises')
  19. perf_hooks:性能钩子

    实现w3c的Performance API的子类。

  20. assert:断言
  21. repl:REPL(交互式解释器)

    • 终端使用Node.js的REPL:

      1. _

        打印最后一次操作结果(不执行语句、没有副作用)。

      2. .help.editor.break.clear.load.save.exit
      3. 可以直接使用系统模块,不需要require引入。

        e.g. REPL中http === require('http')

  22. v8:V8的api
  23. vm:提供V8虚拟机上下文中进行编译和运行代码
  24. console控制台

    require("node:console").Console === console.Console

  25. inspector:与V8调试器交互
  26. tty:终端

    在大多数情况下,没有必要或不可能直接使用此模块。

  27. C++相关

    1. C++ addons
    2. C/C++ addons with Node-API
    3. C++ embedder API

Node.js全局变量

Node.js的全局对象global是所有全局变量的宿主。

  1. 仅在模块内有效

    1. require
    2. exports
    3. module
    4. __filename:当前正在执行的脚本所在文件夹的绝对路径+文件名。
    5. __dirname:当前正在执行的脚本所在文件夹的绝对路径。
  2. process:描述当前Node.js进程状态的对象,提供了一个与操作系统的简单接口。

    1. process.cwd():运行node命令时所在文件夹的绝对路径。
  3. Buffer:二进制数据流。

    虽然Buffer在全局作用域内可用,但仍然建议通过import/require语句显式地引用它:const { Buffer } = require('node:buffer')

    可以将buffer视为整数数组:数组的每一项都是整数,并代表一个数据字节。

  4. setImmediate/clearImmediate
  5. 类似于浏览器的全局对象window所包含的全局变量

    1. setTimeout/clearTimeoutsetInterval/clearInterval
    2. console
    3. URLURLSearchParams
    4. Error

      若在程序执行过程中引发了未捕获的异常未捕获的失败Promise实例,则程序将崩溃。捕获未捕获的异常:

      1. 捕获未捕获的异常

        process.on('uncaughtException', err => {
          // 执行逻辑
        })
        
      2. 捕获未捕获的失败Promise实例

        process.on('unhandledRejection', err => {
          // 执行逻辑
        })
        
    5. debugger

      1. node inspect 脚本.js:命令行调试
      2. node --inspect 脚本.js:与Chrome配合调试(默认:--inspect=127.0.0.1:9229。开启远程调试:--inspect=「公共IP或0.0.0.0」:9229

        有时因为运行的代码,就算退出了程序也无法关闭占用inspect的9229(默认)端口。需要手动杀死占用端口的进程,e.g. lsof -i :9229然后kill -9 「PID」

        • --debug-brk:直接从第一行代码开始进行断点调试。
    6. Blob
    7. EventEventTarget
    8. TextDecoderTextEncoder
    9. WebAssembly
    10. AbortControllerAbortSignal
    11. BroadcastChannel
    12. queueMicrotask

      将微任务放入队列以便调用回调。

    13. MessageChannelMessageEventMessagePort
    14. performance
    15. structuredClone
    16. DOMException

Tips

  1. 调试方法:

    1. 控制台输出console等。
    2. 通过Chrome的 <chrome://inspect/#devices>,监听Node.js程序运行node --inspect 文件,可以使用debugger等进行断点调试。

      调试指南

  2. 服务端开发注意点:

    1. 相对于客户端,服务端要处理大量并发的请求。

      需要学习服务端的各种 高并发、数据库IO 解决方案。前端处理客户端问题无法接触到这些,需要重新踩服务端的坑。

      虽然Node.js是单线程,但是各种异步的操作(如:数据库IO等)需要按照服务端的技术解决方案处理。

    2. 基本数据库的使用,以及如何在Node.js中使用。

      如:MySQL、Redis、MongoDB、等。

    3. 异常处理、错误报警

      对各种IO要进行异常处理(如:try-catch包裹所有IO代码),并需要把错误上报(打日志console或借助第三方监控告警)。

  3. 与浏览器JS的区别

    除了全局变量、提供的模块、模块系统、API不同之外,在Node.js中,可以控制运行环境:除非构建的是任何人都可以在任何地方部署的开源应用程序,否则开发者知道会在哪个版本的Node.js上运行该应用程序。与浏览器环境(无法选择访客会使用的浏览器)相比起来,这非常方便。

  4. Node.js运行环境退出(命令行执行完毕后自动退出):

    代码运行完毕。包括:执行队列、任务队列、等待加入任务队列的其他线程任务,全都执行完毕,当不会有新的指令需要执行时,就自动退出Node.js的进程。e.g. 监听系统端口 或 setTimeout还未触发,意味着还有事件需要待执行。

  5. 不管任何情况,始终保证要有回包,就算代码运行错误,也要兜底回包(.end()
  6. CLI命令行man node

     node [options] [V8 options] [<program-entry-point> | -e "script" | -] [--] [arguments]
    
     node inspect [<program-entry-point> | -e "script" | <host>:<port>] …
    
     node --v8-options
    
    1. 不带参数执行以启动交互式解释器。
    2. -:标准输入的别名。 类似于在其他命令行工具中使用 -,这意味着脚本是从标准输入读取的,其余的选项将传给该脚本。
    3. --:指示 node 选项的结束。 将其余参数传给脚本。 如果在此之前没有提供脚本文件名或评估/打印脚本,则下一个参数用作脚本文件名。

    node -e "「js代码文本」":eval执行字符串;node -p "「js代码文本」":eval执行字符串(-e)并打印。

  7. 返回的内容的属性值为undefined的,可能会把这个属性去除。
  8. 抓包Node.js发起的http/https请求

    1. 本机全局代理到抓包软件、或用Proxifier等软件转发到抓包软件
    2. 或 代码设置Node.js发起请求时通过代理转发

      e.g. 1. Node.js的`host`和`port`参数 ```javascript const http = require("http"); const req = http.request( { port: "8899", // 代理端口 host: "127.0.0.1", // 代理地址 path: "http://127.0.0.1:8080/", // 访问地址 method: "POST", headers: { "Content-Type": "application/json; charset=UTF-8", }, }, (res) => { res.setEncoding("utf8"); let body = ""; res.on("data", (chunk) => { body += chunk || ""; }); res.on("end", () => { console.log(body); // body是POST请求后返回的响应body // 返回接受完成 }); } ); req.on("error", (e) => { console.warn(e); }); req.write(JSON.stringify({ a: "发起的请求body" })); // 保证:发起请求的body和请求头匹配 req.end(); ``` 2. [axios](https://github.com/axios/axios)的`proxy`参数 3. [request](https://github.com/request/request)的`proxy`参数

工具使用

Koa

关键点:级联 + 通过上下文(ctx)在中间件间传递数据 + ctx.body的值为HTTP响应数据。

  1. 级联(Cascading):中间件按顺序执行,随着第二个参数next执行进入执行栈,所有中间件运行完毕后自动返回响应

    为了能够更好的链式调用中间件,要使用await next()return next()的方式,否则虽然会next进入下一个中间件,但下一个中间件的异步代码会导致请求先返回之后再处理异步后代码。

     const Koa = require('koa')
     const app = new Koa()
    
     // 按照①②③④⑤⑥执行后输出
    
     app.use(async (ctx, next) => {
       // ①
       await next()
       // ⑥
     })
    
     app.use((ctx, next) => {
       // ②
       return next().then(() => {
         // ⑤
       })
     })
    
     app.use((ctx, next) => {
       // ③
       return next()
     })
    
     app.use(async ctx => {
       // ④
     })
    
     app.use(async ctx => {
       // 前一个中间件没有执行`next`,因此后面的中间件不再被执行
     })
    
     app.listen(3000)
    

    中间件/拦截器的流程:从上到下,执行中间件,直到抵达路由匹配到的中间件为止,不再继续向下执行(若所有都不匹配,则执行兜底中间件)

  2. const app = new Koa()实例

    1. app.context

      上下文,可以添加新值

    2. app.env

      获取NODE_ENV的值(默认:'development'

    3. app.use(async (上下文, next) => {}))

      使用中间件

    4. app.callback()

      返回一个函数,用于http.createServer()的第一个参数

    5. app.listen(数字)

      创建并返回HTTP服务器

      • 语法糖 等价于: ```javascript const http = require('http'); const Koa = require('koa'); const app = new Koa(); http.createServer(app.callback()).listen(数字); ```
    6. app.on('error', (err, ctx) => {})

      koa的错误处理

      错误处理,中间件产生的 未捕获的 同步错误(或同步ctx.throw)都会捕获到这里

      未捕获的异步错误用process.on('uncaughtException', err => {})捕获;未捕获的失败Promise实例用process.on('unhandledRejection', err => {})捕获。

    7. app.keys =

      设置签名的 Cookie 密钥

    • 其他

      1. app.maxIpsCount

        从代理 ip 消息头读取的最大ip数(默认:0,表示无限)

      2. app.middleware

        所有用到的中间件的引用(数组)。

      3. app.proxy =

        若设置为true,则header fields将被信任

      4. app.proxyIpHeader

        代理 ip 消息头(默认:'X-Forwarded-For'

      5. app.subdomainOffset

        .subdomains忽略的偏移量(默认:2

      6. app.emit(事件名[, ...args])

        发起一个EventEmitter事件。

      7. app.request
      8. app.response
  3. 上下文(context)

    app.context、中间件的第一个参数。

    每一个请求都将创建一个新的上下文(来自app.context),并在中间件间引用传递和新赋值。

    • 对上下文的写入和读取

      1. 写入:

        1. 统一写入app.context.新属性 = 值
        2. 中间件写入

           app.use((ctx) => {
             ctx.新属性 = 
           })
          
      2. 读取:

         app.use((ctx) => {
           console.log(ctx.新属性)
         })
        
    • HTTP响应的内容:所有中间件执行结束之后的ctx.body

    1. .request

      Koa的Request

      1. .header === .request.header === .headers === .request.headers
      2. .header= === .request.header= === .headers= === .request.headers=
      3. .method === .request.method
      4. .method= === .request.method=
      5. .request.length
      6. .url === .request.url
      7. .url= === .request.url=
      8. .originalUrl === .request.originalUrl
      9. .origin === .request.origin
      10. .href === .request.href
      11. .path === .request.path
      12. .path= === .request.path=
      13. .query === .request.query
      14. .query= === .request.query=
      15. .querystring === .request.querystring
      16. .querystring= === .request.querystring=
      17. .request.search
      18. .request.search=
      19. .host === .request.host
      20. .hostname === .request.hostname
      21. .request.URL
      22. .request.type
      23. .request.charset
      24. .fresh === .request.fresh
      25. .stale === .request.stale
      26. .socket === .request.socket
      27. .protocol === .request.protocol
      28. .secure === .request.secure
      29. .ip === .request.ip
      30. .ips === .request.ips
      31. .subdomains === .request.subdomains
      32. .is() === .request.is()
      33. .accepts() === .request.accepts()
      34. .acceptsEncodings() === .request.acceptsEncodings()
      35. .acceptsCharsets() === .request.acceptsCharsets()
      36. .acceptsLanguages() === .request.acceptsLanguages()
      37. .request.idempotent
      38. .get() === .request.get()
    2. .response

      Koa的Response

      1. .response.header === .response.headers
      2. .body === .response.body
      3. .body= === .response.body=
      4. .response.socket
      5. .status === .response.status
      6. .status= === .response.status=
      7. .message === .response.message
      8. .message= === .response.message=
      9. .length === .response.length
      10. .length= === .response.length=
      11. .type === .response.type
      12. .type= === .response.type=
      13. .headerSent === .response.headerSent
      14. .redirect() === .response.redirect()
      15. .attachment() === .response.attachment()
      16. .response.get()
      17. .set() === .response.set()
      18. .append() === .response.append()
      19. .remove() === .response.remove()
      20. .lastModified.response.lastModified
      21. .lastModified= === .response.lastModified=
      22. .etag.response.etag
      23. .etag= === .response.etag=
      24. .response.is()
      25. .response.vary()
      26. .response.flushHeaders()
    3. .state

      推荐的命名空间,用于通过中间件传递信息。

       // 前面的中间件设置
       ctx.state.属性1 = 
      
       // 后面的中间获得
       ctx.state.属性1
      
    4. .app

      应用程序实例引用(const app = new Koa())。

    5. .cookies

      使用cookies模块。

      1. .cookies.get(名[, options])
      2. .cookies.set(名[, 值 [, options]])
    6. .throw([状态[, 信息[, properties]]])

      抛出错误(可以被try-catch捕获处理)

    7. .assert(值[, status[, 信息[, properties]]])

      false时抛出一个类似.throw的错误。与Node.js的assert()方法类似.

      e.g. ctx.assert(ctx.state.user, 401, 'User not found. Please login!');

    8. .req

      Node.js的Request

    9. .res

      Node.js的Response

      • 绕过Koa的Response处理是不被支持的. 应避免使用以下Node.js属性:

        1. res.statusCode
        2. res.writeHead()
        3. res.write()
        4. res.end()
    10. .respond

      是否绕过Koa的Response

  4. 调试模式

    运行前添加环境变量:DEBUG=koa*

express

  1. 通过回调实现异步函数,在多个回调、多个中间件中写起来容易逻辑混乱。
  2. 回调函数的参数数量:

    1. 2个参数一般认为是路由(中间件)
    2. 3个参数一般认为是通用中间件
    3. 4个参数是错误处理中间件

      错误处理中间件,需要回调函数固定传4个参数(Error-first)。

    无论是哪种类型的中间件,都是按代码顺序依次执行。

pm2

后台运行、进程管理(自动重启、永保活动状态、不停机重新加载,显示进程信息,配置进程处理条件)、多进程运行、负载均衡、处理log输出。

  1. pm2CI

    pm2 +

    1. startstoprestartstartOrRestartdeletereload + 执行文件、进程名/进程id/all、配置脚本(.js、.json、.yml)

      1. start 执行文件或配置脚本

        1. -- 「参数,已空格分隔」

          传递进执行脚本。只能用在命令行最后,--之后的所有内容都将当做是参数。

        2. --node-args="「参数」"

          相同:--interpreter-args="「参数」"

          传递给Node.js的node运行命令(不是传递进执行脚本),如:--inspect

        3. --log 「out、error脚本路径」

          1. --output 「out脚本路径」
          2. --error 「error脚本路径」
        4. -i 「进程数:max、-1、数字」

          cluster模式。

          1. --merge-logs

            当多进程使用同一个进程名时,不用进程id区分log文件(同名的多进程写到同一个log文件)。

        5. --watch

          监听改动文件后重新执行指令(先kill再重启)

          1. --ignore-watch="「文件或文件夹」"
        6. --interpreter=bash/python/ruby/coffee/php/perl/node

          需要配置exec_mode: forkexec_interpreter: 对应语言

        7. --max-memory-restart 「数字」K/M/G

          达到最大内存就自动重启应用。

        8. --restart-delay 「数字」ms
        9. --time

          每行log前缀添加时间戳

        10. --env 「环境名」

          使用配置脚本中的env_「环境名」的环境变量配置。不传则默认使用配置脚本中env的环境变量配置。

          e.g. ```javascript // 默认使用 "env": { "DEBUG": "this" }, // 传`--env xx`时使用 "env_xx": { "DEBUG": "that" } ```
        11. --cron 「cron格式」

          cron形式的自动重启配置

        12. --no-daemon

          不使用pm2自己的守护进程运行。

      2. stop all或进程名或进程id或执行文件或配置脚本
      3. restart all或进程名或进程id或执行文件或配置脚本
      4. startOrRestart all或进程名或进程id或执行文件或配置脚本
      5. delete all或进程名或进程id或执行文件或配置脚本
      6. reload all或进程名或进程id或配置脚本

        如果是在cluster mode,reload会依序升级重启每一个程序,达到zero downtime升级

      7. start/stop/restart/startOrRestart/delete/reload 配置脚本 --only 「进程名」

        仅对某一个进程进行操作。

      若使用配置脚本,则命令行参数大部分会被忽略(除了部分参数,如:--env--only、等)。

    2. ecosystem/init

      初始化ecosystem.config.js文件。

    3. list/ls/l/status
    4. monit
    5. logs

      1. 「进程名」
      2. --json
      3. --format
    6. reloadLogs

      重载所有logs。

    7. flush

      清空所有logs。

    8. describe 「进程id或进程名」
    9. show 「进程id或进程名」
    10. ping

      判断pm2守护进程正在运行。

    11. kill

      杀掉pm2自己,包括所有:pm2程序、pm2运行的进程、等,然后重启pm2。

      pm2卡死的时候使用。

    12. 系统重启或pm2自己的重启

      重启信息记录在:$HOME/.pm2/dump.pm2

      1. startup
      2. save
      3. unstartup
      4. resurrect
    13. serve 「文件路径,默认:./」 「端口号,默认:8080」

      静态服务。

    14. install 「模块名」

      1. pm2 install typescript -> pm2 start app.ts --watch
    15. update

      更新在内存中运行的pm2。

      先在全局更新npm install pm2@latest -g,然后再更新在内存中运行的pm2。

    16. deploy

      部署、发布。