Node.js
Node.js是什么?
浏览器端JS:
1)js核心(ECMAScript):描述了JS的语法和基本对象。
2)文档对象模型 (DOM):处理网页内容的方法和接口,document、createElement、getElementById、appendChild等
3)浏览器对象模型(BOM):与浏览器交互的方法和接口,window、location、navigator等
Node.js
1)js核心(ECMAScript)
2)各种类库:文件系统fs、path,网络net、http、url
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
Node.js是一个JS运行环境(解释器),同时在这个环境中提供了一些内置库。
chrome中用js来控制浏览器,Node.js用来控制整个计算机。
在 Node.js 中,可以控制运行环境,因为代码只跑在Server的固定环境中,但浏览器中的js环境各异,兼容性问题多。
CommonJS规范
以前通过script标签引入多个JS,存在无法处理模块依赖的问题,
解决办法:
1)手工维护js顺序
2)不同脚本间调用使用全局变量
Node.js没有script标签,该怎么引入别的js文件?
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports,exports和module.exports是同一个引用)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
// index.js
console.log('start lib');
var lib = require('./lib');
console.log(lib);
console.log('end lib');
// lib.js
console.log(module)
console.log('module.exports === exports', module.exports === exports)
执行node index.js后输出为:
start lib
Module {
id: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\lib.js',
path: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src',
exports: {},
filename: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\lib.js',
loaded: false,
children: [],
paths: [
'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\node_modules',
'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\node_modules',
'D:\\yangzongjun\\temp\\node\\share\\node_modules',
'D:\\yangzongjun\\temp\\node\\node_modules',
'D:\\yangzongjun\\temp\\node_modules',
'D:\\yangzongjun\\node_modules',
'D:\\node_modules'
]
}
module.exports === exports true
{}
end lib
lib.js中可以通过下面3中方式对外暴露方法。常见误区:直接改写exports引用,最后一种不行,因为修改了exports的引用,但module.exports还是指向之前的内存地址。由于模块最终导出的是module.exports,所以修改exports引用不行
module.exports.add = (a, b) => a + b;
exports.add = (a, b) => a + b;
module.exports = (a, b) => a + b
exports = (a, b) => a + b // x
再看一下webpack是如何模拟实现ComonJS的:npx webpack --mode development --devtool inline-source-map,打包后dist产出为:
/******/ (() => {
// webpackBootstrap
var __webpack_modules__ = {
/***/ './src/lib.js':
/*!********************!*\
!*** ./src/lib.js ***!
\********************/
/***/ (module, exports, __webpack_require__) => {
/* module decorator */ module = __webpack_require__.nmd(module);
// exports = function () {
// console.log('foo')
// }
exports = (a, b) => a + b;
console.log(module);
console.log('module.exports === exports', module.exports === exports);
/***/
},
};
/************************************************************************/
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
__webpack_module_cache__[moduleId] = {
id: moduleId,
loaded: false,
exports: {},
};
var module = __webpack_module_cache__[moduleId]
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Flag the module as loaded
module.loaded = true;
// Return the exports of the module
return module.exports;
}
/************************************************************************/
/* webpack/runtime/node module decorator */
(() => {
__webpack_require__.nmd = (module) => {
module.paths = [];
if (!module.children) module.children = [];
return module;
};
})();
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
console.log('start lib');
var lib = __webpack_require__(/*! ./lib */ './src/lib.js');
console.log(lib);
// lib.add(1, 2)
console.log('end lib');
// npx webpack --mode development --devtool inline-source-map
})();
/******/
})();
可以简化为如下demo:
function foo (module, exports) {
// xxx
exports = {'key': 1}
return module.exports;
}
const testModule = {
exports: {}
}
const requredFoo = foo(testModule, testModule.exports)
console.log(testModule) // 输出的仍旧是 {}
命令行工具 –内置API初探
-
如何执行:
方式一:
node filename.js方式二:文件第一行加上shebang
#!/usr/bin/env node执行./filename.js -
全局变量:
module、module.exports、require
__dirname 当前运行的脚本所在目录,注意和运行目录无关,无论在哪里运行,此地址都是固定的,是脚本存放目录
__filename 当前运行的脚本文件地址+文件名
process Node所处的当前进程
global 全局环境 类型浏览器的window
setTimeout、setInterval、setImediate…
重点关注
process.argv,是个数组,前两个参数分别表示node解释器地址、入口文件地址,后面才是用户传入的参数。// index.js console.log('process', process.argv) $ node index.js process [ 'C:\\Program Files\\nodejs\\node.exe', 'D:\\yangzongjun\\temp\\node\\share\\02_apis\\index.js' ] $ node index.js key=1 isInit process [ 'C:\\Program Files\\nodejs\\node.exe', 'D:\\yangzongjun\\temp\\node\\share\\02_apis\\index.js', 'key=1', 'isInit' ] -
模块:
path
fs
child_process
http
net
…
demo演示:
const fs = require('fs') const path = require('path') console.log(path.join('foo', 'bar')) console.log(path.join('/foo', 'bar')) console.log(path.resolve('foo', 'bar')) console.log(path.resolve('/foo', 'bar')) // 读取文件的异步、同步方法: fs.readFile('index.js', 'utf-8', (err, data) => { if (err) { console.log(err); return; } console.log(data) }) console.log(fs.readFileSync('index.js', 'utf-8')) // 写文件 fs.writeFileSync('write.json', '{"key":1}')递归读取文件:
const fs = require('fs') const path = require('path') function getAllFiles(baseDir) { let res = []; const files = fs.readdirSync(baseDir); files.forEach(filePath => { const fullPath = path.join(baseDir, filePath); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { res = res.concat(getAllFiles(fullPath)); } else { res.push(fullPath) } }); return res; } console.log(getAllFiles('/mnt/d/yangzongjun/temp/node/share'))child_process 可以新起一个进程来执行shell脚本:
const {execSync} = require('child_process') console.log(execSync('ls', {encoding: 'utf-8'}))
异步
同步的方式更直观,更符合直觉,为啥还需要异步回调?
// 读取文件的异步、同步方法:
fs.readFile('index.js', 'utf-8', (err, data) => {
if (err) {
console.log(err);
return;
}
console.log(data)
})
console.log(fs.readFileSync('index.js', 'utf-8'))
I/O是昂贵的:网络请求、磁盘、数据库读写。
下面的demo中花费时间分别为:
同步:M + N + ...
异步:max(M, N, ...)cpu不会因为io而阻塞
Node社区有言:
In node everything runs in parallel, except your code. 除了用户代码无法并行执行外,所有的I/O(磁盘I/O和网络I/O等)则是可以并行起来的。
如何理解这句话?
先来看一个Node的内置api:
// index.js
const os = require('os')
console.log('os.cpus()', os.cpus())
// node index.js
os.cpus() [
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 32661796,
nice: 0,
sys: 31846515,
idle: 848752187,
irq: 3762812
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 44414375,
nice: 0,
sys: 39157203,
idle: 829688593,
irq: 453015
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 37623203,
nice: 0,
sys: 27436328,
idle: 848200640,
irq: 312625
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 39422125,
nice: 0,
sys: 26997656,
idle: 846840390,
irq: 246250
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 31362234,
nice: 0,
sys: 21248750,
idle: 860649187,
irq: 221843
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 46002531,
nice: 0,
sys: 28078234,
idle: 839179406,
irq: 285781
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 56157078,
nice: 0,
sys: 38666328,
idle: 818436765,
irq: 235015
}
},
{
model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
speed: 3000,
times: {
user: 77564515,
nice: 0,
sys: 49278562,
idle: 786417078,
irq: 204968
}
}
]
那Node是如何读取到CPU信息的呢,在浏览器端js是没有这个能力的,Node让js能够与操作系统进行交互,机制在于js会通过Node.js bindings的一种桥来调用C++(libuv)代码,最终C++执行结果通过异步回调方式调用js callback。
Node github源码可以看到:github.com/nodejs/node…
Node调用os.cups()调用的是getCPUs方法,这方法是在这里桥接,将getCPUs方法绑定到C++的GetCPUInfo方法,C++执行可以是异步的,像是fs.readFile等api,得到结果后调用js的callback。
至此,我们知道Node的内置api多是通过:js代码调用C++代码,C++再通过callback异步回调js
同步写法时,execSync会阻塞后面的js执行,只有等待execSync这个io操作执行完毕才会执行后面的js:
const {exec, execSync} = require('child_process')
console.time('fs')
execSync('find /d/yangzongjun/temp', {encoding: 'utf-8', maxBuffer: 2000000000})
console.log('execSync done')
console.log(1 + 1)
console.timeEnd('fs')
但异步写法时,exec执行后,js调用到C++代码,C++代码异步执行,js会接着执行下面的代码,很快打印出2,等到C++的异步操作完成后才会进入callback函数
const {exec, execSync} = require('child_process')
console.time('fs')
exec('find /d/yangzongjun/temp/', {encoding: 'utf-8', maxBuffer: 2000000000}, (err, data) => {
if (err) {
console.error('err: ', err)
return;
}
console.log('exec callback done')
})
console.log(1 + 1)
console.timeEnd('fs')
所以说,用户写的js都是同步的,但调用Node.js内置api可以是异步的。
但异步回调有个问题:如果C++代码出错了,改如何通知js?直接throw new Error()?答案是不行
(() => {
function readFile(callback) {
const res = 1 + 1;
setTimeout(() => {
// callback(new Error('error'))
throw new Error('error');
}, 500);
}
try {
readFile(function callback(data) {
console.log('callback done', data)
});
} catch (error) {
console.log('catched error', error) // catch不了,因为不在一个事件循环
}
})()
原因在于try-catch不能捕获非同一事件循环的error,于是Node.js提出了error-first风格的callback函数
(() => {
function readFile(callback) {
const res = 1 + 1;
setTimeout(() => {
callback(new Error('error'))
}, 500);
}
readFile(function callback(err, data) {
if (err) {
console.log('error', err);
return;
}
console.log('callback done', data)
});
})()
也可以通过适配为Promise,实现更直观的同步写法
(() => {
function readFile() {
return new Promise((resolve, reject) => {
const res = 1 + 1;
setTimeout(() => {
if (Math.random() < 0.5) {
reject('err')
} else {
resolve(res)
}
}, 500);
})
}
readFile()
.then(data => {
console.log('catched done', data)
})
.catch (error=> {
console.log('catched error', error)
})
// 利用Promise,结合async、await可以实现更符合程序员直觉的同步写法
Promise.all([readFile(), readFile()])
.then(data => {
console.log('Promise.all catched done', data)
})
.catch (error=> {
console.log('Promise.all catched error', error)
})
})()
作为webServer
应用场景:
1)作为BFF层,在服务端渲染好html,提高首屏渲染速度、SEO友好。 腾讯视频等内容型网站。注意和SPA区别
2)前后端同构网站,后台也全部用node.js。优势在于代码复用,比如rest接口中的某个status状态,编辑中、已编辑,id=1/2,不需要前后端同时定义2份枚举。mongodb+ORM
3)其它场景如爬虫等,demo 参考
由于js是单线程的,如果代码抛异常了会导致整个服务挂掉,这是非常严重的问题,所以在生产环境社区也有解决方案,forever或pm2进行进程守护,当Node.js进程挂了会重启。但我们应当把此工具作为最后一道屏障,尽量不让代码抛异常,否则频繁启动node进程对性能损耗很大。
pm2: pm2 start/stop index.js pm2 ls
// index.js
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
const pathname = url.parse(req.url).pathname;
if (pathname === '/') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Home page! \n');
}
if (pathname === '/list') {
console.log(a.b)
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('list page! \n');
}
});
server.listen(port, hostname, () => {
console.log(`server listening at: http://${hostname}:${port}/`);
});
// 如果直接执行 node index.js,访问 /list 会挂掉,可通过 pm2 start index.js
学习资源
- nodejs.cn入门教程: nodejs.cn/learn/diffe…
- Node.js 入门: cnodejs.org/getstart
- 阮一峰:javascript.ruanyifeng.com/nodejs/path…