Node 基礎學習

node是什麼

  • Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。
  • Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型。
  • Node使用包管理器NPM。

第一句話

Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。

運行環境, 不是一門語言,不是一個框架。只是能夠作爲JavaScript代碼運行的一個環境。

而這個運行環境主要是由V8提供的。

V8做了什麼?

創建了一個callstack。

function main(){
    func1();
}
function func1(){
    func2();
}
function func2(){
    console.log(1);
}
main();

V8 Engine

除去V8,Node中還有哪些東西?

除去V8,Node中另外一個比較重要的組成就是 libuv

What? libuv是什麼鬼?

先說說,關於Node的另外一句話:

Node is designed to build scalable network applications.

這句話的底氣在哪兒,就是Node本身採用的 事件驅動,非阻塞I/O模型

併發模型構建網絡的應用中,每個連接都會生成一個新線程,每個新線程可能需要 2MB 的配套內存。在一個擁有 8 GB RAM 的系統上,理論上最大的併發連接數量是 4,000 個用戶。隨着您的客戶羣的增長,如果希望您的 Web 應用程序支持更多用戶,那麼,您必須添加更多服務器。所以在傳統的後臺開發中,整個 Web 應用程序架構(包括流量、處理器速度和內存速度)中的瓶頸是:服務器能夠處理的併發連接的最大數量。這個不同的架構承載的併發數量是不一致的。而且,多個線程存在的話,也會衍生出線程切換的問題,這也會佔用一定資源。

併發存在的兩個問題:

  • Execution stacks take up memory: 資源有限
  • Context switching is not free:佔用額外資源

traditional-web-server-model

Node的解決這個問題思路:

  1. 在網絡應用中,比較慢的環節主要在 磁盤讀取 或 網絡請求階段,這時候CPU處於閒置狀態。
  2. 如果在等待I/O操作時,能夠釋放處於閒置狀態的CPU,則可以很大程度上利用資源;
  3. 那麼接下來的問題就在於,如何在I/O 操作完成後,繼續執行後續的操作;
  4. 解決方案:事件驅動,採用回調。這和瀏覽器中的Event Loop是一樣的道理。

市場上已經有一些使用同樣處理思路的框架。

  • EventMachine:處理網絡請求的框架,主要是面向ruby;
  • Twisted:用Python實現的基於事件驅動的網絡引擎框架;

nodejs-process-model

具體是怎麼做的?

在程序運行時,V8 會把I/O操作等耗時較多操作及相關回調一併交給libuv去處理。而V8繼續執行後面的代碼。等到I/O操作完成後,libuv會將回調方法放到事件隊列中。

const fs = require('fs');

const readFile = (file) => {
    fs.readFile(file, (err, data) => {
        if(!err) console.log(data);
    });
}

console.log('program start ......');
readFile('file.json');
console.log('readFile has put the I/O ');
console.log('program end!!!!!');

V8 async

負責異步程序調度的工作就是libuv做的事情。

Libuv is a multi-platform support library with a focus on asynchronous I/O.

在上述程序中,當遇到文件讀取操作時,V8會把js接口轉成C++接口 同時把回調方法一併交給libvu。此時libuv接管這個文件讀取任務。當文件讀取完成後,libuv就會把這個事件的回調函數扔到事件隊列裏。當下一次檢查事件隊列時,就會執行該回調函數。

question: 既然這樣,怎麼理解node中的單線程?

再捋一捋Node, V8 和libuv的關係。

1) Node主要由V8 javascript引擎和libuv組成;
2) v8引擎主要負責解釋執行js代碼,碰到需要異步的操作會交給libuv處理;
3) libuv本身是獨立的c語言庫,可以直接使用c/c++來調用;

在瀏覽器端,JS是沒有能力進行文件讀取操作的。js最初的能力也就限制在表單校驗上。而在Node中,JS可以操作本地文件,建立網絡連接。這肯定是Node乾的好事!

再來說說Node中其它一些組成部分

Node擴展了JS的能力:builtin modules

builtin modules是由C++代碼寫成各類模塊,包含了crypto,zlib, file, net等基礎功能。

native modules

除了builtin modules, 還有一個native modules。它們是用js編寫的內建模塊,提供給程序開發者使用。

  • fs
  • http

builtin modules 和 native modules都屬於核心模塊。核心模塊在Node源碼編譯的過程中,編譯進了二進制文件。所以在Node進程啓動的時候,部分核心模塊就已經被直接加載到了內存當中。

