Node.js的進(jìn)程管理的深入理解
眾所周知Node基于V8,而在V8中JavaScript是單線程運行的,這里的單線程不是指Node啟動的時候就只有一個線程,而是說運行JavaScript代碼是在單線程上,Node還有其他線程,比如進(jìn)行異步IO操作的IO線程。這種單線程模型帶來的好處就是系統(tǒng)調(diào)度過程中不會頻繁進(jìn)行上下文切換,提升了單核CPU的利用率。
但是這種做法有個缺陷,就是我們無法利用服務(wù)器CPU多核的性能,一個Node進(jìn)程只能利用一個CPU。而且單線程模式下一旦代碼崩潰就是整個程序崩潰。通常解決方案就是使用Node的cluster模塊,通過master-worker模式啟用多個進(jìn)程實例。下面我們詳細(xì)講述下,Node如何使用多進(jìn)程模型利用多核CPU,以及自帶的cluster模塊具體的工作原理。
如何創(chuàng)建子進(jìn)程
node提供了child_process模塊用來進(jìn)行子進(jìn)程的創(chuàng)建,該模塊一共有四個方法用來創(chuàng)建子進(jìn)程。
const { spawn, exec, execFile, fork } = require('child_process')
spawn(command[, args][, options])
exec(command[, options][, callback])
execFile(file[, args][, options][, callback])
fork(modulePath[, args][, options])
spawn
首先認(rèn)識一下spawn方法,下面是Node文檔的官方實例。
const { spawn } = require('child_process');
const child = spawn('ls', ['-lh', '/home']);
child.on('close', (code) => {
console.log(`子進(jìn)程退出碼:$[code]`);
});
const { stdin, stdout, stderr } = child
stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
通過spawn創(chuàng)建的子進(jìn)程,繼承自EventEmitter,所以可以在上面進(jìn)行事件(discount,error,close,message)的監(jiān)聽。同時子進(jìn)程具有三個輸入輸出流:stdin、stdout、stderr,通過這三個流,可以實時獲取子進(jìn)程的輸入輸出和錯誤信息。
這個方法的最終實現(xiàn)基于libuv,這里不再展開討論,感興趣可以查看源碼。
// 調(diào)用libuv的api,初始化一個進(jìn)程 int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
exec/execFile
之所以把這兩個放到一起,是因為exec最后調(diào)用的就是execFile方法。唯一的區(qū)別是,exec中調(diào)用的normalizeExecArgs方法會將opts的shell屬性默認(rèn)設(shè)置為true。
exports.exec = function exec(/* command , options, callback */) {
const opts = normalizeExecArgs.apply(null, arguments);
return exports.execFile(opts.file, opts.options, opts.callback);
};
function normalizeExecArgs(command, options, callback) {
options = { ...options };
options.shell = typeof options.shell === 'string' ? options.shell : true;
return { options };
}
在execFile中,最終調(diào)用的是spawn方法。
exports.execFile = function execFile(file /* , args, options, callback */) {
let args = [];
let callback;
let options;
var child = spawn(file, args, {
// ... some options
});
return child;
}
exec會將spawn的輸入輸出流轉(zhuǎn)換成String,默認(rèn)使用UTF-8的編碼,然后傳遞給回調(diào)函數(shù),使用回調(diào)方式在node中較為熟悉,比流更容易操作,所以我們能使用exec方法執(zhí)行一些shell命令,然后在回調(diào)中獲取返回值。有點需要注意,這里的buffer是有最大緩存區(qū)的,如果超出會直接被kill掉,可用通過maxBuffer屬性進(jìn)行配置(默認(rèn): 200*1024)。
const { exec } = require('child_process');
exec('ls -lh /home', (error, stdout, stderr) => {
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
fork
fork最后也是調(diào)用spawn來創(chuàng)建子進(jìn)程,但是fork是spawn的一種特殊情況,用于衍生新的 Node.js 進(jìn)程,會產(chǎn)生一個新的V8實例,所以執(zhí)行fork方法時需要指定一個js文件。
exports.fork = function fork(modulePath /* , args, options */) {
// ...
options.shell = false;
return spawn(options.execPath, args, options);
};
通過fork創(chuàng)建子進(jìn)程之后,父子進(jìn)程直接會創(chuàng)建一個IPC(進(jìn)程間通信)通道,方便父子進(jìn)程直接通信,在js層使用 process.send(message) 和 process.on('message', msg => {}) 進(jìn)行通信。而在底層,實現(xiàn)進(jìn)程間通信的方式有很多,Node的進(jìn)程間通信基于libuv實現(xiàn),不同操作系統(tǒng)實現(xiàn)方式不一致。在*unix系統(tǒng)中采用Unix Domain Socket方式實現(xiàn),Windows中使用命名管道的方式實現(xiàn)。
常見進(jìn)程間通信方式:消息隊列、共享內(nèi)存、pipe、信號量、套接字
下面是一個父子進(jìn)程通信的實例。
parent.js
const path = require('path')
const { fork } = require('child_process')
const child = fork(path.join(__dirname, 'child.js'))
child.on('message', msg => {
console.log('message from child', msg)
});
child.send('hello child, I\'m master')
child.js
process.on('message', msg => {
console.log('message from master:', msg)
});
let counter = 0
setInterval(() => {
process.send({
child: true,
counter: counter++
})
}, 1000);

小結(jié)
其實可以看到,這些方法都是對spawn方法的復(fù)用,然后spawn方法底層調(diào)用了libuv進(jìn)行進(jìn)程的管理,具體可以看下圖。

利用fork實現(xiàn)master-worker模型
首先來看看,如果我們在child.js中啟動一個http服務(wù)會發(fā)生什么情況。
// master.js
const { fork } = require('child_process')
for (let i = 0; i < 2; i++) {
const child = fork('./child.js')
}
// child.js
const http = require('http')
http.createServer((req, res) => {
res.end('Hello World\n');
}).listen(8000)

+--------------+
| |
| master |
| |
+--------+--------------+- -- -- -
| |
| Error: listen EADDRINUSE
| |
|
+----v----+ +-----v---+
| | | |
| worker1 | | worker2 |
| | | |
+---------+ +---------+
:8000 :8000
我們fork了兩個子進(jìn)程,因為兩個子進(jìn)程同時對一個端口進(jìn)行監(jiān)聽,Node會直接拋出一個異常(Error: listen EADDRINUSE),如上圖所示。那么我們能不能使用代理模式,同時監(jiān)聽多個端口,讓master進(jìn)程監(jiān)聽80端口收到請求時,再將請求分發(fā)給不同服務(wù),而且master進(jìn)程還能做適當(dāng)?shù)呢?fù)載均衡。
+--------------+
| |
| master |
| :80 |
+--------+--------------+---------+
| |
| |
| |
| |
+----v----+ +-----v---+
| | | |
| worker1 | | worker2 |
| | | |
+---------+ +---------+
:8000 :8001
但是這么做又會帶來另一個問題,代理模式中十分消耗文件描述符(linux系統(tǒng)默認(rèn)的最大文件描述符限制是1024),文件描述符在windows系統(tǒng)中稱為句柄(handle),習(xí)慣性的我們也可以稱linux中的文件描述符為句柄。當(dāng)用戶進(jìn)行訪問,首先連接到master進(jìn)程,會消耗一個句柄,然后master進(jìn)程再代理到worker進(jìn)程又會消耗掉一個句柄,所以這種做法十分浪費系統(tǒng)資源。為了解決這個問題,Node的進(jìn)程間通信可以發(fā)送句柄,節(jié)省系統(tǒng)資源。
句柄是一種特殊的智能指針 。當(dāng)一個應(yīng)用程序要引用其他系統(tǒng)(如數(shù)據(jù)庫、操作系統(tǒng))所管理的內(nèi)存塊或?qū)ο髸r,就要使用句柄。
我們可以在master進(jìn)程啟動一個tcp服務(wù),然后通過IPC將服務(wù)的句柄發(fā)送給子進(jìn)程,子進(jìn)程再對服務(wù)的連接事件進(jìn)行監(jiān)聽,具體代碼如下:
// master.js
var { fork } = require('child_process')
var server = require('net').createServer()
server.on('connection', function(socket) {
socket.end('handled by master') // 響應(yīng)來自master
})
server.listen(3000, function() {
console.log('master listening on: ', 3000)
})
for (var i = 0; i < 2; i++) {
var child = fork('./child.js')
child.send('server', server) // 發(fā)送句柄給worker
console.log('worker create, pid is ', child.pid)
}
// child.js
process.on('message', function (msg, handler) {
if (msg !== 'server') {
return
}
// 獲取到句柄后,進(jìn)行請求的監(jiān)聽
handler.on('connection', function(socket) {
socket.end('handled by worker, pid is ' + process.pid)
})
})

