成為自信的node.js開發者(一)

杜俊成要好好學習發表於2019-02-18

這個部落格是我最近整理了過去的文章。

適合閱讀的同學

想更進一步深入理解node的同學,如果你已經稍微瞭解一點點node, 可以用node做一些小demo,並且想更深一步的瞭解,希望這篇文章可以幫助到你。

不太適合閱讀的同學

  1. 不太熟悉基本的javascript 語法,比如說回撥函式

  2. 對node有深入理解的同學,比如說,可以清晰的說出event-loop

Node 架構——v8、libuv

第一部分,我們先了解一下node的結構,對node先有一個整體上的認識。只有這樣,我們才能編寫出更加高效能的程式碼,在遇到問題時,也知道解決的思路。

先來看一張圖表:

成為自信的node.js開發者(一)

最上面是我們編寫的node.js的程式碼,當我們執行node index.js的命令時,我們是觸發了一個node的程式,和其他的javascript的專案,比如說前端的h5專案一樣,該node程式需要有其他的依賴,其中最主要的兩個依賴是 v8libuv

  1. v8是 google 開源的引擎,目的是在瀏覽器世界外可以執行javascript的程式碼。

  2. libuv 是c++ 開源的專案,最初就是專門為node設計,目的是給node和作業系統互動的能力,比如說網路, 操作檔案。

node在可見的未來仍將繼續使用v8, 但是微軟edge瀏覽器的chakra(讀法:渣坷垃)引擎也是一個強有力的競爭者。github.com/nodejs/node… 這個專案是如何讓node如何跑在chakras 引擎上

v8 引擎

我們現在知道了,node 使用 v8 用來執行javascript 程式碼,這意味著,node中所支援的javascript的特性,是由 v8 引擎所決定的。

V8引擎支援的 javascript 特性被劃分為三個不同的group: ShippingStagedIn Progress

預設情況下Shipping group的特性可以直接使用,Staged group的特性需要使用--harmony選項來開啟。如下所示:

➜ node -v
v7.9.0
➜ node -p 'process.versions.v8'
5.5.372.43
➜ node -p "'Node'.padEnd(8, '*')"    // 預設是不支援的
[eval]:1
'Node'.padEnd(8, '*')
       ^

TypeError: "Node".padEnd is not a function
    at [eval]:1:8
    at ContextifyScript.Script.runInThisContext (vm.js:23:33)
    at Object.runInThisContext (vm.js:95:38)
    at Object. ([eval]-wrapper:6:22)
    at Module._compile (module.js:571:32)
    at evalScript (bootstrap_node.js:387:27)
    at run (bootstrap_node.js:120:11)
    at run (bootstrap_node.js:423:7)
    at startup (bootstrap_node.js:119:9)
    at bootstrap_node.js:538:3
➜ node --harmony -p "'Node'.padEnd(8, '*')"  // 通過--harmony
Node****
複製程式碼

In Progress group的feature不穩定,但你也可以使用特定的flag來開啟,通過 node --v8-options 命令可以檢視,通過grep 命令去查詢in progress,如下:

➜ node --v8-options | grep "in progress"
  --harmony_array_prototype_values (enable "harmony Array.prototype.values" (in progress))
  --harmony_function_sent (enable "harmony function.sent" (in progress))
  --harmony_sharedarraybuffer (enable "harmony sharedarraybuffer" (in progress))
  --harmony_simd (enable "harmony simd" (in progress))
  --harmony_do_expressions (enable "harmony do-expressions" (in progress))
  --harmony_restrictive_generators (enable "harmony restrictions on generator declarations" (in progress))
  --harmony_regexp_named_captures (enable "harmony regexp named captures" (in progress))
  --harmony_regexp_property (enable "harmony unicode regexp property classes" (in progress))
  --harmony_for_in (enable "harmony for-in syntax" (in progress))
  --harmony_trailing_commas (enable "harmony trailing commas in function parameter lists" (in progress))
  --harmony_class_fields (enable "harmony public fields in class literals" (in progress))
