JavaScript 笔记(十二):Node

JavaScript 笔记(十二):Node

Node.js 核心基础

Node.js 简介

Node.js 是一款基于 Chrome V8 引擎 的 JavaScript 运行环境,V8 引擎是一个专门用于解释和执行 JavaScript 代码的虚拟机,程序如果集成了 V8 引擎,便可以执行 JavaScript 代码

Node.js 不是编程语言,是一个运行环境,由于此运行环境集成了 V8 引擎,所以可以在此环境下运行 JavaScript 代码

搭建 Node.js 运行环境

  • http://nodejs.cn/
    • msi
    • zip -> 环境变量
  • NVM

在命令行中执行命令 node -v 查看版本号

在 Node.js 中执行 JS

  • 命令行
    • 在命令行中执行命令 node 打开 REPL(Read Eval Print Loop) 环境
  • .js 文件
    • 在命令行中执行命令 node [.jspath]

浏览器与 Node.js 的区别

  • 内置对象
    • 浏览器环境提供了 window 全局对象
    • Node.js 环境提供的全局对象不是 window,而是 global
  • this 默认值
    • 浏览器环境中全局 this 默认指向 window
    • Node.js 环境中全局 this 默认指向空对象 {}
  • API
    • 浏览器环境提供了 DOM 和 BOM 相关的 API
    • Node.js 环境中没有 DOM 和 BOM 的概念,也没有相关的 API

Node.js 的全局变量和方法

在 Node.js 提供了一些全局变量和方法,示例如下:

console.log(__dirname);         // E:\LearnNotes\nodejs\code
console.log(__filename);        // E:\LearnNotes\nodejs\code\global.js

console.log("Reyn Morales");    // Reyn Morales

setTimeout(() => {
    console.log("Hello World"); // Hello World
}, 5000);

在上述示例中,可以在 Node.js 中使用 console / setTimeout 全局方法,console 和 setTimeout 方法与浏览器环境中的方法相同,可以使用 __dirname / __filename 全局变量,__dirname 表示执行文件所在的绝对路径,__filename 以绝对路径的形式表示执行文件的名称

Node.js 模块

在学习框架时,我们可以发现绝大多数框架的源码总是以一个立即执行的函数作为核心代码的最外层结构,此形式即为以浏览器为宿主下的模块开发,以一个类或者一个立即执行的函数作为一个模块,示例如下:

;(function(window){
    let name = "Reyn Morales";
    // ...

    let say = () => console.log("Hello World");
    // ...

    /* 属性 */
    window.name = name;
    // ...

    /* 方法 */
    window.say = say;
    // ...
})(window);

由于以浏览器为宿主下的模块开发没有相应的标准和规范,所以存在某些问题,而在 Node.js 中采用了 CommonJS 规范实现了模块系统

CommonJS 规范规定了如何定义一个模块、如何导出模块中的内容、以及如何使用定义好的模块,内容如下:

  • 模块以文件为单位,有多少个文件就有多少个模块
  • 每个文件中的内容均是私有的,其它文件不可访问
  • 每个文件中的内容必须通过 exports 实例导出之后,其它文件才可以使用
  • 每个文件必须调用 require 函数导入模块,才能使用相应模块导出的内容

exports 示例如下:

/**
 * FILE NAME: exports.js
 */

let name = "Reyn Morales";

let say = () => console.log("Hello World");

exports.str = name;
exports.say = say;

require 示例如下:

/**
 * FILE NAME: require.js
 */

let exmp = require("./exports");

console.log(exmp.str);  // Reyn Morales

exmp.say(); // Hello World

Node.js 中除了可以使用 exports 实例导出模块内容之外,也可以使用 module.exports 实例导出模块,示例如下:

/**
 * FILE NAME: exports.js
 */

let name = "Reyn Morales";

let say = () => console.log("Hello World");

module.exports.str = name;
module.exports.say = say;

module.exports 和 exports 的区别在于前者在导出时可以不同指定属性或方法名称,示例如下:

/**
 * FILE NAME: exports.js
 */

let name = "Reyn Morales";

module.exports = name;

此时如果访问 require 导入的实例:

/**
 * FILE NAME: require.js
 */

let exmp = require("./exports");

console.log(exmp);  // Reyn Morales

此外,类似于浏览器的惯用做法,Node.js 可以将变量和函数绑定到 global 全局对象中,示例如下:

/**
 * FILE NAME: exports.js
 */

let name = "Reyn Morales";

let say = () => console.log("Hello World");

global.str = name;
global.say = say;

此时必须调用 require 函数才可以访问,示例如下:

/**
 * FILE NAME: require.js
 */

let exmp = require("./exports");

console.log(str);
say();

由于使用 global 违反了 CommonJS 规范,所以不建议使用

另外,通过 require 函数导入模块时,可以不说明模块类型(后缀名),此时系统将依次查找以 .js.json 以及 .node 为后缀的相应文件,不论是哪一种,导入后都将结果转换为一个实例返回,示例如下:

  • JSON
{
    "name": "Reyn Morales",
    "age": 22
}
  • JavaScript
/**
 * FILE NAME: print.js
 */

let person = require("./person");

console.log(person);    // { name: 'Reyn Morales', age: 22 }

此外,通过 require 不仅可以导入自定义模块(文件模块),还可以导入系统模块(核心模块)和第三方模块,在导入自定义模块时,必须加上路径,否则报错,如果导入系统模块,那么程序将在环境变量配置的路径中查找;如果导入第三方模块,那么程序将在 module.paths 数组所保存的路径中依次查找

Node.js 包管理工具

在模块开发过程中,为了便于维护、减少出错,通常在一个模块中只完成一个特定的功能,而在某些情况下,某个功能必须由多个模块组成,为了提高项目的可维护性,此时可以通过某种工具来管理、说明以及维护所有模块之间的关系,通常将所有被管理的模块称为包,包可以由一个或若干个模块构成,所以此工具即被称为包管理工具

npm

Node.js 为了方便开发者发布、安装和管理包,推出了 npm(Node Package Manager)包管理工具

npm 在配置 Node.js 环境时,由系统自动安装,所以可以立即使用

全局

通过 npm 可以将包存储到全局 node_modules 中,在计算机的任何位置都可以使用 node_modules 中的模块,命令如下:

命令说明
npm install -g {package}将 package 以全局模式安装,默认为最新版本
npm install -g {package}@{version}将指定 version 的 package 以全局模式安装
npm uninstall -g {package}将 package 以全局模式卸载
npm update -g {package}将 package 以全局模式更新到最新版本

通过命令 npm config ls -l 可以查看 npm 的配置信息,prefix 所描述的路径即为包以全局模式安装的路径前缀