下面我們通過curl連續(xù)請求 5 次服務(wù)。
for varible1 in {1..5}
do
curl "localhost:3000"
done

可以看到,響應(yīng)請求的可以是父進(jìn)程,也可以是不同子進(jìn)程,多個進(jìn)程對同一個服務(wù)響應(yīng)的連接事件監(jiān)聽,誰先搶占,就由誰進(jìn)行響應(yīng)。這里就會出現(xiàn)一個Linux網(wǎng)絡(luò)編程中很常見的事件,當(dāng)多個進(jìn)程同時監(jiān)聽網(wǎng)絡(luò)的連接事件,當(dāng)這個有新的連接到達(dá)時,這些進(jìn)程被同時喚醒,這被稱為“驚群”。這樣導(dǎo)致的情況就是,一旦事件到達(dá),每個進(jìn)程同時去響應(yīng)這一個事件,而最終只有一個進(jìn)程能處理事件成功,其他的進(jìn)程在處理該事件失敗后重新休眠,造成了系統(tǒng)資源的浪費。

ps:在windows系統(tǒng)上,永遠(yuǎn)都是最后定義的子進(jìn)程搶占到句柄,這可能和libuv的實現(xiàn)機制有關(guān),具體原因往有大佬能夠指點。

出現(xiàn)這樣的問題肯定是大家都不愿意的嘛,這個時候我們就想起了nginx的好了,這里有篇文章講解了nginx是如何解決“驚群”的,利用nginx的反向代理可以有效地解決這個問題,畢竟nginx本來就很擅長這種問題。
http {
upstream node {
server 127.0.0.1:8000;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
keepalive 64;
}
server {
listen 80;
server_name shenfq.com;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://node; # 這里要和最上面upstream后的應(yīng)用名一致,可以自定義
}
}
}
小結(jié)
如果我們自己用Node原生來實現(xiàn)一個多進(jìn)程模型,存在這樣或者那樣的問題,雖然最終我們借助了nginx達(dá)到了這個目的,但是使用nginx的話,我們需要另外維護(hù)一套nginx的配置,而且如果有一個Node服務(wù)掛了,nginx并不知道,還是會將請求轉(zhuǎn)發(fā)到那個端口。
cluster模塊
除了用nginx做反向代理,node本身也提供了一個cluster模塊,用于多核CPU環(huán)境下多進(jìn)程的負(fù)載均衡。cluster模塊創(chuàng)建子進(jìn)程本質(zhì)上是通過child_procee.fork,利用該模塊可以很容易的創(chuàng)建共享同一端口的子進(jìn)程服務(wù)器。
上手指南
有了這個模塊,你會感覺實現(xiàn)Node的單機集群是多么容易的一件事情。下面看看官方實例,短短的十幾行代碼就實現(xiàn)了一個多進(jìn)程的Node服務(wù),且自帶負(fù)載均衡。
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) { // 判斷是否為主進(jìn)程
console.log(`主進(jìn)程 ${process.pid} 正在運行`);
// 衍生工作進(jìn)程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作進(jìn)程 ${worker.process.pid} 已退出`);
});
} else { // 子進(jìn)程進(jìn)行服務(wù)器創(chuàng)建
// 工作進(jìn)程可以共享任何 TCP 連接。
// 在本例子中,共享的是一個 HTTP 服務(wù)器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`工作進(jìn)程 ${process.pid} 已啟動`);
}

