Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。
Node的REPL
什么是REPL呢?
REPL是Read-Eval-Print Loop的简称,翻译为“读 取 -求值-输出”循环; REPL是一个简单的、交互式的编程环境;
事实上,我们浏览器的console就可以看成一个REPL。
◼ Node也给我们提供了一个REPL环境,我们可以在其中演练简单的代码。
argv
在C /C + +程序中的main函数中,实际上可以获取到两个参数:
argc:argument counter的缩写,传递参数的个数;
argv:argument vector的缩写,传入的具体参数。
vector翻译过来是矢量的意思,在程序中表示的是一种数据结构。
在C + +、Java中都有这种数据结构,是一种数组结构;
在JavaScript中也是一个数组,里面存储一些参数信息;
我们可以在代码中,将这些参数信息遍历出来,使用:
Node的输出
console.log
最常用的输入内容的方式:console.log
◼ console.clear
清空控制台:console.clear
◼ console.trace
打印函数的调用栈:console.trace
特殊的全局对象
为什么我称之为特殊的全局对象呢?
这些全局对象可以在模块中任意使用,但是在命令行交互中是不可以使用的;
包括: dirname、 filename、exports、module、require()
◼ dirname:获取当前文件所在的路径:
注意:不包括后面的文件名
◼ filename:获取当前文件所在的路径和文件名称:
注意:包括后面的文件名称
global和window的区别
在浏览器中,全局变量都是在window上的,比如有document、setInterval、setTimeout、alert、console等等
◼ 在Node中,我们也有一个global属性
但是在浏览器中执行的JavaScript代码,如果我们在顶级范围内通过var定义的一个属性,默认会被添加到window
对象上:
但是在node中,我们通过var定义一个变量,它只是在当前模块中有一个变量,不会放到全局中:
模块化
-
事实上模块化开发最终的目的是将程序划分成一个个小的结构;
-
这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
3.这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
4.也可以通过某种方式,导入另外结构中的变量、函数、对象等;
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
JavaScrip缺陷
比如var定义的变量作用域问题;
比如JavaScript的面向对象并不能像常规面向对象语言一样使用class;
比如JavaScript没有模块化的问题;
CommonJS和Node
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了
体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
Node是CommonJS在服务器端一个具有代表性的实现;
Browserify是CommonJS在浏览器中的一种实现;
webpack打包工具具备对CommonJS的支持和转换;
◼ 所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
在Node中每一个js文件都是一个单独的模块;
这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
我们可以使用这些变量来方便的进行模块化开发;
◼ 前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:
exports和module.exports可以负责对模块中的内容进行导出;
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
commonjs
exports【Obejct】导出
require【method】导入
// bar.js
const name = 'admin';
const sum = (a, b) => {
return a + b;
};
// 导出
exports.name = name;
exports.sum = sum;
module.exports.age = 10;
// main.js
// 导入
const bar = require('./bar');
// 热部署 npm i -g nodemon
console.log(bar.name);
console.log(bar.sum(10, 20));
// 解构导入
const { name, sum } = require('./bar');
console.log(name, sum(10, 50));
exports导出
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
另外一个文件中可以导入:
理解下面这句话,Node中的模块化一目了然
意味着main中的bar变量等于exports对象;
也就是require通过各种查找方式,最终找到了exports这个对象;
并且将这个exports对象赋值给了bar变量;
bar变量就是exports对象了;
浅层拷贝
为了进一步论证,bar和exports是同一个对象:
bar对象是exports对象的浅拷贝(引用赋值);
浅拷贝的本质就是一种引用的赋值而已;
module.exports
Node中我们经常导出东西的时候,又是通过module.exports导出的:
module.exports和exports有什么关系或者区别呢?
◼ 我们追根溯源,通过维基百科中对CommonJS规范的解析:
CommonJS中是没有module.exports的概念的;
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;
所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
因为module才是导出的真正实现者;
但是,为什么exports也可以导出呢?
这是因为module对象的exports属性是exports对象的一个引用;
也就是说 module.exports = exports = main中的bar;
require
require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象。
情况一:X是一个核心模块,比如path、http
直接返回核心模块,并且停止查找
情况二:
X是以 ./ 或 …/ 或 /(根目录)开头的
第一步:将X当做一个文件在对应的目录下查找;
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序:
✓ 1> 直接查找文件X ✓ 2> 查找X.js文件
✓ 3> 查找X.json文件
✓ 4> 查找X.node文件
第二步:没有找到对应的文件,将X作为一个目录
查找目录下面的index文件
✓ 1> 查找X/index.js文件
✓ 2> 查找X/index.json文件
✓ 3> 查找X/index.node文件
如果没有找到,那么报错:not found
情况三:直接是一个X(没有路径),并且X不是一个核心模块
ESModule
JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJ S、AMD、CMD等,
所以在ES推出自己的模块化系统时,大家也是兴奋异常。
ES Module和CommonJS的模块化有一些不同之处:
一方面它使用了import和export关键字;
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
◼ ES Module模块采用export和import关键字来实现模块化:
export负责将模块内的内容导出;
import负责从其他模块导入内容;
export关键字
export关键字将一个模块中的变量、函数、类等导出;
我们希望将其他中内容全部导出,它可以有如下的方式:
◼ 方式一:在语句声明的前面直接加上export关键字
◼ 方式二:将所有需要导出的标识符,放到export后面的 {}中 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
所以: export {name: name},是错误的写法;
◼ 方式三:导出时给标识符起一个别名
import关键字
import关键字负责从另外一个模块中导入内容
◼ 导入内容的方式也有多种:
◼ 方式一:import {标识符列表} from ‘模块’;
注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
◼ 方式二:导入时给标识符起别名
◼ 方式三:通过 *将模块功能放到一个模块功能对象(a module object)上
Export和import结合使用
补充:export和import可以结合使用
为什么要这样做呢?
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
这样方便指定统一的接口规范,也方便阅读;
这个时候,我们就可以使用export和import结合使用;
default用法
前面我们学习的导出功能都是有名字的导出(named exports):
在导出export时指定了名字;
在导入import时需要知道具体的名字;
◼ 还有一种导出叫做默认导出(default export)
默认导出export时可以不需要指定名字;
在导入时不需要使用 {},并且可以自己来指定名字;
它也方便我们和现有的CommonJS等规范相互操作;
◼ 注意:在一个模块中,只能有一个默认导出(default export)
import函数
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
◼ 为什么会出现这个情况呢?
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;
◼ 但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不懂的条件,动态来选择加载模块的路径;
这个时候我们需要使用 import() 函数来动态加载;
CommonJS的加载过程
CommonJS模块加载js文件的过程是运行时加载的,并且是同步的:
运行时加载意味着是js引擎在执行js代码的过程中加载 模块;
同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行;
CommonJS通过module.exports导出的是一个对象:
导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量;
但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改;
ES Module加载过程
ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的:
编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:
比如from后面的路径需要动态获取;
比如不能将import放到if等语句的代码块中;
所以我们有时候也称ES Module是静态解析的,而不是动态或者运行时解析的;
◼ 异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继
续执行;
也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;
如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行;
Web服务器
创建服务器
创建服务器对象,我们是通过 createServer 来完成的
http.createServer会返回服务器的对象;
底层其实使用直接 new Server 对象
我们也可以自己来创建这个对象:
监听主机和端口号
Server通过listen方法来开启服务器,并且在某一个主机和端口上监听网络请求:
也就是当我们通过 ip:port的方式发送到我们监听的Web服务器上时;
我们就可以对其进行相关的处理;
◼ listen函数有三个参数:
◼ 端口port: 可以不传, 系统会默认分配端口(通过server.address().port), 后续项目中我们会写入到环境变量中;
◼ 主机host: 通常可以传入localhost、ip地址127.0.0.1、或者ip地址0.0.0.0,默认是0.0.0.0;
localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1;
127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
✓ 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ; ✓ 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
✓ 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的;
0.0.0.0: ✓ 监听IPV4上所有的地址,再根据端口找到不同的应用程序;
✓ 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的;
◼ 回调函数:服务器启动成功时的回调函数;
request对象
在向服务器发送请求时,我们会携带很多信息,比如:
本次请求的URL,服务器需要根据不同的URL进行不同的处理;
本次请求的请求方式,比如GET、POST请求传入的参数和处理的方式是不同的;
本次请求的headers中也会携带一些信息,比如客户端信息、接受数据的格式、支持的编码格式等;
◼ 这些信息,Node会帮助我们封装到一个request的对象中,我们可以直接来处理这个request对象
URL的处理
客户端在发送请求时,会请求不同的数据,那么会传入不同的请求地址:
比如 http://localhost:8000/login;
比如 http://localhost:8000/products;
◼ 服务器端需要根据不同的请求地址,作出不同的响应:
URL的解析
◼ 那么如果用户发送的地址中还携带一些额外的参数呢?
http://localhost:8000/login?name=why&password=123;
这个时候,url的值是 /login?name=why&password=123;
◼ 我们如何对它进行解析呢?使用内置模块url:
method的处理
在Restful规范(设计风格)中,我们对于数据的增删改查应该通过不同的请求方式:
GET:查询数据;
POST:新建数据;
PATCH:更新数据;
DELETE:删除数据;
◼ 所以,我们可以通过判断不同的请求方式进行不同的处理。
比如创建一个用户:
请求接口为 /users; 请求方式为 POST请求;
携带数据 username和password;
创建用户接口
在我们程序中如何进行判断以及获取对应的数据呢?
这里我们需要判断接口是 /users,并且请求方式是POST方法去获取传入的数据;
获取这种body携带的数据,我们需要通过监听req的 data事件来获取;
headers属性(一)
◼ 在request对象的header中也包含很多有用的信息,客户端会默认传递过来一些信息:
◼ content-type是这次请求携带的数据的类型:
application/json表示是一个json类型;
text/plain表示是文本类型;
application/xml表示是xml类型;
multipart/form-data表示是上传文件;
headers属性(二)
content-length:文件的大小和长度
◼ keep-alive: http是基于TCP协议的,但是通常在进行一次请求和响应结束后会立刻中断;
在http1.0中,如果想要继续保持连接:
✓ 浏览器需要在请求头中添加 connection: keep-alive;
✓ 服务器需要在响应头中添加 connection:keey-alive;
✓ 当客户端再次放请求时,就会使用同一个连接,直接一方中断连接;
在http1.1中,所有连接默认是 connection: keep-alive的;
✓ 不同的Web服务器会有不同的保持 keep-alive的时间;
✓ Node中默认是5s中;
◼ accept-encoding:告知服务器,客户端支持的文件压缩格式,比如js文件可以使用gzip编码,对应 .gz文件;
◼ accept:告知服务器,客户端可接受文件的格式类型;
◼ user-agent:客户端相关的信息;
返回响应结果
如果我们希望给客户端响应的结果数据,可以通过两种方式:
Write方法:这种方式是直接写出数据,但是并没有关闭流;
end方法:这种方式是写出最后的数据,并且写出后会关闭流;
◼ 如果我们没有调用 end和close,客户端将会一直等待结果:
所以客户端在发送网络请求时,都会设置超时时间。
返回状态码
Http状态码(Http Status Code)是用来表示Http响应状态的数字代码:
Http状态码非常多,可以根据不同的情况,给客户端返回不同的状态码;
常见的状态码是下面这些(后续项目中,也会用到其中的状态码);
设置状态码常见的有两种方式:
响应头文件
返回头部信息,主要有两种方式:
res.setHeader:一次写入一个头部信息;
res.writeHead:同时写入header和status;
Header设置 Content-Type有什么作用呢?
默认客户端接收到的是字符串,客户端会按照自己默认的方式进行处理;
http请求
axios库可以在浏览器中使用,也可以在Node中使用:
在浏览器中,axios使用的是封装xhr;
在Node中,使用的是http内置模块;