web 前端培训:Node 的重新认识
最初做 Node 的目的是什么?
Node 作者 Ryan Dahl:
基于 V8 创建一个轻量级的高性能 Web 服务器并提供一套库
为什么是 JavaScript?
Ryan Dahl 是一名资深的 C/C++程序员,创造出 Node 之前主要工作是围绕 Web 高性能服务器进行的
他发现 Web 高性能服务器的两个要点:
事件驱动
非阻塞 I / O
Ryan Dahl 也曾评估过使用 C、Lua、Haskell、Ruby 等语言作为备选实现,得出以下结论:
C 的开发门槛高,可以预见不会有太多的开发者能将它用于业务开发
Ryan Dahl 觉得自己还不足够玩转 Haskell,所以舍弃它
Lua 自身已经含有很多阻塞 I / O 库,为其构建非阻塞 I / O 库不能改变开发者使用习惯
Ruby 的虚拟机性能不佳
JavaScript 的优势:
开发门槛低
在后端领域没有历史包袱
第二次浏览器大战渐渐分出高下,Chrome 浏览器的 JavaScript 引擎 V8 摘得性能第一的桂冠
Node 给 JavaScript 带来的意义
除了 HTML、Webkit 和显卡这些 UI 相关技术没有支持外,Node 的结构与 Chrome 十分相似。他们都是基于事件驱动的异步架构:
浏览器通过事件驱动来服务界面上的交互
Node 通过事件驱动来服务 I / O
在 Node 中,JavaScript 还被赋予了新的能力:
随心所欲地访问本地文件
搭建 WebSocket 服务端
连接数据库,进行业务研发
像 Web Worker 一样玩转多进程
Node 使 JavaScript 可以运行在不同的地方,不再限制在浏览器中、DOM 树打交道。如果 HTTP 协议是水平面,Node 就是浏览器在协议栈另一边的倒影。
Node 不处理 UI,但用与浏览器相同的机制和原理运行,打破了 JavaScript 只能在浏览器中运行的局面。前后端编程环境统一,可以大大降低前后端转换所需要的上下文代价。
Node 的特点
异步 I / O
以读取文件为例
var fs = require('fs');
fs.readFile('/path', function (err, file) {
console.log('读取文件完成')
});
console.log('发起读取文件');
熟悉的用户必知道,“读取文件完成”是在“发起读取文件”之后输出的
fs.readFile 后的代码是被立即执行的,而“读取文件完成”的执行时间是不被预期的
只知道它将在这个异步操作后执行,但并不知道具体的时间点
异步调用中对于结果值的捕获是符合“Don't call me, I will call you”原则的
这也是注重结果,不关心过程的一种表现
Node 中,绝大多数操作都以异步的方式进行调用,Ryan Dahl 排除万难,在底层构建了很多异步 I / O 的 API,从文件读取到网络请求等。使开发者很已从语言层面很自然地进行并行 I / O 操作,在每个调用之间无需等待之前的 I / O 调用结束,在编程模型上可以极大提升效率_前端培训
事件与回调函数
「事件」
随着 Web2.0 的到来,JavaScript 在前端担任了更多的职责,时间也得到了广泛的应用。将前端浏览器中广泛应用且成熟的事件与回到函数引入后端,配合异步 I / O ,可以很好地将事件发生的时间点暴露给业务逻辑。
服务端例子
对于服务器绑定了 request 事件
对于请求对象,绑定了 data 和 end 事件
var http = require('http');
var querystring = require('querystring');
// 侦听服务器的 request 事件
http.createServer(function (req, res) {
var postData = '';
req.setEncoding('utf8');
// 侦听请求的 data 事件
req.on('data', function (trunk) {
postData += trunk;
});
// 侦听请求的 end 事件
req.on('end', function () {
res.end(postData);
});
}).listen(8080);
console.log('服务器启动完成');
前端例子
发出请求后,只需关心请求成功时执行相应的业务逻辑即可
request({
url: '/url',
method: 'POST',
data: {},
success: function (data) {
// success 事件
}
});
事件的编程方式具有轻量级、松耦合、只关注事务点等优势,但是在多个异步任务的场景下,事件与事件之间各自独立,如何协作是一个问题,后续也出现了一系列异步编程解决方案:
事件发布/订阅模式
Promise、async / await
流程控制库
「回调函数」
Node 除了异步和事件外,回调函数也是一大特色
纵观下来,回调函数也是最好的接收异步调用返回数据的方式 但是这种编程方式对于很多习惯同步思路编程的人来说,也许是十分不习惯的 代码的编写顺序与执行顺序并无关系,这对他们可能造成阅读上的障碍
在流程控制方面,因为穿插了异步方法和回调函数,与常规的同步方式相比变得不那么一目了然了 转变为异步编程思维后,通过对业务的划分和对事件的提炼,在流程控制方面处理业务的复杂度是与同步方式实际上是一致的
单线程
Node 保持了 JavaScript 在浏览器中单线程的特点
JavaScript 与其他线程是无法共享任何状态的,最大的好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销
单线程的缺点 无法利用多核 CPU 错误会引起整个应用退出,健壮性较差 大量计算占用 CPU 导致无法继续调用异步 I / O
后续也推出了 child_process 和 cluster 模块较好地缓解了以上缺点
跨平台
起初 Node 只能在 Linux 平台上运行,如果想在 Windows 平台上学习和使用 Node,则必须通过 Cygwin / MinGW,后微软投入通过基于 libuv 实现跨平台架构
libuv
在操作系统与 Node 上层模块系统之间构建了一层平台架构
通过良好的架构,Node 的第三方 C++模块也可以借助 libuv 实现跨平台
Node 模块机制 - CommonJS
背景:
在其他高级语言中,Java 有类文件,Python 有 import 机制,Ruby 有 require,PHP 有 include 和 require。而 JavaScript 通过 script 标签引入代码的方式显得杂乱无章,。人们不得不用命名空间等方式人为地约束代码,以达到安全和易用的目的。
直到后来出现了 CommonJS...
愿景
希望 JavaScript 能在任何地方运行
出发点
对于 JavaScript 自身而言,它的规范依然是薄弱的,还有以下缺陷:
没有模块系统
标准库较少 ECMAScript 仅定义了部分核心库 对于文件系统 I / O 流等常见需求没有标准 API
没有标准接口 在 JavaScript 中,几乎没有定义过如 Web 服务器或者数据库之类的标准统一接口
缺乏包管理系统 导致 JavaScript 应用中基本没有自动加载和安装以来的能力
CommonJS 的提出,主要是为了弥补当前 JavaScript 没有标准的缺陷,以达到像 Python、Ruby 和 Java 具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段,希望可以利用 JavaScript 开发:
服务端 JavaScript 程序
命令行工具
桌面图形界面应用程序
混合应用
CommonJS 规范涵盖了:
模块
二进制
Buffer
字符集编码
I / O 流
进程环境
文件系统
套接字
单元测试
Web 服务器网关接口
包管理
Node 与浏览器以及 W3C 组织、CommonJS 组织、ECMAScript 之间的关系,共同构成了一个繁荣的生态系统
模块规范
模块定义
上下文提供了 exports 对象用于导出当前模块的方法或者变量,并且它是导出的唯一出口
在模块中,还存在一个 module 对象,它代表模块自身,而 exports 是 module 的属性
在 Node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性即可定义导出的方式
// math.js
exports.add = function(a, b){
return a + b;
}
模块引用
const math = require('./math');
const res = math.add(1, 1);
console.log(res);
// 2
在 CommonJS 规范中,存在 require 方法,这个方法接受模块标识,以此引入一个模块的 API 到当前上下文中
模块标识
模块标识就是传递给 require 方法的参数,可以是:
如何小驼峰命名的字符串
以./ 、../ 开头的相对路径 or 绝对路径
可以没有文件名后缀.js
模块的定义十分简单,接口也十分简洁
每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落
意义:
将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖
模块实现
在 Node 引入模块,需要经历以下三个步骤
路径分析
文件定位
编译执行
Node 中模块分为两类:
核心模块
编译过程中,编译进了二进制执行文件
在 Node 进程启动时,部分核心模块就直接被加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略,并且在路径分析中优先判断,所以它的加载速度是最快的。
用户编写的文件模块
运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢
优先从缓存加载
与浏览器会缓存静态脚本文件以提高性能一样,Node 对引入过的模块都会进行二次缓存,以减少二次引入时的开销。不同点在于:
浏览器仅缓存文件
Node 缓存的是编译和执行之后的对象
无论核心模块还是文件模块,require 方法对相同模块的二次加载都一律采用缓存优先的方式
路径分析和文件定位
「标识符分析(路径)」
前面说到过,require 方法接受一个参数作为标识符,分为以下几类:
核心模块
优先级仅次于缓存加载,在 Node 的源代码编译过程中已编译为二进制代码,加载过程最快
「注:加载一个与核心模块标识符相同的自定义模块是不会成功的,只能通过选择不同的标识符 / 换用路径的方式实现」
路径形式的文件模块
以 ./ 、../ 开头的标识符都被当做文件模块处理
require 方法会将路径转为真实路径,并以真实路径为索引,将编译执行后的结果存放到缓存中,以使二次加载更快
文件模块给 Node 指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度仅慢于核心模块
自定义模块
是一种特殊的文件模块,是一个文件或者包的形式
这类模块的查找是最费时的,也是最慢的一种
先介绍一下模块路径这个概念,也是定位文件模块时制定的查找策略,具体表现为一个路径组成的数组
console.log(module.path)
你可以得到一个路径数组
['/home/bytedance/reasearch/node_modules',
'/home/bytedance/node_modules',
'home/node_module', /node_modules']
可以看出规则如下:
当前文件目录下的 node_modules 目录
父目录下的 node_modules 目录
父目录的父目录下的 node_modules 目录
沿路径向上逐级递归,直到根目录下的 node_modules 目录
它的生成方式与 JavaScript 原型链 / 作用域链的查找方式十分类似
在加载过程中,Node 会逐个尝试模块路径中的路径,直到找到目标文件
文件路径越深,模块查找耗时会越多,这是自定义模块的加载速度最慢的原因
「文件定位」
文件扩展名分析
require 分析标识符会出现不包含文件扩展名的情况
会按.js、.json、.node 的次序补足扩展名,一次尝试
过程中,需调用 fs 模块同步阻塞地判断文件是否存在,Node 单线程因此会引起性能问题
如果是.node / .json 文件带上扩展名能加快点速度,配合缓存机制,可大幅缓解 Node 单线程阻塞调用的缺陷
目录分析和包
分析标识符的过程中可能没有找到文件,却得到一个目录,则会将目录当做一个包来处理
通过解析 package.json 文件对应该包的 main 属性指定的文件名
如果 main 相应文件解析错误 / 没有 package.json 文件,node 会将 index 作为文件名
一次查找 index.js index.json index.node
该目录没有定位成功则进行下一个模块路径进行查找
直到模块路径数组都被遍历完依然没有查找到目标文件则抛出异常
模块编译
在 Node 中,每个文件模块都是一个对象
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
js 文件 通过 fs 模块同步读取文件后编译执行
node 文件 这是用 C/C++编写的扩展文件,通过 dlopen 方法加在最后编译生成的文件
json 文件 通过 fs 模块同步读取文件后,JSON.parse 解析返回的结果
其他 都被当作 js 文件载入
每一个编译成功的模块都会将其文件路径作为索引存在 Module.cache 对象上,以提高二次引入的性能_web前端培训
包与 NPM
Node 组织了自身核心模块,也使得第三方文件模块可以有序地编写和使用
但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用
而在模块之外,包和 NPM 则是将模块联系起来的一种机制
一定程度上解决了变量依赖、依赖关系等代码组织性问题
包结构
包实际上是一个存档文件,即一个目录直接打包为一个.zip/tar.gz 格式的文件,安装后解压还原为目录
符合 CommonJS 规范的包目录应该包含如下文件 package.json 包描述文件 bin 用于存放可执行二进制文件 lib 用于存放 JavaScript 代码的目录 doc 用于存放文档的目录 test 用于存放单元测试用例的代码
包描述文件
package.json
CommonJS 为 package.json 定义了如下一些必须的字段
name 包名
description 包简介
version 版本号
keywords 关键词数组,用于做 npm 搜索
maintainers 包维护者列表
contributors 贡献者列表
bugs 一个可以反馈 bug 的网页地址 / 邮件地址
licenses 许可证列表
repositories 托管源代码的位置列表
dependencies 使用当前包所需要依赖的包
homepage 当前包的网站地址
os 操作系统支持列表 aix、freebsd、linux、macos、solaris、vxworks、windows
cpu CPU 架构的支持列表 arm、mips、ppc、sparc、x86、x86_64
builtin 标志当前包是否是内建在底层系统的标准组件
implements 实现规范的列表
scripts 脚本说明对象
包规范的定义可以帮助 Node 解决依赖包安装的问题,而 NPM 正是基于该规范进行了实现
NPM 常用功能
CommonJS 包规范是理论,NPM 是其中一种实践
NPM 于 Node,相当于 gem 于 Ruby,pear 于 PHP
帮助完成了第三方模块的发布、安装和依赖等
查看帮助
查看版本 npm -v
查看命令 npm
安装依赖包
npm install {packageName}
执行该命令后,NPM 会在当前目录下创建 node_modules 目录下创建包目录,接着将相应的包解压到这个目录下
全局安装模式
npm install {packageName} -g
全局模式并不是将一个模块包安装为一个全局包的意思,它并不意味着可以从任何地方 reuqire 它
全局模式这个称谓并不精确,-g 实际上是将一个包安装为全局可用的执行命令
它根据包描述文件中的 bin 字段配置,将实际脚本链接到与 Node 可执行文件相同的路径下
从本地安装
对于一些没有发布到 NPM 上的包,或者因为网络原因无法直接安装的包
可以通过将包下载到本地,然后本地安装
npm install <tarball file>
npm install <tarball url>
npm install folder>
从非官方源安装
如果不能通过官方源安装,可以通过镜像源安装
npm install --registry={urlResource}
如果使用过程中几乎全使用镜像源,可以指定默认源
npm config set registry {urlResource}
NPM 钩子命令
package.json 中 scripts 字段的提出就是让包在安装或者卸载等过程中提供钩子机制
"scripts":{
"preinstall": "preinstall.js",
"install": "install.js",
"uninstall": "uninstall.js",
"test": "test.js",
}
Install 在以上字段执行 npm install <package>时,preinstall 指向的脚本会被加载执行,然后 install 指向的脚本会被执行
Uninstall 执行 npm uninstall <package>时,uninstall 指向的脚本也许会做一些清理工作
Test 执行 npm test 将会运行 test 指向的脚本,一个优秀的包应当包含测试用例,并在 package.json 文件正配置好运行测试的命令,方便用户运行测试用例,以便检验包是否稳定可靠
局域 NPM
背景
企业的限制在于,一方面需要享受到模块开发带来的低耦合和项目组织上的好处,另一方面却要考虑模块保密性的问题。所以,通过 NPM 共享和发布存在潜在的风险。
解决方案
为了同时能够享受到 NPM 上众多的包,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的 NPM 仓库,NPM 无论是它的服务端和客户端都是开源的。
局域 NPM 仓库的搭建方法与搭建镜像站的方式几乎一样,与镜像仓库不同的地方在于可以选择不同步官方源仓库中的包
作用 私有的可重用模块可以打包到局域 NPM 仓库中,这样可以保持更新的中心化,不至于让各个小项目维护相同功能的模块 杜绝通过复制粘贴实现代码共享的行为
异步 I / O
为什么需要异步 I / O ?
用户体验
浏览器中 JavaScript 在单线程上执行,还和 UI 渲染共用一个线程,如果脚本执行的时间超过 100ms 用户就会感到页面卡顿
如果网页临时需要获取一个网络资源,通过同步的方式获取,JS 需要等资源完全从服务器获取后才能继续执行,这期间 UI 将停顿,不响应用户的交互行为。可以想象,这样的用户体验将会多差。
而采用异步请求,JavaScript 和 UI 的执行都不会处于等待状态,给用户一个鲜活的页面
I / O 是昂贵的,分布式 I / O 是更昂贵的
只有后端能够快速响应资源,才能让前端体验变好
资源分配
计算机在发展过程中将组件进行了抽象,分为了 I / O 设备和计算设备
假设业务场景有一组互不相关的任务需要完成,主流方法有两种:
1.多线程并行完成
多线程的代价在于创建线程和执行线程上下文切换的开销较大。
在复杂的业务中经常面临锁、状态同步等问题。但是多线程在多核 CPU 上能够有效提升 CPU 利用率
2.单线程串行依次执行
单线程顺序执行任务比较符合编程人员按顺序思考的思维方式,依然是主流的编程方式
串行执行的缺点在于性能,任意一个略慢的任务都会导致后续执行代码被阻塞
在计算机资源中,通常 I / O 与 CPU 计算是可以并行的,同步编程模型导致的问题是,I / O 的进行会让后续任务等待,这造成资源不能更好地被利用
Node 在两者之间给出了它的答案
利用单线程,远离多线程死锁、状态同步等问题;
利用异步 I / O,让单线程可以远离阻塞,更好地使用 CPU
为了弥补单线程无法利用多核 CPU 的缺点,Node 提供了类似前端浏览器中 Web Workers 的子进程,该子进程可以通过工作进程高效地利用 CPU 和 I / O
异步 I / O 的提出是期望 I / O 的调用不再阻塞后续运算,将原有等待 I / O 完成的这段时间分配给其余需要的业务去执行
异步 I / O 现状
异步 I / O 与非阻塞 I / O
操作系统内核对于 I / O 方式只有两种:阻塞与非阻塞
在调用阻塞 I / O 时,应用程序需要等待 I / O 完成才返回结果
特点:调用之后一定要等到系统内核层面完成所有操作后调用才结束
例子:系统内核在完成磁盘寻道、读取数据、复制数据到内幕才能中之后,这个调用才结束
非阻塞 I / O 与阻塞 I / O 的差别为调用之后会立即返回
非阻塞 I / O 返回之后,CPU 的时间片可以用来处理其他事务,此时的性能提升是明显的
存在的问题:
由于完整的 I / O 没有完成,立即返回的并不是业务层期望的数据而仅仅是当前调用的状态
为了获取完整的数据,应用程序需要重复调用 I / O 操作来确认是否完成,称之为“轮询”。
主要的轮询技术
read
它是最原始、性能最低的一种,通过重复调用检查 I / O 的状态来完成数据的完整读取
在得到最终数据前,CPU 一直耗用在等待上
select
它是在 read 的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断
限制:它采用一个 1024 长度的数组来存储状态,最多可以同时检查 1024 个文件描述符
poll
较 select 有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查
文件描述符较多时,它的性能还是十分低下的
epoll
该方案是 Linux 下效率最高的 I / O 事件通知机制,在进入轮询的时候如果没有检查到 I / O 事件,将会进行休眠,直到事件将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费 CPU,执行效率较高
理想的非阻塞异步 I / O
尽管 epoll 已经利用了时间来降低 CPU 的耗用,但是休眠期间 CPU 几乎是限制的,对于当前线程而言利用率不够
完美的异步 I / O 应该是应用程序发起非阻塞调用,无需通过遍历或者时间唤醒等方式轮询
可以直接处理下一个任务,只需在 I / O 完成后通过信号或回调将数据传递给应用程序即可
Linux 下存在原生提供的一种异步 I / O 方式(AIO)就是通过信号或者回调来传递数据的
缺点:
仅 Linux 下有
仅支持 I / O 中的 O_DIRECT 方式读取,导致无法利用系统缓存
现实的异步 I / O
通过让部分线程进行阻塞 I / O 或者非阻塞 I / O 加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将 I / O 得到的数据进行传递,这就轻松实现了异步 I / O
libeio 实质上是采用线程池与阻塞 I / O 模拟异步 I / O
Node 最初在*nix 平台下采用 libeio 配合 libev 实现异步 I / O,后通过自行实现线程池完成
Windows 下的 IOCP 调用异步方法,等待 I / O 完成之后的通知,执行回调,用户无需考虑轮询 内部其实仍是线程池的原理,不同之处在于这些线程池由系统内核接手管理 与 Node 异步调用模型十分近似
由于 Windows 平台和*nix 平台的差异,Node 提供了 libuv 作为抽象封装层,做兼容性判断 保证上层 Node 与下层的自定义线程池和 IOCP 各自独立
我们时常提到 Node 是单线程的 这里的单线程仅仅只是 JavaScript 执行在单线程中罢了 无论是*nix 还是 Windows 平台,内部完成 I / O 任务的另有线程池
Node 的异步 I / O
Node 完成整个异步 I / O 环节的有事件循环、观察者和请求对象等
事件循环
着重强调一下 Node 自身的执行模型——事件循环
Node 进程启动时,会创建一个类似 while(true)的循环
每次循环体的过程称之为 Tick,每个 Tick 的过程就是查看是否有事件待处理
如果有就取出事件及其相关的回调函数,并执行它们
观察者
每个事件循环中有一个或多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件
浏览器采用了类似的机制 事件可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者
Node 中事件主要来源于网络请求、文件 I / O 等 这些时间对应的观察者有文件 I / O 观察者、网络 I / O 观察者等,将事件进行了分类
事件循环是一个典型的生产者 / 消费者模型 异步 I / O、网络请求等则是事件的生产者 这些事件被传递到对应的观察者,事件循环则从观察者那取出事件并处理
小结
事件循环、观察者、请求对象、I / O 线程池这四者共同构成了 NOde 异步 I / O 模型的基本要素
由于我们知道 JavaScipt 是单线程的,所以按尝试很容易理解它不能充分利用多核 CPU
事实上在 Node 中,除了 JavaScript 是单线程外,Node 自身其实是多喜爱昵称的,只是 I / O 线程使用的 CPU 较少
另一个需要注意的点是,除了用户代码无法并行执行以外,所有的 I / O 是可以并行执行的
注:图为 Node 整个异步 I / O 过程
事件驱动与高性能服务器
前面对异步的讲解,也基本勾勒出了事件驱动的实质,即通过主循环加事件触发的方式来运行程序
下面为几种经典的服务器模型:
同步式 一次只能处理一个请求,并且其余请求都处于等待状态
进程 / 请求 这样可以处理多个请求,但是它不具备扩展性,因为系统资源只有那么多
线程 / 请求 尽管线程比进程要清凉,但是由于每个线程都占用一定内存,当大并发请求到来时,内存将会很快用光,导致服务器缓慢 比进程 / 请求要好,但对于大型站点而言依然不够
总结 这使得服务器能够有条不紊地处理请求,即使在大量连接的情况下,也不受上下文切换开销的影响,这也是 Node 高性能的一个原因 线程 / 请求的方式目前还被 Apache 所采用 Node 通过事件驱动的方式处理请求,无需为每一个请求创建额外的线程,可以省掉创建线程和销毁线程的开销 同时操作系统在调度任务时因为线程较少,上下文的代价很低
事件驱动带来的高效已经渐渐开始为业界所重视
知名服务器 Nginx 也摒弃了多线程的方式,采用和 Node 相同的事件驱动
不同之处在于 Nginx 采用纯 C 写成,性能较高,但是它仅适合于做 Web 服务器,用于反向代理或者负载均衡服务,在业务处理方面较为欠缺
Node 则是一套高性能平台,可以利用它构建与 Nginx 相同的功能,也可以处理各种具体业务
Node 没有 Nginx 在 Web 服务器方面那么专业,但场景更大,自身性能也不错
在实际项目中可以结合它们各自的优点以达到应用的最优性能
JavaScript 在服务器端近乎空白,使得 Node 没有任何历史包袱,而 Node 在性能优化上的表现使得它一下子就在社区中流行了起来~
文章来源于全栈成长之路
评论