示例如下:

  • 安装(默认版本)

npm install -g nrm

  • 卸载

npm uninstall -g nrm

  • 安装(特定版本)

npm install -g nrm@1.0.0

如果在 /E 目录下启动 cmd,并执行命令 nrm --version,结果为 1.0.0

  • 更新(最新版本)

npm update -g nrm

此时如果在任意路径下启动 cmd,并执行 nrm --version,结果为 1.2.5,在 nmp 官方 查询 nrm 包的最新版本为 1.2.5

默认情况下,npm 从国外服务器下载资源,速度较慢,nrm 可以切换资源下载地址,从而提高下载速度,首先,通过命令 nrm ls 查看允许切换的资源地址,之后通过命令 nrm use taobao 将资源下载地址切换到淘宝,结果如下:

Registry has been set to: https://registry.npmmirror.com/

此时即将资源地址切换为国内,另外,通过 cnpm 包管理工具安装模块时,也可以自动将资源地址切换为国内,通常情况下,将 cnpm 以全局模式安装,之后就可以使用 cnpm 安装包,使用方式和 npm 基本相同,区别在于将 npm 切换为 cnpm,示例如下:

cnpm install electron --save-dev

本地

通过 npm 也可以将包存储到本地项目的 node_modules 中,此时 node_modules 中的模块只能在本地项目中引用,在将包安装到本地之前,必须在本地项目目录下通过 npm init 或者 npm init -y 命令初始化 package.json 文件,内容如下:

{
  "name": "npm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

此文件中描述了项目的名称、版本、作者、许可证和所需的模块等基本信息,通过 npm install 命令安装包时,此命令根据 package.json 配置文件自动下载所需的模块,即配置项目的运行和开发环境

npm 的本地项目包管理命令如下所示:

命令说明
npm install {package}将 package 以本地模式安装到当前项目中,默认作为生产环境包的依赖
npm uninstall {package}将 package 以本地模式从当前项目中卸载
npm upgrade {package}将 package 以本地模式更新到最新版本
npm install {package} --save将 package 作为当前项目的生产环境包的依赖安装到本地项目中
npm install {package} --save-dev将 package 作为当前项目的开发环境包的依赖安装到本地项目中

生产环境包是指在本地项目中引用的用于实际实现某个功能的包,开发环境包是指在本地项目中引用的用于调式功能等辅助开发的工具包,此类包被卸载之后并不影响本地项目的使用

示例如下:

  • 安装(默认)

npm install jquery

  • 安装(生产环境包)

npm install swiper --save

  • 安装(开发环境包)

npm install fastclick --save-dev

此时若打开 package.json 文件,内容如下:

{
  "name": "npm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.6.1",
    "swiper": "^8.4.3"
  },
  "devDependencies": {
    "fastclick": "^1.0.6"
  }
}

package.json 文件中,dependencies 即为生产环境包,devDependencies 即为开发环境包,不论是哪一种包,保存的内容均为包名和版本号,版本号有 3 种形式,不同的形式在依赖被安装时所选择的版本不同,示例和说明如下:

示例说明
3.6.13.6.1 指定版本
^3.6.13.6.X 中的最新版本
~3.6.13.X.X 中的最新版本

此外,若将项目分享给别人时,并不需要拷贝本地项目的 node_modules 包文件,而是拷贝配置文件即可,被分享者获取到项目的配置文件之后,通过 npm i 命令即可自动配置项目的生产和开发环境,即自动安装项目依赖的生产环境包和开发环境包,此外,通过 npm i --production 命令将自动安装项目依赖的生产环境包,通过 npm i --development 命令将自动安装项目依赖的开发环境包

yarn

yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出的 JS 包管理工具,目的在于弥补 npm 5.0 版本之前的一些缺陷,不过,现在 npm 已经更新到 8.x 版本,由于 npm 随着升级的优化性能以及超越了 yarn,所以不再详细介绍,yarn 的使用和 npm 类似,区别在于命令的选项不同,例如 npm 中安装、卸载和更新的选项分别为 install、uninstall 和 update,在 yarn 中分别是 add、remove 和 upgrade,如果以全局模式管理,npm 通过 -g 选项说明,而 yarn 中通过 global 选项说明

Node.js 核心 API

Buffer

在 Node.js 中,Buffer 类专门用于以 2 进制形式存储字节数据

由于 Buffer 相关的内容已经绑定在了 global 上,所以不用导入即可使用

基本使用

Buffer 类通过 alloc 静态方法开辟指定大小的存储空间,并且可以初始化数据和字符编码,原型如下:

Buffer.alloc(size[, fill[, encoding]])

示例如下:

let buf = Buffer.alloc(5);
console.log(buf);   // <Buffer 00 00 00 00 00>

默认情况下以 0 填充数据、utf8 编码,此外,如果通过 log 打印 Buffer 实例,那么将以 16 进制的形式输出实例中的内容,示例如下:

let buf = Buffer.alloc(5, 15);
console.log(buf);   // <Buffer 0f 0f 0f 0f 0f>

此外,Buffer 类可以通过 from 静态方法根据参数自动创建实例,原型如下:

Buffer.from(string[, encoding])

示例如下:

let buf = Buffer.from("reyn");
console.log(buf);   // <Buffer 72 65 79 6e>

如果参数为字符串,那么将以二进制形式存储各个字符的 ASCII 码,此外,from 也可以传入一个数组,原型如下:

Buffer.from(array)

示例如下:

let buf = Buffer.from([1, 3, 5]);
console.log(buf);   // <Buffer 01 03 05>

实际上,Buffer 实例的本质是一个数组,示例如下:

let buf = Buffer.from([1, 3, 5]);
console.log(buf);   // <Buffer 01 03 05>

buf[0] = 7;
console.log(buf.length);    // 3
console.log(buf);   // <Buffer 07 03 05>
实例方法
  1. toString

将 Buffer 实例中存储的字节数据转换为字符串,示例如下:

let buf = Buffer.from("Reyn");
console.log(buf);   // <Buffer 52 65 79 6e>
console.log(buf.toString());    // Reyn
  1. write

向 Buffer 实例中写入数据,原型如下:

buf.write(string[, offset[, length]][, encoding])

offset 为字节的起始下标,length 为字节个数,示例如下:

let buf = Buffer.alloc(5);
buf.write("Morales", 2, 2);
console.log(buf);   // <Buffer 00 00 4d 6f 00>
console.log(byteNum);   // 2
  1. slice

从存在的 Buffer 实例中截取一个新的 Buffer,原型如下:

buf.slice([start[, end]])