cluster模塊源碼分析
首先看代碼,通過isMaster來判斷是否為主進(jìn)程,如果是主進(jìn)程進(jìn)行fork操作,子進(jìn)程創(chuàng)建服務(wù)器。這里cluster進(jìn)行fork操作時,執(zhí)行的是當(dāng)前文件。cluster.fork最終調(diào)用的child_process.fork,且第一個參數(shù)為process.argv.slice(2),在fork子進(jìn)程之后,會對其internalMessage事件進(jìn)行監(jiān)聽,這個后面會提到,具體代碼如下:
const { fork } = require('child_process');
cluster.fork = function(env) {
cluster.setupMaster();
const id = ++ids;
const workerProcess = createWorkerProcess(id, env);
const worker = new Worker({
id: id,
process: workerProcess
});
// 監(jiān)聽子進(jìn)程的消息
worker.process.on('internalMessage', internal(worker, onmessage));
// ...
};
// 配置master進(jìn)程
cluster.setupMaster = function(options) {
cluster.settings = {
args: process.argv.slice(2),
exec: process.argv[1],
execArgv: process.execArgv,
silent: false,
...cluster.settings,
...options
};
};
// 創(chuàng)建子進(jìn)程
function createWorkerProcess(id, env) {
return fork(cluster.settings.exec, cluster.settings.args, {
// some options
});
}
子進(jìn)程端口監(jiān)聽問題
這里會有一個問題,子進(jìn)程全部都在監(jiān)聽同一個端口,我們之前已經(jīng)試驗過,服務(wù)監(jiān)聽同一個端口會出現(xiàn)端口占用的問題,那么cluster模塊如何保證端口不沖突的呢? 查閱源碼發(fā)現(xiàn),http模塊的createServer繼承自net模塊。
util.inherits(Server, net.Server);
而在net模塊中,listen方法會調(diào)用listenInCluster方法,listenInCluster判斷當(dāng)前是否為master進(jìn)程。
Server.prototype.listen = function(...args) {
// ...
if (typeof options.port === 'number' || typeof options.port === 'string') {
// 如果listen方法只傳入了端口號,最后會走到這里
listenInCluster(this, null, options.port | 0, 4, backlog, undefined, options.exclusive);
return this;
}
// ...
};
function listenInCluster(server, address, port, addressType, backlog, fd, exclusive, flags) {
if (cluster === undefined) cluster = require('cluster');
if (cluster.isMaster) {
// 如果是主進(jìn)程則啟動一個服務(wù)
// 但是主進(jìn)程沒有調(diào)用過listen方法,所以沒有走這里一步
server._listen2(address, port, addressType, backlog, fd, flags);
return;
}
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
};
// 子進(jìn)程獲取主進(jìn)程服務(wù)的句柄
cluster._getServer(server, serverQuery, listenOnMasterHandle);
function listenOnMasterHandle(err, handle) {
server._handle = handle; // 重寫handle,對listen方法進(jìn)行了hack
server._listen2(address, port, addressType, backlog, fd, flags);
}
}
看上面代碼可以知道,真正啟動服務(wù)的方法為server._listen2。在_listen2方法中,最終調(diào)用的是_handle下的listen方法。
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
// ...
this._handle.onconnection = onconnection;
var err = this._handle.listen(backlog || 511);
// ...
}
Server.prototype._listen2 = setupListenHandle; // legacy alias
那么cluster._getServer方法到底做了什么呢?
搜尋它的源碼,首先向master進(jìn)程發(fā)送了一個消息,消息類型為queryServer。
// child.js
cluster._getServer = function(obj, options, cb) {
// ...
const message = {
act: 'queryServer',
index,
data: null,
...options
};
// 發(fā)送消息到master進(jìn)程,消息類型為 queryServer
send(message, (reply, handle) => {
rr(reply, indexesKey, cb); // Round-robin.
});
// ...
};
這里的rr方法,對前面提到的_handle.listen進(jìn)行了hack,所有子進(jìn)程的listen其實是不起作用的。
function rr(message, indexesKey, cb) {
if (message.errno)
return cb(message.errno, null);
var key = message.key;
function listen(backlog) { // listen方法直接返回0,不再進(jìn)行端口監(jiān)聽
return 0;
}
function close() {
send({ act: 'close', key });
}
function getsockname(out) {
return 0;
}
const handle = { close, listen, ref: noop, unref: noop };
handles.set(key, handle); // 根據(jù)key將工作進(jìn)程的 handle 進(jìn)行緩存
cb(0, handle);
}
// 這里的cb回調(diào)就是前面_getServer方法傳入的。 參考之前net模塊的listen方法
function listenOnMasterHandle(err, handle) {
server._handle = handle; // 重寫handle,對listen方法進(jìn)行了hack
// 該方法調(diào)用后,會對handle綁定一個 onconnection 方法,最后會進(jìn)行調(diào)用
server._listen2(address, port, addressType, backlog, fd, flags);
}
主進(jìn)程與子進(jìn)程通信
那么到底在哪里對端口進(jìn)行了監(jiān)聽呢?
前面提到過,fork子進(jìn)程的時候,對子進(jìn)程進(jìn)行了internalMessage事件的監(jiān)聽。
worker.process.on('internalMessage', internal(worker, onmessage));
子進(jìn)程向master進(jìn)程發(fā)送消息,一般使用process.send方法,會被監(jiān)聽的message事件所接收。這里是因為發(fā)送的message指定了cmd: 'NODE_CLUSTER',只要cmd字段以NODE_開頭,這樣消息就會認(rèn)為是內(nèi)部通信,被internalMessage事件所接收。
// child.js
function send(message, cb) {
return sendHelper(process, message, null, cb);
}
// utils.js
function sendHelper(proc, message, handle, cb) {
if (!proc.connected)
return false;
// Mark message as internal. See INTERNAL_PREFIX in lib/child_process.js
message = { cmd: 'NODE_CLUSTER', ...message, seq };
if (typeof cb === 'function')
callbacks.set(seq, cb);
seq += 1;
return proc.send(message, handle);
}
master進(jìn)程接收到消息后,根據(jù)act的類型開始執(zhí)行不同的方法,這里act為queryServer。queryServer方法會構(gòu)造一個key,如果這個key(規(guī)則主要為地址+端口+文件描述符)之前不存在,則對RoundRobinHandle構(gòu)造函數(shù)進(jìn)行了實例化,RoundRobinHandle構(gòu)造函數(shù)中啟動了一個TCP服務(wù),并對之前指定的端口進(jìn)行了監(jiān)聽。
// master.js
const handles = new Map();
function onmessage(message, handle) {
const worker = this;
if (message.act === 'online')
online(worker);
else if (message.act === 'queryServer')
queryServer(worker, message);
// other act logic
}
function queryServer(worker, message) {
// ...
const key = `${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
var handle = handles.get(key);
// 如果之前沒有對該key進(jìn)行實例化,則進(jìn)行實例化
if (handle === undefined) {
let address = message.address;
// const RoundRobinHandle = require('internal/cluster/round_robin_handle');
var constructor = RoundRobinHandle;
handle = new constructor(key,
address,
message.port,
message.addressType,
message.fd,
message.flags);
handles.set(key, handle);
}
// ...
}
// internal/cluster/round_robin_handle
function RoundRobinHandle(key, address, port, addressType, fd, flags) {
this.server = net.createServer(assert.fail);
// 這里啟動一個TCP服務(wù)器
this.server.listen({ port, host });
// TCP服務(wù)器啟動時的事件
this.server.once('listening', () => {
this.handle = this.server._handle;
this.handle.onconnection = (err, handle) => this.distribute(err, handle);
});
// ...
}
可以看到TCP服務(wù)啟動后,立馬對connection事件進(jìn)行了監(jiān)聽,會調(diào)用RoundRobinHandle的distribute方法。
// RoundRobinHandle
this.handle.onconnection = (err, handle) => this.distribute(err, handle);
// distribute 對工作進(jìn)程進(jìn)行分發(fā)
RoundRobinHandle.prototype.distribute = function(err, handle) {
this.handles.push(handle); // 存入TCP服務(wù)的句柄
const worker = this.free.shift(); // 取出第一個工作進(jìn)程
if (worker)
this.handoff(worker); // 切換到工作進(jìn)程
};
RoundRobinHandle.prototype.handoff = function(worker) {
const handle = this.handles.shift(); // 獲取TCP服務(wù)句柄
if (handle === undefined) {
this.free.push(worker); // 將該工作進(jìn)程重新放入隊列中
return;
}
const message = { act: 'newconn', key: this.key };
// 向工作進(jìn)程發(fā)送一個類型為 newconn 的消息以及TCP服務(wù)的句柄
sendHelper(worker.process, message, handle, (reply) => {
if (reply.accepted)
handle.close();
else
this.distribute(0, handle); // 工作進(jìn)程不能正常運行,啟動下一個
this.handoff(worker);
});
};
在子進(jìn)程中也有對內(nèi)部消息進(jìn)行監(jiān)聽,在cluster/child.js中,有個cluster._setupWorker方法,該方法會對內(nèi)部消息監(jiān)聽,該方法的在lib/internal/bootstrap/node.js中調(diào)用,這個文件是每次啟動node命令后,由C++模塊調(diào)用的。
function startup() {
// ...
startExecution();
}
function startExecution() {
// ...
prepareUserCodeExecution();
}
function prepareUserCodeExecution() {
if (process.argv[1] && process.env.NODE_UNIQUE_ID) {
const cluster = NativeModule.require('cluster');
cluster._setupWorker();
delete process.env.NODE_UNIQUE_ID;
}
}
startup()
下面看看_setupWorker方法做了什么。
cluster._setupWorker = function() {
// ...
process.on('internalMessage', internal(worker, onmessage));
function onmessage(message, handle) {
// 如果act為 newconn 調(diào)用onconnection方法
if (message.act === 'newconn')
onconnection(message, handle);
else if (message.act === 'disconnect')
_disconnect.call(worker, true);
}
};
function onconnection(message, handle) {
const key = message.key;
const server = handles.get(key);
const accepted = server !== undefined;
send({ ack: message.seq, accepted });
if (accepted)
server.onconnection(0, handle); // 調(diào)用net中的onconnection方法
}
最后子進(jìn)程獲取到客戶端句柄后,調(diào)用net模塊的onconnection,對Socket進(jìn)行實例化,后面就與其他http請求的邏輯一致了,不再細(xì)講。
至此,cluster模塊的邏輯就走通了。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
使用Meteor配合Node.js編寫實時聊天應(yīng)用的范例
這篇文章主要介紹了使用Meteor配合Node.js編寫實時聊天應(yīng)用的范例,Node.js作為異步框架,其最突出的使用便是用來編寫實時應(yīng)用程序,需要的朋友可以參考下2015-06-06
NodeJs crypto加密制作token的實現(xiàn)代碼
這篇文章主要介紹了NodeJs crypto加密制作token的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Node.js利用debug模塊打印出調(diào)試日志的方法
debug日志打印模塊主要實現(xiàn)功能是帶命名空間(模塊名)、時間戳、色彩輸出日志;將日志寫入文件;瀏覽器端使用;格式化函數(shù);支持自定義方法。下面這篇文章主要介紹了Node.js利用debug模塊打印出調(diào)試日志的方法,需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04
node全局變量__dirname與__filename的區(qū)別
這篇文章主要介紹了node全局變量__dirname與__filename的區(qū)別,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01
PostgreSQL Node.js實現(xiàn)函數(shù)計算方法示例
這篇文章主要給大家介紹了關(guān)于PostgreSQL Node.js實現(xiàn)函數(shù)計算的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
node?gyp安裝canvas原生模塊編譯node?pregyp詳解
這篇文章主要為大家介紹了Nodejs關(guān)于原生模塊編譯node-gyp + node-pre-gyp (以安裝canvas為例)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11

