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

這個博客是我最近整理了過去的文章。javascript

適合閱讀的同窗前端

想更進一步深刻理解node的同窗,若是你已經稍微瞭解一點點node, 能夠用node作一些小demo,而且想更深一步的瞭解,但願這篇文章能夠幫助到你。java

不太適合閱讀的同窗node

  1. 不太熟悉基本的javascript 語法,好比說回調函數c++

  2. 對node有深刻理解的同窗,好比說,能夠清晰的說出event-loopgit

Node 架構——v八、libuv

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

先來看一張圖表:shell

最上面是咱們編寫的node.js的代碼,當咱們執行node index.js的命令時,咱們是觸發了一個node的程序,和其餘的javascript的項目,好比說前端的h5項目同樣,該node程序須要有其餘的依賴,其中最主要的兩個依賴是 v8libuvexpress

  1. v8是 google 開源的引擎,目的是在瀏覽器世界外能夠運行javascript的代碼。npm

  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 還有其餘的一些比較重要的依賴。

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 很是有用:

process.env 提供了當前環境的一些信息

建議從 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 分爲幾步

當咱們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] }
複製代碼

上面的代碼中,對於 .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']
複製代碼

下一期咱們再見~

相關文章
相關標籤/搜索