start 为截取的起始下标(默认值为 0),end 为截取的结束下标(不包含)

示例如下:

let buf = Buffer.alloc(5);
buf.write("Morales");
console.log(buf.toString());    // Moral

let subBuf = buf.slice(2, 4);
console.log(subBuf.toString()); // ra

必须注意的是,如果被写入的字节个数大于 Buffer 实例长度,那么剩余的字节将被丢弃,此外,每个中文字符占用 3 个字节,示例如下:

let buf = Buffer.from("乔布斯");
console.log(buf.length);    // 9
静态方法
  1. isEncoding

原型如下:

Buffer.isEncoding(encoding)

示例如下:

console.log(Buffer.isEncoding("gbk"));  // false
  1. isBuffer

原型如下:

Buffer.isBuffer(obj)

示例如下:

let arr = new Array();
console.log(Buffer.isBuffer(arr));  // false
let buf = Buffer.alloc(5);
console.log(Buffer.isBuffer(buf));  // true
  1. byteLength

原型如下:

Buffer.byteLength(string[, encoding])

示例如下:

let buf = Buffer.alloc(5);
console.log(Buffer.byteLength(buf));    // 5
  1. concat

原型如下:

Buffer.concat(list[, totalLength])

示例如下:

let buf1 = Buffer.from("0320 ");
let buf2 = Buffer.from("Reyn");
let buf3 = Buffer.from(" Moralers");
let totalBuf = Buffer.concat([buf1, buf2, buf3]);
console.log(totalBuf.toString());   // 0320 Reyn Moralers

concat 静态方法不可以合并以数组构造 Buffer 实例

path

在 Node.js 中,path 模块专门用于处理路径相关内容,在使用前必须通过 require 方法导入,示例如下:

let path = require("path");

path 是 Node.js 的核心模块之一,所以不用以路径的形式导入

属性
  1. path.sep