複製程式碼

比如說,上面列印出來的倒數第二行-- harmony_trailing_commas 可以支援函式傳參尾逗號:

node -p 'function tc(a,b,) {}'   // 會報錯,因為最後一個逗號
=========================
node --harmony_trailing_commas -p 'function tc(a,b,) {}'   //不會報錯
複製程式碼

libuv

  1. libuv 提供了和作業系統互動的能力,比如說操作檔案,網路等等,並且磨平了作業系統的差異。

  2. node還使用libuv來處理非同步操作,比如非阻塞IO(file system/TCP socket/child process)。當非同步操作完成時,node通常需要呼叫回撥函式,當呼叫回撥函式時,node會把控制權交給V8引擎。 當回撥函式執行完畢,控制權從v8引擎重新回到node.

    v8 引擎是單執行緒的,當v8引擎獲得控制權的時候,node 只能等待v8 引擎操作完成。

    這讓node沒有死鎖,競爭的概念。

  3. libuv 包含一個執行緒池,從作業系統的層面來做那些不能被非同步做的事情

  4. libuv 給node 提供了 event-loop, 會在第二節介紹

其他依賴

除了v8引擎和 libuv, node 還有其他的一些比較重要的依賴。

成為自信的node.js開發者(一)

http-parser 用來解析http內容的

c-ares 是用來支援非同步的DNS 查詢的

openSSL 常用在 tlscrypto 的包中,提供了加密的方法

zlib 是用來壓縮和解壓的

node REPL

你可以在terminal裡面執行node來啟動CLI,如下所示,REPL十分方便

例如,你定義一個array,當你arr.然後tab-tab(tab兩次),array自身的方法會顯示出來

➜ node
> var arr = [];
undefined
> arr.
arr.toString              arr.valueOf
arr.concat                arr.copyWithin         arr.entries               arr.every              arr.fill                  arr.filter
arr.find                  arr.findIndex          arr.forEach               arr.includes           arr.indexOf               arr.join
arr.keys                  arr.lastIndexOf        arr.length                arr.map                arr.pop                   arr.push
arr.reduce                arr.reduceRight        arr.reverse               arr.shift              arr.slice                 arr.some
arr.sort                  arr.splice             arr.unshift
複製程式碼

你也可以輸入.help,然後可以看到各種快捷鍵如下:

> .help
.break    Sometimes you get stuck, this gets you out
.clear    Alias for .break
.editor   Enter editor mode
.exit     Exit the repl
.help     Print this help message
.load     Load JS from a file into the REPL session
.save     Save all evaluated commands in this REPL session to a file
複製程式碼

你還可以用_(underscore)來得到上次evaluated的值:

> 3 - 2
1
> _
1
> 3 < 2
false
> _
false
複製程式碼

你還可以自定義REPL選項,如下,你自定義repl.js並選擇忽視undefined,這樣output裡面就不會有undefined輸出,同時你還可以預先載入你需要的library比如lodash

// repl.js
let repl = require('repl');
let r = repl.start({ ignoreUndefined: true  });
r.context.lodash = require('lodash');
複製程式碼
➜ node ~/repl.js
> var i = 2;
> 
>
複製程式碼

你可以用下面的command來檢視更多的選項 node --help | less

-p, --print     evaluate script and print result

-c, --check     syntax check script without executing

-r, --require   module to preload (option can be repeated)
複製程式碼

例如,node -c bad-syntax.js可以用來檢查語法錯誤, node -p 'os.cpus()'可以用來執行script並輸出結果,你還可以傳入引數,如下所示

➜ node -p 'process.argv.slice(1)' test 666
[ 'test', '666' ]
複製程式碼

node -r babel-core/register可以用來預載入,相當於require('babel-core/register')

global 中的 process 和 buffer

global相當於瀏覽器裡面的window,你可以global.a = 1;這樣a就是全域性變數,但一般不推薦這樣做

global 物件身上有兩個屬性特別重要: processbuffer

process

processapplicationrunning env之間的橋樑,可以得到執行環境相關資訊,如下所示:

> process.
process.arch
process.argv
process.argv0                       process.assert                      process.binding                     
process.chdir
process.config                      process.cpuUsage                    
process.cwd                         process.debugPort
process.dlopen                      process.emitWarning                 
process.env                         process.execArgv
process.execPath                    
process.exit                        process.features                    process.getegid
process.geteuid                     process.getgid                      process.getgroups                   process.getuid
process.hrtime                      process.initgroups                  
process.kill                        process.memoryUsage
process.moduleLoadList              process.nextTick                    process.openStdin                   
process.pid
process.platform                    process.reallyExit                  process.release                     process.setegid
process.seteuid                     process.setgid                      process.setgroups                   process.setuid
process.stderr                      process.stdin                       process.stdout                      
process.title
process.umask                       process.uptime                      process.version                     process.versions
process._events                     process._maxListeners               process.addListener                 process.domain
process.emit                        process.eventNames                  process.getMaxListeners             process.listenerCount
process.listeners                   
process.on                          
process.once                        process.prependListener
process.prependOnceListener         process.removeAllListeners          process.removeListener              process.setMaxListeners\
複製程式碼

process.versions 非常有用:

成為自信的node.js開發者(一)

process.env 提供了當前環境的一些資訊

成為自信的node.js開發者(一)

建議從 process.env 中只讀,因為改了也沒有用。

同時,process也是一個event emitter,例如:

process.on('exit', code => {
  // 並不能阻止node程式退出
  console.log(code)
})

process.on('uncaughtException', err => {
  console.error(err)
  process.exit(1)
})
複製程式碼
  1. 在process 的事件處理函式中,我們只能執行同步的方法,而不能使用event_loop,

  2. exituncaughtException 的區別。如果uncaughtException 註冊了事件,則node遇到錯誤並不會退出,也就是說,不會觸發exit 事件。這會讓node的執行變的不可預測。證明如下:

    process.on('exit', (code) => {
        console.log('ssss')    
    })
    process.on('uncaughtException', (err) => {
        console.error(err);
    })
    // keep the event loop busy
    process.stdin.resume()
    
    // 在這裡觸發了bug
    console.logg()
    複製程式碼

    上面的程式碼即使遇到了錯誤也不會退出執行,exit 事件處理函式並不會觸發。所以需要我們手動觸發 process.exit(1) 才可以。

buffer

buffer 也是 global 物件中的一個屬性,主要用來處理二進位制流buffer 本質上是一段記憶體片段,是放在v8引擎的堆的外面。

我們可以在buffer 這個記憶體中存放資料。

buffer讀取資料時,我們必須指定encoding, 因此從 filessockets 中讀取資料時,如果不指定encoding, 我們會得到一個 buffer 物件。

一旦buffer 被建立,就不能修改大小

buffer 在處理讀取檔案,網路資料流的時候非常有用

建立buffer的三種方式:

  1. Buffer.alloc(2)

    在記憶體中劃分出固定的大小

  2. Buffer.allocUnsafe(8)

    沒有指定具體的資料,可能會包含老的資料和敏感的資料,需要被正確的『填充』

  3. Buffer.from()

buffer的方法

和陣列類似,但是不同。比如說 slice 方法擷取出來的新buffer 和 老的buffer是共享同一個記憶體。

stringDecode

當轉變二進位制資料流的時候,toString() 不如使用 stringDecode 模組,因為該模組可以處理不完整的資料呢。

Require() 的背後

如果想深入瞭解node, 必須要深入瞭解 require 方法。

涉及到兩個核心模組——require 方法(在grobal物件上,但是每一個模組都有自己的require 方法) 和 Module 模組 (同樣在grobal物件上,用來管理模組的)

require 分為幾步

成為自信的node.js開發者(一)

當我們require一個module時,整個過程有五個步驟:

Resolving 找到module的絕對檔案路徑

Loading 將檔案內容載入到記憶體

Wrapping 給每個module創造一個private scope並確保require對每個module來說是local變數

