Node服务器端JavaScript REPL 模式 单线程

核心的JavaScript语法定义了最小限度的API,可以操作数值、文本数组等,但不包含输入输出功能。输出入是内嵌JavaScript的宿主环境的责任 。

宿主环境:浏览器、node

  • 前端的JavaScript是由ECMAScript、DOM、BOM组合而成,Node.js是由ECMAScript、OS、File、Net、DB组成
  • Node.js是使 JavaScript 运行在浏览器之外,即服务端的开发平台, 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,也是一个
  • 可以在node.green/上获取到Node各个版本对ES语法的支持

Node,JavaScript与底层操作系统绑定的结合,与限制JavaScript只能使用浏览器提供的API不同,Node给予JavaScript可以访问整个操作系统的权限,允许JavaScript读写文件、通过网络发送和接收数据。应用:

  • 替代命令行脚本、交互式终端程序
  • 运行受信程序的编程语言,没有浏览器运行不受信代码带来的安全限制
  • 编写高效、高并发web服务器的流行环境:高并发、实时聊天、实时消息推送、基于社交网络的大规模 Web 应用
  • 客户端逻辑强大的SPA
  • Restful API,可以处理数万条链接
  • 实时websocket应用
  • 前端工具链,e.g:单元测试工具、客户端 JavaScript 编译器
  • 桌面开发、带有图形用户界面的本地应用程序
  • TCP/UDP 套接字应用程序
  • Node.js 还可以部署到非网络应用的环境下,比如一个命令行工具。Node.js 还可以调用C/C++ 的代码,这样可以充分利用已有的诸多函数库,也可以将对性能要求非常高的部分用C/C++ 来实现。

Node.js的结构图
在这里插入图片描述
libuv是提供异步功能的C库,在运行时负责一个事件循环,一个线程池、文件系统I/O、DNS相关的IO和网络IO。

  • node.js启动后,会开启一个JS主线程和libuv提供的线程池和Event Loop。当发现有IO操作就交给线程池并注册回调函数。
  • node.js的全局对象有global、process、console、module、exports
  • process :node进程相关的信息,比如运行node程序时的命令行参数。或者设置进程相关信息,比如设置环境变量。

REPL 模式

  • REPL【Read-eval-print loop输入—求值—输出循环】
  • 和python一样,Node.js 也有这样的功能,运行无参数的 node 将会启动一个 JavaScript 的交互式 shell
  • 打开命令提示符,然后输入 node,进入 REPL 模式以后,会出现一个“>”提示符提示你输入命令,输入后按回车,Node.js 将会解析并执行命令。
  • 执行了一个函数,REPL 会在下一行显示这个函数的返回值。
  • 连续按两次 Ctrl + C 退出Node.js 的 REPL 模式。
  • 如果你输入了一个错误的指令,REPL 则会立即显示错误并输出调用栈。

process.argv命令行参数

  • 第一、第二个的元素是node可执行文件(环境)和被执行JavaScript文件路径
  • 提供给node可执行文件且由它解释的命令行参数会被node可执行文件使用,不会出现在process.argv中

node也会从unix风格的环境变量中获取输入,node把这些环境变量保存在process.env对象中使用

1
2
3
js复制代码SHELL: '/bin/bash',
USER: 'joy',
HOME: '/Users/joy'

单线程、非阻塞的事件编程模式

  • 传统的架构是多线程模型,为每个业务逻辑提供一个系统线程,通过系统线程切换来弥补同步式【即时回复】 I/O 调用时的时间开销
  • Node.js 使用的是单线程模型
  • V8引擎提供的异步执行回调接口可以处理大量的高并发,IO密集型处理是强项
  • 对于所有 I/O 都采用异步请求,避免了频繁的上下文切换
  • node创建于JavaScript支持promise之前,其异步主要依赖回调函数实现
  • **事件式编程:在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式 I/O 请求完成后会被推送到事件队列,等待程序进程进行处理。
  • 同步的阻塞式IO在高并发环境将是一个很大的性能问题,所以同步一般只在基础框架启动时使用,用来加载配置文件、初始化程序等。
  • 其他同步的应用需求:和操作系统的shell命令交互,调用可执行文件等
  • Node与进程相关的模块process、child_process、cluster
  • 大多数API都有同步/异步两个版本

注意:Node.js在底层访问I/O其实还是多线程,可以翻看fs模块源码,里面用libuv来处理I/O

异步错误不能被同步线程捕获,有可能会变成致命错误,终止程序。注册全局处理程序

1
2
3
js复制代码process.on("unhandledRejection", (reason, promise) => {
//
});

多线程

在支持HTML5的浏览器里,我们可以使用web worker来处理一些耗时运算。
对应Node.js也提供了cluster、child_process模块的web worker来解决。