至此,Node的基本構成和運行原理已經講完了。

Node內部結構
Node 構成

補充一句:Node.js的單線程並不是真正的單線程,只是開啓了單個線程(可以定義爲主線程)進行業務處理,同時開啓了其他線程專門處理I/O。當一個指令到達主線程,主線程發現有I/O之後,直接把這個事件傳給libuv處理。libuv會管理一個線程池,I/O操作就是由線程池裏面的線程完成的。等I/O 操作完成,libuv,會把對應I/O操作的回調放到事件循環當中。在線程上,不會等待I/O 操作完成,繼續執行後續的代碼。這就是“單線程”、“異步I/O”。

IO.js

var fork = require('child_process').fork;
var fs = require('fs');

console.log('start......');

// blocking
// console.log(fs.readFileSync('test.json', 'utf-8'));

// non blocking
var childProcess = fork('another-thread.js');
childProcess.on('message', function(data){
    console.log(data);
})

console.log('end !!!');

another-child.js

var fs = require('fs');
process.send( fs.readFileSync('test.json', 'utf-8'));
Everything runs in parallel except your code! 在Node中)除了代碼,一切都是並行的!

由於node中主任務的執行是以單線程的方式進行,如果程序出錯導致崩潰,就會終止整個流程。爲此,市場上有些Node進程管理工具,它們會維護Node程序的狀態,當程序掛掉時,會自動重啓。比如我們使用的pm2

NPM

對於node沒有的一些模塊(native modules),可以引入外部模塊。這些外部模塊通常是其它開發者貢獻的。

那麼問題來了,對於數量衆多的模塊中,如何快速找到自己想要的並能夠快速的引進到自己的項目當中。

這就是npm幫我們做的工作。

Use npm to install, share, and distribute code; manage dependencies in your projects; and share & receive feedback with others.

npm官網

  • 模塊安裝:
  • 模塊共享
  • 發佈代碼
  • 管理依賴
  • 共享和反饋

NPM的基本模式

NPM是JavaScript包的管理器。

NPM basic

廣義的npm的構成

npm consists of three distinct components:

  • the website: NPM官方站點
  • the Command Line Interface (CLI) : NPM命令行工具
  • the registry: JS模塊的數據庫

Use the website to discover packages, set up profiles, and manage other aspects of your npm experience. For example, you can set up Orgs (organizations) to manage access to public or private packages.

The CLI runs from a terminal. This is how most developers interact with npm.

The registry is a large public database of JavaScript software and the meta-information surrounding it.

npm中的使用

npm默認隨node一起安裝,在Node安裝完成後,npm已經安裝。

查找一個包

去npm官網,按關鍵詞查找。

管理模塊

  • npm install [module name] :普通安裝方式,包安裝完成後,會在當前目錄生成一個node_modules目錄。這是一個存放外部js模塊的地方,通過npm安裝的包都放在node_modules下。
  • npm install -g [module name]:全局安裝,模塊被安裝在node安裝路徑下的 node_modules中。
  • npm install [folder path]:可以指定npm 安裝某個目錄folder path下的的文件,前提是這個目錄下包含package.json文件。
  • npm install [module name]@[version]: 安裝包的時候,指定對應的版本號。

    • npm isntall chalk@latest : 安裝最新版
    • npm install [email protected]: 安裝2.0.0版
    • npm install chalk@">=2.0.0":安裝大於2.0.0的版本
  • npm install --save-prod [module name]: 在本地安裝包,並將安裝信息寫入 package.json文件中的dependencies中, 不寫--save-prod 或者只寫 --save 默認跟 --save-prod一樣。
  • npm install --save-dev [module name]:在本地安裝包,並將安裝信息寫入 package.json文件中的devDependencies

    • dependencies:在生產環境中需要用到的依賴
    • devDependencies:在開發、測試環境中用到的依賴
  • npm update [module name]: 更新本地模塊
  • npm uninstall [module name]: 卸載模塊

package.json

package.json是一個node和npm都會自動讀取的配置文件,它裏面是個標準的JSON格式字符串。

對於NPM而言, package.json做了以下工作:

  • 存儲項目依賴的所有包
  • 允許你指定項目依賴的包的版本規則,不滿足項目需求的版本,不需要
  • 讓你的項目構建具有可複用性,容易分享你的項目

對於你的項目而言,package.json定義了一些基礎信息,比如項目名稱,版本等等。