Evaluating VM執行module程式碼

Caching 快取module以備下次使用

module 物件

Module {
  id: '.',
  exports: {},
  parent: undefined,
  filename: '/Users/xxx/lib/find.js',
  loaded: false,
  children: [],
  paths: 
   [ '/Users/xxx/lib/node_modules',
     '/Users/xxx/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }
複製程式碼

在Module物件裡面,id 是module的identity,通常它的值是module檔案的全路徑,除非是root,這時它的值是.(dot)

filename 是檔案的路徑

paths 從當前路徑開始,往上一直到根路徑

require.resolve 和require一樣,但是它不會載入檔案,只是resolve

模組不一定是檔案

  1. 可以是檔案,比如說 node_module/find-me.js

  2. 可以是目錄帶index.js,比如說 node_module/find-me/index.js

  3. 可以是目錄帶package.json, 比如說node_module/find-me/main.js

    {
        "name": "find-me",
        "main": "start.js"
    }
    複製程式碼

exports 屬性

exportsmodule 上一個特殊的屬性,我們放入它的任何變數都可以在require時得到 。

loaded

Module物件的loaded屬性會保持false,直到所有content都被載入

因此,exports 不能放在的非同步的setImmediate

迴圈引用

例如A require B,B require A

JSON 檔案 和 c++ Addon 檔案

Node會首先查詢.js檔案,再查詢.json檔案,最後.node檔案 比如說,在主檔案中,引入.json 檔案

// 在主檔案中
let mock = require('mockData.json')
console.log(mock)
複製程式碼

mockData.json 檔案中,不需要匯出什麼,直接寫json格式的即可

{
    "a": "abc",
    "b": "abc",
}
複製程式碼

如果node找不到 .js , .json 檔案,就會找.node 檔案,會把.node 檔案作為一個編譯好的addon(外掛) module。那麼 .node 檔案是從哪裡來的呢?

  1. 先有一個 hello.cc 檔案,是用 c++ 程式碼寫的

  2. 再有一個 binding.gyp, 相當於的編譯的配置檔案,裡面是json 格式的配置項, 如下面所示:

    {
      "targets": [
        {
          "target_name": "addon",
          "sources": [ "hello.cc" ]
        }
      ]
    }
    複製程式碼
  3. 安裝 npm install node-gyp -g , node 和 npm 自帶的那個不是給開發者用的,而是需要重新安裝一個

  4. node-gyp configure 根據平臺生成專案,再執行node-gyp build 生成 .node 檔案,可以在 js的程式碼中直接引用使用了。

    你可以通過require.extensions來檢視Node支援的副檔名:

> require.extensions
{ '.js': [Function], '.json': [Function], '.node': [Function] }
複製程式碼

成為自信的node.js開發者(一)

上面的程式碼中,對於 .js 檔案,是直接編譯引入,對於.json 檔案,是使用了JSON.parse 方法,對於 .node 檔案,是使用了 process.dlopen() 方法。

包裹模組

exports.id = 1;   // 對的

exports = {
    id: 1,        // 錯的
}

module.exports = {
    id: 1        // 對的
}
複製程式碼

上面的程式碼中,為什麼exportsmodule.exports 有區別?

原因是,node 引入一個模組程式碼後,node 會給這些程式碼外面包裹上一層方法,這個方法是module 模組的wrapper 方法:

> require('module').wrapper
>[ '(function (exports, require, module, __filename, __dirname) { ',
  '\n});' ]
複製程式碼

這個方法接受5個引數: exports, require, module, __filename, __dirname

這個方法,讓 exports, require, module 看起來是全域性變數,但其實是每個檔案所獨有的。

exportsmodule 物件的module.exports 方法的引用,相當於 let exports s = module.exports, 如果讓 exports = {} 等於讓 exports 變數改寫了引用

快取模組

當第二次引入同一個檔案的時候,將會走了快取。

console.log(require.cache)
delete require.cache['/User/sss/sss/cache.js']
複製程式碼

下一期我們再見~

相關文章