sep 属性保存了系统的路径分隔符(windows 系统中为 \、Linux 系统中为 /

示例如下:

console.log(path.sep);
  1. path.delimiter

delimiter 属性保存了系统的环境变量分隔符(windows 系统中为 ;、Linux 系统中为 :

示例如下:

console.log(path.delimiter);
方法
  1. path.parse(path)

parse 方法将一个目录(字符串)解析为一个实例,示例如下:

let pathStr = "/a/b/c/index.html";
let pathObj = path.parse(pathStr);
console.log(pathObj);

输出内容如下:

{
    root: '/',            // 根路径名
    dir: '/a/b/c',        // 路径
    base: 'index.html',   // 文件名称
    ext: '.html',         // 文件后缀
    name: 'index'         // 文件标识
}

实际上,path 模块中的如下方法可以获取路径中的部分信息

  • path.basename(path[, ext])
  • path.dirname(path)
  • path.extname(path)

示例如下:

let pathStr = "/a/b/c/index.html";

let pathDir = path.dirname(pathStr);
let pathBase = path.basename(pathStr);
let pathExt = path.extname(pathStr);
let pathName = path.basename(pathStr, ".html");

console.log(pathDir, pathBase, pathExt, pathName);  // /a/b/c index.html .html index

在 basename 方法中,参数 [, ext] 的含义是在 base 的基础上不包含特定的后缀名

  1. path.format(object)

format 方法将特定类型的实例解析为一个路径,示例如下:

let pathObj = {
    root: '/',
    dir: '/a/b/c',
    base: 'index.html',
    ext: '.html',
    name: 'index'
}

let pathStr = path.format(pathObj);
console.log(pathStr);   // /a/b/c\index.html
  1. path.isAbsolute(path)

isAbsolute 方法判断参数是否为一个绝对路径(windows 系统中以盘符开头、linux 系统中以 / 开头),示例如下:

let wPathStr = "C:\\Program Files";
let wRes = path.isAbsolute(wPathStr);
console.log(wRes);  // true

let lPathStr = "/home/reyn/code";
let lRes = path.isAbsolute(lPathStr);
console.log(lRes);  // true
  1. path.normalize(path)

normalize 方法格式化参数为规范路径,示例如下:

let wrongPathStr = "/a//b///cd/index.html";
let rightPathStr = path.normalize(wrongPathStr);
console.log(rightPathStr);  // \a\b\c\d\index.html
  1. path.join(…paths)

join 方法将路径或路径片段的序列合并,示例如下:

let pathStr = path.join("/a/", "/b", "c", "index.html");
console.log(pathStr);   // \a\b\c\index.html

必须注意的是,不论每个路径片段如何,最终路径的分隔符有且只有一个,此外,路径片段也可以是 ./../, 且此类片段将参考已存在的路径参与运算,示例如下:

let pathStr = path.join("/a/", "/b", "../", "/c", "index.html");
console.log(pathStr);   // \a\c\index.html
  1. path.relative(from, to)

relative 方法将计算可以从 from 路径切换到 to 路径的相对路径,示例如下:

let fromPath = "/a/b/css/index.css";
let toPath = "/a/b/js/index.js";
let relPath = path.relative(fromPath, toPath);
console.log(relPath);   // ..\..\js\index.js
  1. path.resolve(…paths)

resolve 方法将路径或路径片段的序列解析为绝对路径,示例如下:

let pathStr = path.resolve("/a", "b", "c", "index.html");
console.log(pathStr);   // E:\a\b\c\index.html

如果参数中存在若干绝对路径,那么将忽略之前所有已合并的路径,示例如下:

let pathStr = path.resolve("/a", "b", "/c", "index.html");
console.log(pathStr);   // E:\c\index.html

fs

在 Node.js 中,fs 模块专门用于处理文件和目录相关内容,在使用前必须通过 require 方法导入,示例如下:

let fs = require("fs");

在 fs 模块中,许多方法存在异步和同步(Sync)的写法,不论是同步还是异步,方法的功能和参数的含义几乎相同

文件
  1. stat/statSync

stat 方法用于读取指定路径的状态信息,原型如下:

fs.stat(path[, options], callback)

示例如下:

fs.stat(__filename, function (err, stat) {
    if (err) {
        console.log("ERROR");
    } else {
        console.log(stat);
    }
});

stat 是一个实例,属性保存了路径的状态信息,例如 birthtime 属性表示路径的创建时间,此外,stat 的 isFile 和 isDirectory 方法可以判断路径是文件还是目录,示例如下:

fs.stat(__filename, function (err, stat) {
    if (err) {
        console.log("ERROR");
    } else {
        if (stat.isFile()) {
            console.log("FILE");    // FILE
        } else if (stat.isDirectory()) {
            console.log("DIR");
        }
    }
});

方法 statSync 的示例如下:

let stat = fs.statSync(__dirname);
if (stat && stat.isDirectory()) {
    console.log("DIR"); // DIR
}
  1. readFile/readFileSync

readFile 方法用于读取指定路径的内容,原型如下:

fs.readFile(path[, options], callback)

示例如下:

let readPath = path.join(__dirname, "read-demo.txt");
fs.readFile(readPath, function (err, data) {
    if (err) {
        throw new Error("Read Failed");
    } else {
        console.log(data);  // <Buffer 52 65 ... 50 21>
        console.log(data.toString());   // Reyn Never Give UP!
    }
});

默认情况下,readFile 方法将指定路径中的内容读取到 Buffer 实例中,如果在 options 选项中传入 utf8,那么将以字符串形式输出,示例如下:

let readPath = path.join(__dirname, "read-demo.txt");
fs.readFile(readPath, "utf8", function (err, data) {
    if (err) {
        throw new Error("Read Failed");
    } else {
        console.log(data);  // Reyn Never Give UP!
    }
});

方法 readFileSync 的示例如下:

let readPath = path.join(__dirname, "read-demo.txt");
let data = fs.readFileSync(readPath, "utf8");
if (data) {
    console.log(data);  // Reyn Never Give UP!
}
  1. writeFile/writeFileSync

writeFile 方法将数据写入到指定路径中,原型如下:

fs.write(fd, {buffer/string}[, options], callback)

示例如下:

let writePath = path.join(__dirname, "write-demo.txt");
fs.writeFile(writePath, "stay hungry stay foolish", function (err) {
    if (err) {
        throw new Error("ERROR");
    } else {
        console.log("Success"); // Success
    }
});

writeFile 方法也可以将 Buffer 实例中的内容写入到指定路径中,示例如下:

let writePath = path.join(__dirname, "write-demo.txt");
let buf = Buffer.from("Reyn Morales");
let err = fs.writeFileSync(writePath, buf, "utf8");
if (!err) {
    console.log("Success");
}
  1. appendFile/appendFileSync

appendFile 方法将数据追加到指定路径中,原型如下:

fs.appendFile(path, data[, options], callback)

默认情况下,write 方法写入时,先删除文件中的原有内容,之后写入新内容,追加则是将新内容写入到原有内容之后

示例如下:

let appendPath = path.join(__dirname, "write-demo.txt");
fs.appendFile(appendPath, " Never Give UP!", "utf8", function (err) {
    if (err) {
        throw new Error("ERROR");
    } else {
        console.log("Success"); // Success
    }
});
  1. createReadStream

createReadStream 方法创建并返回一个 read 流,原型如下:

fs.createReadStream(path[, options])

通过 read 流实例读取文件内容时,可以指定每次读取的字节个数,适合处理大文件

示例如下:

let readPath = path.join(__dirname, "read-demo.txt");
let readStream = fs.createReadStream(readPath, {
    encoding: "utf8",   // 字符编码
    highWaterMark: 1    // 字节个数
});

readStream.on("open", function () {
    console.log("Open Connection Between Stream & File");
});

readStream.on("error", function () {
    console.log("ERROR");
});

readStream.on("data", function (data) {
    console.log("Get Data", data);
});

readStream.on("close", function () {
    console.log("Close Connection Between Stream & File");
});

输出结果如下所示:

Open Connection Between Stream & File
Get Data R
Get Data e
Get Data y
Get Data n
Get Data
Get Data N
Get Data e
Get Data v
Get Data e
Get Data r
Get Data
Get Data G
Get Data i
Get Data v
Get Data e
Get Data  
Get Data U
Get Data P
Get Data !
Close Connection Between Stream & File

通过 read 流读取文件时的过程是先建立流和文件的连接,之后读取文件内容,结束后自动关闭此连接,上述示例中,以事件监听的方式说明此过程

  1. createWriteStream

createWriteStream 方法创建并返回一个 write 流,原型如下:

fs.createWriteStream(path[, options])

通过 write 流实例将数据写入文件时,可以指定写入数据的模式(waw+a+ 等)

示例如下:

let writePath = path.join(__dirname, "write-demo.txt");
let writeStream = fs.createWriteStream(writePath, {
    encoding: "utf8",   // 字符编码
    flags: "a"          // 追加模式
});

writeStream.on("open", function () {
    console.log("Open Connection Between Stream & File");
});
writeStream.on("error", function () {
    console.log("ERROR");
});
writeStream.on("close", function () {
    console.log("Close Connection Between Stream & File");
});

writeStream.write("\nSteven Jobs");

输出内容如下:

Open Connection Between Stream & File

上述示例中,以事件监听的方式说明 write 流的工作原理,和 read 流不同的是,write 流在写入数据之后不会自动关闭连接,必须调用 end 方法关闭连接,示例如下:

writeStream.end();

输出内容如下:

Open Connection Between Stream & File
Close Connection Between Stream & File

实际上在创建 write 流是可以通过配置 autoClose 属性以在写入文件后自动关闭连接,示例如下:

let writeStream = fs.createWriteStream(writePath, {
    encoding: "utf8",   // 字符编码
    flags: "a",         // 追加模式
    autoClose: true     // 自动关闭
});
  1. pipe

pipe 方法可以将 read 流所关联文件中的内容拷贝到 write 流所关联的文件,示例如下:

let readPath = path.join(__dirname, "from.mp4");
let writePath = path.join(__dirname, "to.mp4");

let readStream = fs.createReadStream(readPath);
let writeStream = fs.createWriteStream(writePath);

readStream.pipe(writeStream);
目录
  1. mkdir/mkdirSync

mkdir 方法用于创建目录,原型如下:

fs.mkdir(path[, options], callback)

示例如下:

let pathDir = path.join(__dirname, "samp");
fs.mkdir(pathDir, function (err) {
    if (err) {
        throw new Error("ERROR");
    } else {
        console.log("Success"); // Success
    }
});
  1. readdir/readdirSync

readdir 方法用于读取目录,原型如下:

fs.readdir(path[, options], callback)

示例如下:

let pathDir = path.join(__dirname, "samp");
fs.readdir(pathDir, function (err, files) {
    if (err) {
        throw new Error("ERROR");
    } else {
        files.forEach(function (file) {
            let filePath = path.join(pathDir, file);
            let fileStat = fs.statSync(filePath);
            if (fileStat.isFile()) {
                console.log(`${file} is a FILE`);
            } else if (fileStat.isDirectory()) {
                console.log(`${file} is a DIR`);
            }
        })
    }
})

输出内容如下:

jack.txt is a FILE
reyn.txt is a FILE
subDir is a DIR
  1. rmdir/rmdirSync

rmdir 方法用于删除目录,原型如下:

fs.rmdir(path[, options], callback)

示例如下:

let pathDir = path.join(__dirname, "samp");
fs.rmdir(pathDir, function (err) {
    if (err) {
        throw new Error("ERROR");
    } else {
        console.log("Success"); // Success
    }
});

http

Node.js 中的 http 模块中包含了网络相关的 APIs,通过这些 API 可以快速构建 web 服务器,在使用时,必须导入 http 模块,示例如下:

let http = require("http");
服务器

通过 http 模块搭建 web 服务器的过程如下:

  1. 导入 http 模块
  2. 创建 server 实例
  3. 监听 server 的 request 事件
  4. 指定 server 的端口

示例如下:

/* 1. 导入 http 模块 */
let http = require("http");

/* 2. 创建 server 实例 */
let server = http.createServer();

/* 3. 监听 server 的 request 事件 */
server.on("request", function (request, response) {
    response.end("Hello World!");
});

/* 4. 指定 server 的端口 */
server.listen(3000);

在浏览器的 url 栏中输入 127.0.0.1:3000 即可访问本地服务器并查看响应内容

上述示例中,回调函数的参数分别是 request 和 response 实例,request 实例存储网页请求信息,response 实例负责响应信息,response 实例的 end 方法可以将数据返回给浏览器并结束此次请求,此外,以链式编程的方式可以简写上述内容,示例如下:

let http = require("http");
http.createServer(function (request, response) {
    response.end("Hello World!");
}).listen(3000);

必须注意的是,若返回的数据中包含了中文字符,那么必须将数据类型以及字符编码通过响应头告知浏览器,否则将出现乱码,示例如下:

http.createServer(function (request, response) {
    response.writeHead(200, {
        "Content-Type": "text/plain; charset=utf-8"
    });
    response.end("我自横刀向天笑,去留肝胆两昆仑");
}).listen(3000);

通过 response 实例的 writeHead 方法将响应数据的相关信息写入到响应头中,参数 200 说明响应成功,之后一个参数则说明了此次响应数据的类型为文本并以 utf8 编码

请求

通常情况下,浏览器总是在 url 中包含参数从而实现向服务器请求网页或数据,而 request 实例专门用于保存浏览器请求的 url 等相关内容,所以通过 url 中包含的不同信息从而实现不同的响应

request 是 http 模块中 IncomingMessage 类的实例

路径分发

路径分发亦称之为路由,即根据不同的请求路径返回不同的数据,示例如下:

http.createServer(function (request, response) {
    response.writeHead(200, {
        "Content-Type": "text/plain; charset=utf-8"
    });
    if (request.url.startsWith("/index")) {
        response.end("Home page");
    } else if (request.url.startsWith("/login")) {
        response.end("Login Page");
    } else {
        response.end("Content Not Found");
    }
}).listen(3000);

request.url 属性保存了浏览器请求时 url 中端口号之后的内容,上述示例中,如果在 url 中包含 /index 那么将响应 Home Page,如果在 url 中包含 /login 那么将响应 Login Page,其它情况则响应 Content Not Found,此外 response 实例的 end 方法由于调用之后即表明此次响应结束,所以 end 方法只能返回一次数据,实际上,response 实例中存在 write 方法专门用于向浏览器传输数据,可以多次调用,不会自动结束响应,在传输完成后必须调用 end 结束响应,示例如下:

http.createServer(function (request, response) {
    response.write("Home\n");
    response.write("Username\n");
    response.write("Password\n");
    response.end();
}).listen(3000);
GET

GET 请求方式的特点在于将参数以特定格式置于 url 中发送到服务器,Node.js 中 url 模块专门用于处理 URL,通过 parse 方法可以将 url 中的各种信息保存到实例中从而简化参数处理,示例如下:

let http = require("http");
let url = require("url");

let sampURL = "http://root:1234@www.site.com/index.html?name=reyn&age=21";
let urlObj = url.parse(sampURL);
console.log(urlObj);

输出内容如下:

Url {
    protocol: 'http:',
    slashes: true,
    auth: 'root:1234',
    host: 'www.site.com',
    port: null,
    hostname: 'www.site.com',
    hash: null,
    search: '?name=reyn&age=21',
    query: 'name=reyn&age=21',
    pathname: '/index.html',
    path: '/index.html?name=reyn&age=21',
    href: 'http://root:1234@www.site.com/index.html?name=reyn&age=21'
}

不难发现,Url 实例的 query 属性即保存了 url 中的参数,实际上,在 url 模块的 parse 中可以传入 2 个参数,如果最后一个参数为 true,那么返回的实例中 query 属性也将成为实例而非字符串,示例如下:

let sampURL = "http://root:1234@www.site.com/index.html?name=reyn&age=21";
let urlObj = url.parse(sampURL, true);
console.log(urlObj);
console.log(urlObj.query.name); // reyn
console.log(urlObj.query.age);  // 21

以下是一个使用示例:

  • HTML
<form action="http://127.0.0.1:3000" method="get">
    <input type="text" name="name">
    <input type="text" name="age">
    <input type="submit" value="SUBMIT">
</form>
  • JavaScript
http.createServer(function (request, response) {
    let obj = url.parse(request.url, true);
    response.end(`${obj.query.name} --- ${obj.query.age}`);
}).listen(3000);
POST

POST 请求方式的特点在于可以传输任意类型的数据且不限大小,Node.js 中 querystring 模块专门用于处理 POST 传递的参数,通过 parse 方法可以将 POST 参数中的各种信息保存到实例中,示例如下:

  • HTML
<form action="http://127.0.0.1:3000" method="post">
    <input type="text" name="name">
    <input type="text" name="age">
    <input type="submit" value="SUBMIT">
</form>
  • JavaScript
let http = require("http");
let queryString = require("querystring");

http.createServer(function (request, response) {
    let params = new String();
    request.on("data", function (chunk) {
        params += chunk;
    });
    request.on("end", function () {
        let obj = queryString.parse(params);
        response.end(`${obj.name} --- ${obj.age}`);
    });
}).listen(3000);

Node.js 为了提高 POST 传输性能,必须分段获取 POST 传递的参数,所以在上述示例中,通过监听 request 实例的 data 事件获取数据,在传输结束后将开启 request 实例的 end 事件,从而解析参数并将参数返回,此外,request 实例中的 method 属性存储了请求类型,示例如下:

http.createServer(function (request, response) {
    if (request.method.toLocaleLowerCase() === "get") {
        response.end("GET Method");
    } else if (request.method.toLocaleLowerCase() == "post") {
        response.end("POST Method");
    }
}).listen(3000);
响应

在浏览器向服务器发送请求后,通常情况下,服务器将某些资源或数据返回给浏览器,而 response 实例专门用于向浏览器传输资源或数据,浏览器通过此数据实时为用户提供最新的内容

response 是 http 模块中 ServerResponse 类的实例

网页

HTML 文件本质上是一个文本文件,所以可以使用 fs 模块相关功能读取 .html 文件内容,之后通过 response 实例将内容返回,示例如下:

let http = require("http");
let fs = require("fs");
let path = require("path");

http.createServer(function (request, response) {
    if (request.url.startsWith("/index")) {
        let filePath = path.join(__dirname, "www", "index.html");
        fs.readFile(filePath, "utf8", function (err, data) {
            if (err) {
                response.end("Page Not Found");
            } else {
                response.end(data);
            }
        });
    } else if (request.url.startsWith("/login")) {
        let filePath = path.join(__dirname, "www", "login.html");
        fs.readFile(filePath, "utf8", function (err, data) {
            if (err) {
                response.end("Page Not Found");
            } else {
                response.end(data);
            }
        });
    } else {
        response.end("Content Not Found");
    }
}).listen(3000);

上述示例中,以路径分发的方式处理不同的请求,不论在哪一个请求中,均是先合并资源路径,之后通过 fs 模块的 readFile 方法读取文件内容,若读取成功则返回相应的内容,此外,不难发现:在任何一个路径中的处理方式基本相应,所以可以重构为一个方法从而简化代码,示例如下:

let returnStaticPage = function (rootPath, resourceName, response) {
    let filePath = path.join(rootPath, resourceName);
    fs.readFile(filePath, "utf8", function (err, data) {
        if (err) {
            response.end("Page Not Found");
        } else {
            response.end(data);
        }
    });
}

let root = path.join(__dirname, "www");

http.createServer(function (request, response) {
    if (request.url.startsWith("/index")) {
        returnStaticPage(root, "index.html", response);
    } else if (request.url.startsWith("/login")) {
        returnStaticPage(root, "login.html", response);
    } else {
        response.end("Content Not Found");
    }
}).listen(3000);
静态资源

通过 response 实例向浏览器传输静态资源的方式和传输网页的原理基本相同,关键在于,静态资源除了类似于静态网页的文本数据之外,还包括图片、视频、音频等二进制文件,所以在读取文件必须以不同的方式读取数据,此外,不同类型的文件有不同的解析方式,如果向浏览器传输数据时未说明何种类型的数据,那么浏览器是无法正确解析的,所以在传输静态资源时,必须编写相应的响应头,示例如下:

let http = require("http");
let fs = require("fs");
let path = require("path");
let mime = require("./mime.json");

let returnData = function (rootPath, resourceName, response) {
    let filePath = path.join(rootPath, resourceName);

    let ext = path.extname(filePath);
    let cType = mime[ext];
    if (cType.startsWith("text")) {
        cType += "; charset=utf8";
    }
    response.writeHead(200, {
        "Content-Type": cType
    });

    fs.readFile(filePath, function (err, data) {
        if (err) {
            response.end("Resource Not Found");
        } else {
            response.end(data);
        }
    });
}

let root = path.join(__dirname, "www");

http.createServer(function (request, response) {
    returnData(root, request.url, response);
}).listen(3000);

上述示例中,利用了相应静态网页时的 returnData 方法,添加了响应头相关的代码,并且在读取文件时删除了 utf8 的选项,此外,通过 mime 实例可以从后缀确定传输数据的类型,mime 部分内容如下:

{
    ".bmp": "image/bmp",
    ".html": "text/html",
    ".jpg": "image/jpeg",
    ".js": "application/x-javascript"
}

Node.js 模块系统

require 原理

在 Node.js 中,必须通过 require 方法导入模块,所以理解 require 方法的原理是有必要的,由于模块通常是一个文件,所以导入模块时一定读取了文件,不过读取的数据并不能直接执行,而在 JavaScript 中,可以通过 eval 或者 new Function 执行字符串中的代码(浏览器),示例如下:

  • eval
let str1 = "console.log('Hello World');";
eval(str1);  // Hello World

let name = "Reyn";
let str2 = "console.log(name);";
eval(str2); // Reyn
  • new Function
let str1 = "console.log('Hello World');";
let fn1 = new Function(str1);
fn1();  // Hello World

let name = "Reyn";
let str2 = "console.log(name);";
let fn2 = new Function(str2);
fn2();  // Reyn

不论何种方式,若在字符串代码中引用外部数据,都是可以访问的,所以以此方式运行代码并不安全,Node.js 提供的 vm 模块专门用于执行字符串代码,示例如下:

let vm = require("vm");

let str1 = "console.log('Hello World');";
vm.runInThisContext(str1);

let name = "Reyn";
let str2 = "console.log(name);";
vm.runInThisContext(str2);  // name is not defined

虽然 vm 模块中的 runInThisContext 方法无法访问外部数据,不过可以访问 global,示例如下:

let vm = require("vm");

global.name = "Reyn";
let str3 = "console.log(name);";
vm.runInThisContext(str3);  // Reyn

实际上 vm 模块中的 runInNewContext 方法即无法访问外部数据,也无法访问 global,示例如下:

let vm = require("vm");

let str1 = "console.log('Hello World');";
vm.runInNewContext(str1);

let name = "Reyn";
let str2 = "console.log(name);";
vm.runInNewContext(str2);  // name is not defined

global.name = "Reyn";
let str3 = "console.log(name);";
vm.runInNewContext(str3);  // name is not defined
require 核心源码

示例如下:

let fs = require("fs");
let path = require("path");
let vm = require("vm");

/**
 * myModule 类
 */
class myModule {
    constructor(id) {
        /* 模块路径 */
        this.id = id;
        /* 模块数据 */
        this.exports = new Object();
    }
}

/**
 * 模块缓存
 * @key     id      --- 模块路径
 * @value   exports --- 模块数据
 */
myModule._cache = new Object();

/**
 * 模块解析
 * @key     js / json
 */
myModule._extensions = {
    /**
     * 如果待加载的文件类型是 JavaScript
     * @param {myModule} module 待绑定数据的模块实例
     */
    ".js": function (module) {
        // 1. 读取文件内容
        let codeStr = fs.readFileSync(module.id);
        // 2. 将文件内容包裹在一个函数中
        let wrap = myModule.wrapper[0] + codeStr + myModule.wrapper[1];
        // 3. 执行字符串代码、获取可执行的函数代码
        let fn = vm.runInThisContext(wrap);
        // 4. 通过 call 方法调用函数,此时函数中的 this 属性以及参数均为模块实例的 exports 属性
        fn.call(module.exports, module.exports);
    },
    /**
     * 如果待加载的文件类型是 JSON
     * @param {myModule} module 待绑定数据的模块实例
     */
    ".json": function (module) {
        // 1. 读取文件内容
        let json = fs.readFileSync(module.id);
        // 2. 将 JSON 字符串转换为对象
        let obj = JSON.parse(json);
        // 3. 将 JSON 实例绑定到模块实例的 exports 属性上
        module.exports = obj;
    }
}

/**
 * 模块模板
 */
myModule.wrapper = [
    '(function (exports) { ',
    '\n});'
];

/**
 * 类似于 Node.js 中的 require 方法
 * @param {string} filePath 模块路径(相对)
 * @returns 模块中通过 exports 绑定的属性或方法(实例)
 */
function myRequire(filePath) {
    // 1. 绝对路径
    let moduleAbsolutePath = path.join(__dirname, filePath);

    // 2. 查询缓存
    let cacheModule = myModule._cache[moduleAbsolutePath];
    if (cacheModule) {  // 如果存在,那么返回缓存中相应模块的 exports 属性
        return cacheModule.exports;
    }

    // 3. 如果不存在,那么创建一个新的模块实例,并将它添加到缓存中
    let nModule = new myModule(moduleAbsolutePath);
    myModule._cache[moduleAbsolutePath] = nModule;

    // 4. 加载模块
    tryLoadModule(nModule, moduleAbsolutePath);

    // 5. 返回模块实例中的 exports 属性
    return nModule.exports;
}

/**
 * 此方法将模块数据绑定到模块实例中
 * @param {myModule} module 待绑定数据的模块实例
 * @param {string} filename 模块路径(绝对)
 */
function tryLoadModule(module, filename) {
    // 1. 获取待加载的文件类型
    let extName = path.extname(filename);

    // 2. 根据不同的文件类型调用不同的加载方式
    myModule._extensions[extName](module);
}

/* 测试用例 -> JavaScript */
let mdJS = myRequire("./module-demo.js");
console.log(mdJS);      // { name: 'Reyn Morales' }

/* 测试用例 -> JSON */
let mdJSON = myRequire("./module-demo.json");
console.log(mdJSON);    // { name: 'Reyn Morales', age: 21, gender: 'male' }

相信示例中的注意可以帮助理解原理,所以此处不再详细说明,相对难以理解的地方在于通过 call 方法执行函数时所传入的参数,此时可以研究一下函数中的内容,如下所示:

(function (exports) {
    let name = "Reyn Morales";
    exports.name = name;
});

函数体即为模块中的代码,而 exports 本质上是此匿名函数的参数,在调用此函数时,我们将 module.exports 传入,所以将属性或方法绑定到 exports 上时,实际上是绑定到了模块实例的 exports 属性上,在利用 require 方法导入时,require 方法的返回值亦为模块实例的 exports 属性,在了解了 require 的原理之后,可以解决如下问题:

  1. 为什么 Node.js 中的 this 是 {}

每一个模块本质上是通过一个函数执行的,此函数通过 call 方法调用时修改了函数原本的 this 属性为模块实例的 exports 属性,默认情况下,创建一个新的模块实例时,exports 属性即为 {}

  1. 为什么 Node.js 中可以使用 exports、require、module、__filename、__dirname

实际上,将模块代码包裹到函数中时,函数如下所示:

上述示例中简化了此函数的参数

(function (exports, require, module, __filename, __dirname) {
    // 模块代码
});

在通过 call 方法调用此函数时,将传入相应的参数

  1. 为什么 Node.js 中不可以赋值 exports, 却可以赋值 module.exports

exports 本质上是模块实例的 exports 属性,此属性是一个实例,如果以如下方式赋值:

exports = "Reyn Morales";

那么相当于将 exports 被赋值为字符串,而不再和模块实例的 exports 属性相同,此时模块实例的 exports 属性即为 {},若通过 module.exports 属性赋值,本质上即将模块实例的 exports 属性修改

  1. 通过 require 导入包时候应该使用 var、let 还是 const

导入包的目的在于使用模块中的属性或方法而非修改模块本身,所以应该使用 const 关键字

事件循环

JavaScript 代码的执行分为同步和异步代码,同步代码以顺序执行,而所有的异步代码将保存在事件循环中,待所有同步代码执行完毕之后,再开启死循环执行事件循环中满足条件的异步代码,示例如下:

console.log("1");   // Fst
setTimeout(function () {
    console.log("2");   // Trd
}, 0);
console.log("3");   // Snd

实际上,事件循环中所保存的事件存在宏任务和微任务的区别,宏任务即为执行时间较长的任务,微任务即为执行时间相对较短的任务,不同类型的任务有不同优先级从而导致不同的执行结果

类型事件
宏任务setTimeoutsetIntervalsetImmediate、…
微任务PromiseMutationObserverprocess.nextTick、…

setImmediate 类似于 setTimeout,区别在于既没有也不能添加延迟时间,MutationObserver 实例专门用于监听浏览器中节点内容的改变,示例如下:

  • HTML
<div class="box"></div>
<button class="append">Append Node</button>
<button class="remove">Remove Node</button>
  • JavaScript
let oBox = document.querySelector(".box");

let oAppendBtn = document.querySelector(".append");
oAppendBtn.onclick = function () {
    let oP = document.createElement("p");
    oP.innerText = "Reyn Morales";
    oBox.appendChild(oP);
}
let oRemoveBtn = document.querySelector(".remove");
oRemoveBtn.onclick = function () {
    let oP = document.querySelector("p");
    oBox.removeChild(oP);
}

new MutationObserver(function () {
    console.log("Observer Alert");
}).observe(oBox, {
    "childList": true
});

此外,宏任务和微任务在不同宿主环境下的执行也不尽相同

浏览器

在浏览器中,优先以顺序执行同步代码,将所有的异步代码保存到事件循环中,如果是宏任务,那么将宏任务保存到宏任务队列中,如果是微任务,那么将微任务保存到微任务队列中,待所有同步代码执行完毕后,开启一个死循环执行异步代码,优先执行微任务队列中的所有任务,执行完毕后再执行宏任务队列中的任务,示例如下:

setTimeout(function () {
    console.log("st1");
}, 0);
Promise.resolve().then(function () {
    console.log("p1");
});
console.log("Play");
setTimeout(function () {
    console.log("st2");
});
Promise.resolve().then(function () {
    console.log("p2");
});
console.log("Stop");

由于存储任务的数据结构是队列,所以任务队列中的执行遵循先进先出原则,输出内容如下:

Play
Stop
p1
p2
st1
st2

必须注意的是,在每一个宏任务执行完毕时,将检查微任务队列中是否存在任务,如果存在,那么将优先清空微任务队列中的任务,之后再执行宏任务中的任务,示例如下:

setTimeout(function () {
    console.log("st1");
    Promise.resolve().then(function () {
        console.log("p1");
    });
    Promise.resolve().then(function () {
        console.log("p2");
    });
}, 0);
console.log("Play");
Promise.resolve().then(function () {
    console.log("p3");
});
setTimeout(function () {
    console.log("st2");
}, 0);
console.log("Stop");

输出内容如下:

Play
Stop
p3
st1
p1
p2
st2
Node.js

Node.js 中的事件循环和浏览器的区别如下所示:

  • 任务队列
    • Node.js 中存在 6 个事件队列
      • timers
        • setTimeout / setInterval
      • pending callbacks
        • tcp、udp
      • idle、prepare
        • use inner
      • poll
        • IO
      • check
        • setImmediate
      • close callbacks
        • close
  • 微任务
    • Node.js 中不存在专门用于执行微任务的队列,当所有同步代码执行完毕或者切换下一个执行队列时将清空所有微任务,此外,由于微任务并非以队列的形式组织,所以执行微任务时将以优先级顺序执行,例如,process.nextTick 大于 Promise 的优先级

Node.js 的事件队列中,当队列为空(执行完毕 or 无满足条件的任务)或者已执行任务数量达到系统阈值时,将切换队列,此外,关注 timers、poll 和 check 事件队列,poll 事件队列存储了非计时器相关事件的所有回调,所以简化之后的执行顺序如下所示:

事件循环

示例如下:

setTimeout(function () {
    console.log("st1");
    Promise.resolve().then(function () {
        console.log("p1");
    });
    Promise.resolve().then(function () {
        console.log("p3");
    });
}, 0);
Promise.resolve().then(function () {
    console.log("p2");
});
console.log("Play");
setTimeout(function () {
    console.log("st2");
}, 0);
process.nextTick(function () {
    console.log("nt1");
});
console.log("Stop");

输出内容如下:

Play
Stop
nt1
p2
st1
p1
p3
st2

实际上,执行完 poll 队列中的任务后,将检查 check 队列是否存在任务,如果存在,那么切换到 check 队列执行,如果不存在,那么将检查 timers 队列是否存在任务,如果存在,那么切换到 timers 队列执行,如果不存在,那么事件循环将阻塞在 poll 队列中以避免资源损耗,示例如下:

const fs = require("fs");
const path = require("path");
Promise.resolve().then(function () {
    console.log("Micro Event p1");
});
setTimeout(function () {
    console.log("Timers Event st1");
    Promise.resolve().then(function () {
        console.log("Micro Event p2");
    });
}, 2000);
console.log("Play");
setTimeout(function () {
    console.log("Timers Event st2");
});
fs.readFile(path.join(__dirname, "el-start.html"), function () {
    console.log("Poll Event rf1");
});
console.log("Stop");

输出内容如下:

Play
Stop
Micro Event p1
Timers Event st2
Poll Event rf1
Timers Event st1
Micro Event p2

此外,由于时间误差的原因,将导致代码的执行结果是随机的,示例如下:

setTimeout(function () {
    console.log("st");
}, 0);

setImmediate(function () {
    console.log("si");
});

原因在于 setTimeout 中的 0 存在一定的误差,导致 timers 事件队列可能在 check 事件队列之后执行,不过如下形式的执行顺序是确定的,示例如下:

const fs = require("fs");
const path = require("path");
fs.readFile(path.join(__dirname, "el-start.html"), function () {
    setTimeout(function () {
        console.log("st");
    }, 0);
    setImmediate(function () {
        console.log("si");
    });
});

输出内容如下:

si
st

原因在于,上述示例中的事件循环先执行 poll 事件队列中任务,执行完毕后将依次检查并执行 check 和 timers 队列,所以执行顺序是确定的

自定义包

在 Node.js 中,我们可以自定义包(本地/全局)并将其发布到 npm 官网,包的规范如下:

  • \MyPackage
    • \bin
      • .bin
    • \lib
      • .js
    • \doc
      • .doc
    • \test
    • index.js (Entry Point)
    • package.json

此外 package.json 中的字段说明如下:

字段描述
name名称
description概要
version版本号
keywords关键字
main入口文件
scripts指令集合
maintainers维护者
contributors贡献者
licenses许可证
dependencies依赖(产品)
devDependencies依赖(开发)

此处详细说明 main 和 scripts,详情可以访问官网文档

package.json 中的 main 字段用于存储包的入口文件,默认情况下为 index.js,如果包中不存在 index.js 文件,那么必须配置 main 字段,scripts 字段用于存储若干指令,以通过 npm run 指令 的形式快速执行指令,示例如下:

{
    "scripts": {
        "move": "node index.js Reyn Morales"
    }
}

此时若在包目录下运行 npm run move 命令,那么相当于在命令行中执行了 node index.js Reyn Morales 命令,Reyn Morales 是 index.js 执行时的参数,在 index.js 中可以通过 process 的 argv 属性访问,示例如下:

const p = require("process");

p.argv.forEach(function (cv, ci) {
    console.log(`${ci} : ${cv}}`);
});

输出内容如下:

> my_first_package@1.0.0 move
> node index.js Reyn Morales

0 : C:\Program Files\nodejs\node.exe}
1 : E:\LearnNotes\nodejs\code\myPackage\my_first_package\index.js}
2 : Reyn}
3 : Morales}

若指令名称为 starttest,那么通过 npm 执行命令时参数 run 是缺省的

本地

步骤如下:

  1. 创建包目录
  2. 执行 npm init -y 命令初始化包
  3. 编写包入口 .js 文件
  4. 将自定义包发布到官网
    • 注册账号
    • 在包目录下执行 npm addUser 命令
    • 在包目录下执行 npm publish 命令
全局

步骤如下:

  1. 创建包目录
  2. 执行 npm init -y 命令初始化包
  3. 包命令文件
    • 命令文件中必须指定运行环境 —— #! /usr/bin/env node
  4. 编写 package.json 的 bin 字段
    • 在命令行中输入某个命令,执行哪一个 .js 文件
  5. 执行 npm link 命令将包链接到全局
  6. 将自定义包发布到官网

示例如下:

{
    "print": "index.js"
}

在 cmd 命令行中输入 print 即可运行 index.js 脚本

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值