模块

  • node在JavaScript支持模块系统之前就诞生,所以它拥有自己的模块系统——module.exports 和 require
  • 模块是 Node.js 应用程序的基本组成部分,文件即模块。文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。
  • Node 13增加了对ES6模块import、export的支持,同时支持自己的模块系统。
  • mjs扩展名文件-ES6模块
  • cjs扩展名文件-CommonJS模块格式
  • 没有明确扩展名的,会向上查找package.json文件,检查JSON对象的顶级type属性的值commonjs、module
  • 运行node并不需要有package.json,如果没有找到默认CommonJS模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码//module.js 
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};

//getmodule.js
var myModule1 = require('./module');
myModule1.setName('1');
var myModule = require('./module');
myModule.setName('BYVoid');
myModule1.sayHello();

运行node getmodule.js,结果是:
Hello BYVoid

require 不会重复加载模块,也就是说无论调用多少次获得的模块都是最后一次。

封装一个对象到模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码//hello.js 
function Hello() {
var name;

this.setName = function(thyName) {
name = thyName;
};

this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;//如果是exports.Hello = Hello;

//gethello.js
var Hello = require('./hello'); //那么是require('./hello').Hello
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

http模块

http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。
node内建了 HTTP 服务器支持,这和 PHP、Perl 不一样,因为在使用 PHP 的时候,必须先搭建一个 Apache 之类的HTTP 服务器,然后通过 HTTP 服务器的模块加载或 CGI 调用,才能将 PHP 脚本的执行结果呈现给用户。
而使用 Node.js 时,无需额外搭建一个 HTTP 服务器。直接可以用来调试代码,并且它本身就可以部署到产品环境,其性能足以满足要求。

1
2
3
4
5
6
7
8
9
10
js复制代码function serve(rootDirectory, port) {
let server = new http.Server();
server.listen(port);
console.log('listening on port', port);

server.on('request', (request,response) => {
let temp = url.parse(request.url).pathname;
//查找文件,返回状态码及文件
})
}

包package

NPM

安装node同时会得到npm包管理工具

通常一个包的配置如下

  1. GZIP压缩文件
  2. 解析GZIP的URL
  3. 为注册表添加< name > @< version >的URL信息
  4. package.json配置文件(在包目录中运行npm init 会根据交互式问答产生一个符合标准的package.json)
  • package.json文件中使用script定义脚本命令(脚本文件一般位于node_modules/.bin子目录里)
  • Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作为包的接口。
1
2
3
4
5
6
7
8
9
10
css复制代码{
//...
"script":{
"build":"node index.js"
}
//...
"main" : "./lib/interface.js"
}

//npm run build等同于node index.js
  • 二进制文件应该在 bin 目录下;
  • JavaScript 代码应该在 lib 目录下;
  • 文档应该在 doc 目录下;
  • 单元测试应该在 test 目录下

工具模块

  • JavaScript 的单元测试可使用一个 Node.js 的 模 块,Karma 执行
  • JavaScript 没有编译器,不能在早期验证代码的合法性。有效的单元测试常常扮演一个伪编译器的角色,立刻反馈代码的质量,并且一有缺陷,就能检测到。
  • JavaScript 中有很多自动化文档生成工具。JSDoc有着和Javadoc 类似的标记和输出,Dox是一个生成文档的Node.js 模块。
    Docco将代码和注释组织成一种类似文章的格式。虽然Docco 不直接验证和执行代码规范,使用它却能鼓励大家使用良好的代码结构和注释

package.json

完全符合规范的 package.json 文件应该含有以下字段。

  • name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
  • description:包的简要说明。
  • version:符合语义化版本识别规范的版本字符串。
  • keywords:关键字数组,通常用于搜索。
  • maintainers:维护者数组,每个元素要包含 name、email(可选)、web(可选)
    字段
  • contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者
    数组的第一个元素。
  • bugs:提交bug的地址,可以是网址或者电子邮件地址。
  • licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到
    许可证文本的地址)字段。
  • repositories:仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、
    url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
  • dependencies:包的依赖,一个关联数组,由包名称和版本号组成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
javascript复制代码{ 
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}

使用npm安装包

在使用 npm 安装包的时候,有两种模式

  1. 本地模式:把目标包作为工程的一部分
    使用 npm 安装包的命令格式为:
1
js复制代码npm [install/i] [package_name]
  • 安装成功放置在当前目录的 node_modules 子目录下,require 在加载模块时会尝试搜寻 node_modules 子目录
  • npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中的 bin 目录下可执行文件没有包含在 PATH 环境变量中,不能直接在命令行中调用。
  1. 全局模式:全局使用,多个工程,支持在命令行下使用包