pakcage.json必須具有的兩個字段: nameversion。這倆個字段有什麼意義呢?
NPM 作爲一個包管理平臺,當有開發者提交(publish)模塊時,必須提供一些基本信息便於管理。

  • name: 項目名稱或者模塊名稱
  • version:版本號,應當遵循 x.x.x的格式
  • description:項目信息描述,方便別人瞭解你的模塊,也利於搜索
  • keywords:項目的關鍵詞,便於搜索
  • homepage:項目的主頁;
  • scripts:scripts屬性是一個對象,裏面的每一個屬性對應一段腳本;腳本可以使用 npm run + 屬性名 執行
  • main:指定項目的程序入口文件,該文件的exports對象同時也是require項目時,取到的對象。
  • repository:指明代碼存在的地址,便於別人更好的查看你的源碼
  • dependencies:一個對象,配置模塊依賴的模塊列表,key是模塊名稱,value是版本描述(遵循semantic規則)
  • devDependencies:一個對象,開發或測試過程中的一些依賴模塊,跟上線後的依賴模塊區分出來
  • engines: 指定項目運行的Node版本
  • author:項目的開發者
  • contributors:一堆項目的開發者
{
  "name": "vue-todo",
  "version": "1.0.0",
  "description": "a simply todolist using vuejs",
  "scripts": {
    "start": "node server.js",
    "stop": "egg-scripts stop --title=egg-server-example",
    "dev": "egg-bin dev"
  },
  "dependencies": { // 線上生產環境必須,當然開發環境也會用到
    "babel-runtime": "^6.23.0",
    "vue": "^2.0.1",
    "vue-localstorage": "^0.1.1",
    "vuex": "^2.2.1"
  },
  "devDependencies": { // 開發環境會用到的東東
    "webpack": "^1.13.2",
    "webpack-dev-middleware": "^1.8.3",
    "webpack-hot-middleware": "^2.12.2",
    "webpack-merge": "^0.14.1"
  }
}

semantic versioning(語義化版本規則)

版本格式:主版本號.次版本號.修訂號, 例如 1.2.3

版本號遞增規則如下:

  • 主版本號:當你做了不兼容的 API 修改; 1.2.3 ---> 2.0.0
  • 次版本號:當你做了向下兼容的功能性新增; 1.2.3 ---> 1.3.0
  • 修訂號:當你做了向下兼容的問題修正; 1.2.3 ---> 1.2.4

在package.json定義版本規則的時候,可以這麼做:

  • 如果只打算接受補丁版本的更新(也就是最後一位的改變),就可以這麼寫:

    • 1.0
    • 1.0.x
    • ~1.0.4
  • 如果接受小版本的更新(第二位的改變),就可以這麼寫:

    • 1
    • 1.x
    • ^1.0.4
  • 如果可以接受大版本的更新(自然接受小版本和補丁版本的改變),就可以這麼寫:

    • *
    • x

在使用npm install --save || npm install --save-dev 安裝的時候,寫入pakcage.json中的依賴,默認接受小版本的更新,即在版本號前添加 '^'。

NPM document

Node 模塊

Node中的模塊分爲兩類:

  • 核心模塊(Node中內嵌的,比如fshttp等)
  • 文件模塊(開發者自己編寫的,NPM上的模塊都屬於開發者自定義的模塊)

文件模塊是在運行的時候,動態加載。需要進行路徑分析,文件定位 和 編譯執行。

Node加載模塊時,優先從緩存中加載,如果緩存中不存在該模塊,纔會按照上述的三個步驟進行模塊加載。

模塊標識符在Node中,主要有以下幾類:

  • 核心模塊,比如fs, http
  • ...開頭的相對路徑文件模塊
  • / 開頭的絕對路徑文件模塊
  • 非路徑形式的文件模塊;

這幾類模塊的加載速度是依次降低的。

module.js

同瀏覽器中的window一樣,在Node中的全局變量都掛在global下。
先說一下,常用到一些變量:

  • __dirname: 當前模塊所在目錄
  • __filename: 當前模塊文件名稱
  • require: 引入其它模塊
  • module:
  • exports

上面5個變量,貌似全局變量,但不是全局變量。他們都是模塊系統下的東西。

question: 它們不在全局變量下,那它們爲何可以在模塊中直接調用?

Node引入模塊

在Node中,引入模塊分爲三個步驟:

  • 路徑分析
  • 文件定位
  • 編譯執行

模塊編譯

編譯和執行是引入文件模塊的最後一個階段。

  • .js文件 : Node 會對js源碼進行一個首尾的封裝。返回一個function,並將當前的環境的exports,require,module,__dirname,__filename作爲形參傳遞給這個function。

包裝後的代碼:

(function(exports, require, module, __filename, __dirname){
    // js文件中的源碼
})

這就是爲什麼 它們不在 全局變量下,卻可以在模塊當中使用的原因。

  • .node文件: 對於.node文件, 實際上並不需要編譯過程。因爲.node文件本身就是C/C++編譯後的文件,它只有加載和執行過程。
  • .json文件: Node 會讀取json文件內容,並將它賦予exports對象,直接傳遞給第三方調用。

Node_module_callstack

在NPM中的模塊,基本屬於Node中文件模塊裏的Javascript模塊。

require的規則

Node的模塊系統參照CommonJS規範實現。

  1. 如果參數字符串不以.//../開頭,說明要加載的不是一個文件,而是一個默認提供的核心模塊。

    1. 此時則先在node平臺所提供的核心模塊當中找;
    2. 然後再尋找NPM模塊(即第三方模塊包,或自己寫的模塊包)

      1. 在尋找NPM模塊包時,會從當前目錄出發,向上搜索各級當中的node_modules文件夾當中的文件,但若有兩個同名文件,則遵循就近原則。(module.paths是一個模塊路徑數組。)
  2. 如果require當中的參數字符串以/開頭:則表示從系統根目錄開始尋找該模塊文件。
  3. 如果require當中的參數字符串以./(從當前目錄出發)或../(從上一級目錄出發)開頭:表示按照相對路徑,從當前文件所在的文件夾開始尋找要載入的模塊文件。

    1. 按js文件來執行(先找對應路徑當中的module.js文件來加載)
    2. 按json文件來解析(若上面的js文件找不到時,則找對應路徑當中的module.json文件來加載)
    3. 按照預編譯好的c++模塊來執行(尋找對應路徑當中的module.node文件來加載)
    4. 若參數字符串爲一個目錄(文件夾)的路徑,則自動先查找該文件夾下的package.json文件,然後再再加載該文件當中main字段所指定的入口文件。(若package.json文件當中沒有main字段,或者根本沒有package.json文件,則再默認查找該文件夾下的index.js文件作爲模塊來載入。)

Node查找模塊示意圖

process

process 對象是一個全局變量,它提供當前 Node.js 進程的有關信息,以及控制當前 Node.js 進程。

  • process.argv: 包含命令行參數的數組。第一個元素會是'node',第二個元素將是.js文件的名稱,接下來的參數依次是命令行參數
  • process.execArgv: 啓動進程所需的 node 命令行參數。這些參數不會在 process.argv 裏出現,並且不包含 node 執行文件的名字,或者任何在名字之後的參數。這些用來生成子進程,使之擁有和父進程有相同的參數
  • process.env: 獲取當前系統環境信息的對象,輸出內容是環境變量等內容,這個對象可以修改
  • nextTick(callback): 將callback放到事件輪詢隊列首位,下一次事件輪詢開始時,先執行callback
  • process.abort:結束當前進程
  • process.kill(pid):結束一個進程

process.js

console.log(process.argv);
console.log(process.execArgv);
node process.js
# [ '/usr/local/bin/node', '/root/node-demo/process.js' ]
# []

node process.js abc 234 cvb=cvb
# [ '/usr/local/bin/node',
#   '/root/node-demo/process.js',
#   'abc',
#   '234',
#   'cvb=cvb' ]
# []


node --harmony  --use-openssl-ca  process.js abc 234 cvb=cvb
# [ '/usr/local/bin/node',
#   '/root/node-demo/process.js',
#   'abc',
#   '234',
#   'cvb=cvb' ]
# [ '--harmony', '--use-openssl-ca' ]

Node'Process document

fs

file.js

Node'FileSystem document

questions require('../fs/file.js')這裏是異步還是同步?

deno,下一代Node?

  • package.json
  • node_modules
  • gyp

等等............

the heaviest object

Node之父JS大會介紹deno時的PPT(2018)

Node之父JS大會介紹Node時的PPT(2009)

Nodejs的運行原理-科普篇

視頻:callstack 和異步 基本原理

Awesome Micro npm Packages

libuv 官網

[深入淺出Node.js]()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章