1
js复制代码npm [install/i] -g [package_name]
  • 使用全局模式安装时,npm 会将包安装到系统目录,譬如/usr/local/lib/node_modules/,同时 package.json 文件中 bin 字段包含的文件会被链接到 /usr/local/bin/
  • /usr/local/bin/ 是在PATH 环境变量中默认定义的,因此就可以直接在命令行中运行 xxx.js命令了。
  • 注:使用全局模式安装的包并不能直接在 JavaScript 文件中用 require 获得,因为 require 不会搜索 /usr/local/lib/node_modules/

相比npm、yarn有了很多改进:yarn new package/

  • 安装速度快(cache和依赖解析做得好)
  • 模块安装保证幂等性
  • 锁定各依赖模块的版本(npm 使用的semver默认是指定了一个range)
  • 兼容性好(兼容旧有npm的工作流)

npm link创建符号链接

npm 提供了一个命令 npm link,在本地包和全局包之间创建符号链接,解决-使用全局模式安装的包不能直接通过 require 使用、局部包不支持命令行

npm link 命令不支持 Windows。

1
2
3
4
js复制代码npm install -g express
npm link express
// 显示
./node_modules/express -> /usr/local/lib/node_modules/express

在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这种方法,就可以把全局包当本地包来使用了。

将本地的包链接到全局:

1
js复制代码在包目录( package.json 所在目录)中运行 npm link 命令。

开发包时,利用这种方法可以非常方便地在不同的工程间进行测试。

发布包

包目录下运行

1
2
3
4
js复制代码npm init :生成一个符合 npm 规范的 package.json 文件
npm adduser :获得一个账号用于今后维护自己的包
npm whoami :测验是否已经取得了账号
npm publish:发布

访问 search.npmjs.org/ 就可以找到刚刚发布的包了。可以在世界的任意一台计算机上使用 npm install byvoidmodule 命令来安装它
更新

1
2
js复制代码在 package.json 文件中修改 version 字段,然后npm publish
npm unpublish 命令来取消发布。

Runtime和vm

Runtime :解释型语言的数据类型的确定由编译推迟至运行时,所以需要一个运行时系统来处理编译后的代码。

JavaScript引擎负责解析和JIT编译,例如编译成机器码。Runtime提供内建的库(例如常用数据类型、Window对象、DOM API),可以在运行时使用。

  • vm:通常认为是硬件和二进制文件之间的中间层
  • c++编译好的二进制文件交给OS直接调用
  • Java编译好的二进制文件交给Java虚拟机运行,对开发者屏蔽了不同操作系统的差异
  • Node的一个核心模块vm提供了一系列API用于在V8虚拟机环境中编译和运行代码。

node.js中的stream流

基于EventEmitter的数据管理模式,由各种不同的抽象接口组成,主要包括可写、可读、可读写、可转换等类型

  • 流是非阻塞数据处理模式,可以提升效率、节省内存、有助于处理管道且可扩展等。
  • 流的应用:文件读写、网络请求、数据转换、音频、视频等方面有很广泛的应用
  • 监听error事件,可以捕获流的错误事件

node支持四种流

  1. 可读流
  2. 可写流
  3. 双工流
  4. 转换流

流常与管道共用

有时候,我们需要把可读流中获取的数据写入可写流。例如,写一个HTTP服务器提供对静态文件目录的访问。需要将文件流写入网络套接字。与其自己写代码来处理读和写,不如把这两个接口连接为一个“管道”,将可写流简单传递给可读流的pipe()方法,即可让node帮我们实现复杂操作

1
2
3
4
5
js复制代码const fs = require('fs');

function pipeFileToSocket(file, socket){
fs.createReadStream(file).pipe(socket);
}

使用

1
2
3
4
5
6
7
8
javascript复制代码//app.js 
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");

运行 node app.js命令,打开浏览器访问 http://127.0.0.1:3000
这个程序调用了 Node.js 提供的http 模块,对所有 HTTP 请求答复同样的内容并监听 3000 端口。在终端中运行这个脚本时,我们会发现它并不像 Hello World 一样结束后立即退出,而是一直等待,直到按下 Ctrl + C 才会结束。这是因为 listen 函数中创建了事件监听器,使得 Node.js 进程不会退出事件循环。

因为 Node.js 只有在第一次引用到某部份时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,开发 Node.js 实现的 HTTP 应用时会发现,无论修改了代码的哪一部份,都必须终止Node.js 再重新运行才会奏效。

1
2
3
4
5
js复制代码supervisor 可以监视你对代码的改动,并自动重启 Node.js。【相当于持续重启,有选择的使用】
首先使用 npm 安装 supervisor:
$ npm install -g supervisor
接下来,使用 supervisor 命令启动 app.js:
$ supervisor app.js

调试

node debug debug.js:打开了一个 Node.js 的调试终端
node-inspector

控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。

Mocha

一个基于Node.js和浏览器的集合各种特性的JavaScript测试框架

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%