寫在前面
- 書籍介紹:本書由首章Node介紹為索引,涉及Node的各個方面,主要內容包含模塊機制的揭示、異步I/O實作原理的展現、異步編程的探討、記憶體控制的介紹、二進制資料Buffer的細節、Node中的網路編程基礎、Node中的Web開發、行程間的訊息傳遞、Node測驗以及通過Node構建產品需要的注意事項,
- 我的簡評:這是一本難得的好書,這本書理論和實踐結合的很好,如果你是一個純前端的開發者,這本書可以讀讀開拓些視野,如果你是一個全堆疊的開發者,這本書作為入門和深入后端也很不錯,推薦拜讀,
- !!文末有pdf書籍、筆記思維導圖、隨書代碼打包下載地址,需要請自取!閱讀[書籍精讀系列]所有文章,請移步:推薦收藏-JavaScript書籍精讀筆記系列導航
第一章 Node簡介
1.1.Node的誕生歷程
- 2009年3月, Ryan Dahl
1.2.Node的命名與起源
- 別名 Nodejs、 NodeJS、 Node.js
- 找到了設計高性能, Web服務器的幾個要點: 事件驅動、非阻塞I/O
- JavaScript 高性能、符合事件驅動、沒有歷史包袱
- 構建網路應用的一個基礎框架
1.3.Node給JavaScript帶來的意義
- 瀏覽器中除了V8作為JavaScript引擎外,還有一個WebKit布局引擎
- 瀏覽器通過事件驅動來服務界面上的互動, Node通過過事件驅動來服務I/O
1.4.Node的特點
- 異步I/O、事件與回呼函式、單執行緒、跨平臺
- 單執行緒:弱點1:無法利用多核CPU;弱點2:錯誤會引起整個應用退出,應用的健壯性值得考驗;弱點3:大量計算占用CPU導致無法繼續呼叫異步I/O;Node采用與Web Workers相同的思路來解決單執行緒中大計算量的問題:child_process;
- 跨平臺:在作業系統與Node上層模塊系統之間構建了一層平臺層架構,即libuv;Node的第三方C++模塊也可以借助libuv實作跨平臺;
1.5.Node的應用場景
- I/O密集型:面向網路且擅長并行I/O
- 是否不擅長CPU密集型業務:采用使用多執行緒的方式進行計算;通過撰寫C++擴展的方式更高效利用CPU;
- 與遺留系統和平共處
- 分布式應用:資料平臺、資料庫集群
1.6.Node的使用者
- 前后端編程語言環境統一
- Node帶來的高性能I/O用于實時應用
- 并行I/O使得使用者可以更高效的利用分布式環境
- 并行I/O,有效利用穩定介面提升Web渲染能力
- 云計算平臺提供Node支持
- 游戲開發領域
- 工具類應用
第二章 模塊機制
- 大致經歷了工具類別庫、組件庫、前端框架、前端應用的變遷
2.1.CommonJS規范
- CommonJS的出發點:規范薄弱,以下缺陷(沒有模塊系統、標準庫較少、沒有標準介面、缺乏包管理系統)
- CommonJS的模塊規范:主要分為模塊參考、模塊定義和模塊標識3個部分
2.2.Node的模塊實作
- 優先從快取加載:瀏覽器僅僅快取檔案,而Node快取的是編譯和執行之后的物件
- 路徑分析和檔案定位:Node 會按.js、.json、.node的次序補足擴展名
- 模塊編譯:每一個編譯成功的模塊都會將其檔案路徑作為索引快取在Module._cache物件上;在編譯的程序中,Node對獲取的JavaScript檔案內容進行頭尾包裝;(function(exports, require, module, __filename, __dirname) {\n, 在尾部添加了\n});C/C++模塊,Node呼叫process.dlopen()方法進行加載和執行;
2.3.核心模塊
- JavaScript核心模塊的編譯程序:C/C++檔案放在Node專案的src目錄下,JavaScript檔案存放在lib目錄下;編譯程式需要將所有的JavaScript模塊檔案編譯為C/C++代碼;
- C/C++核心模塊的編譯程序:Node在啟動時,會生成一個全域變數process,并提供Binding()方法來協助內建模塊
2.4.C/C++擴展模塊
- 說明:JavaScript的一個典型弱點就是位運算;*nix下通過g++/gcc等編譯器編譯為動態鏈接共享物件檔案.so,Windows下則需要通過VisualC++的編譯器編譯為元件檔案.dll;
- 前提條件:GYP專案生成工具、V8引擎C++庫、libuv庫、Node內部庫、等等
- C/C++擴展模塊的撰寫:普通的擴展模塊與內建模塊的區別在于無需將源代碼編譯進Node,而是通過dlopen()方法動態加載
- C/C++擴展模塊的編譯:寫好.gyp專案檔案是除編碼外的頭等大事;編譯程序會根據平臺不同,分別通過make或者vcbuild進行編譯;
- C/C++擴展模塊的加載:require()方法通過決議識別符號、路徑分析、檔案定位,然后加載執行即可;加載.node檔案實際上經歷了兩個步驟,第一個步驟是掉用uv_dlopen方法去打開元件,第二個步驟呼叫uv_dlsym()方法找到元件中通過NODE_MODULE宏定義的方法地址;
2.5.模塊呼叫堆疊
- JavaScript核心模塊主要扮演的職責有兩類:一類是作為C/C++內建模塊的封裝層和橋接層,供檔案模塊呼叫;一類是純粹的功能模塊,不需要跟底層打交道
2.6.包和NPM
- 包描述檔案與NPM:包規范的定義可以幫助Node解決依賴包安裝的問題;NPM實際需要的欄位主要有name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies;
- NPM常用功能:NPM幫助完成了第三方模塊的發布、安裝和依賴;查看幫助npm、分析包npm ls;
- 局域NPM:能夠享受到NPM上眾多的包,同時對自己的包進行保密和限制
- NPM潛在問題:開發人員水平不一,包質量也良莠不齊;NPM模塊首頁上的依賴榜可以說明模塊的質量和可靠性;GitHub上專案的觀察者數量和分支數量從側面反映模塊的可靠性和流行度;計劃引入CPAN社區中的Kwalitee風格來讓模塊進行自然排序;
2.7.前后端共用模塊
- 模塊的側重點:前端通過網路加載代碼,瓶頸在于帶寬,后端從磁盤加載,瓶頸在于CPU和記憶體等資源
- AMD規范:AMD模塊需要用define來明確定義一個模塊,而在Node實作中是隱式包裝的
- CMD規范:CMD與AMD規范的主要區別在于定義模塊和依賴引入的部分
- 兼容多種模塊規范:包裝兼容Node、AMD、CMD以及常見的瀏覽器環境中
第三章 異步IO
- 伴隨著異步I/O的還有事件驅動和單執行緒
3.1.為什么要異步I/O
- 用戶體驗:I/O是昂貴的,分布式I/O是更昂貴的
- 資源分配:利用單執行緒,遠離多執行緒死鎖、狀態同步等問題;利用異步I/O,讓單執行緒遠離阻塞,以更好的使用CPU
3.2.異步I/O與實作現狀
- 異步I/O在Node中應用最為廣泛,但是它并非Node的原創
- 異步I/O與非阻塞I/O:作業系統內核對于I/O只有兩種方式:阻塞與非阻塞;現存的輪詢技術主要有:read,select,poll,epoll,kequeue(read:重復呼叫來檢查I/O的狀態來完成完整資料的讀取;select:通過對檔案描述符上的事件狀態來進行判斷;poll:采用鏈表的方式避免陣列長度的限制,其次能避免不需要的檢查;epoll:Linux下效率最高的I/O事件通知機制;kqueue:與epoll類似,不過僅在FreeBSD系統下存在)
- 理想的非阻塞異步I/O
- 現實的異步I/O:在Node中,無論是*nix還是Windows平臺,內部完成I/O任務的另有執行緒池
3.3.Node的異步I/O
- 事件回圈:Node自身的執行模型:事件回圈
- 觀察者:在Node中,事件主要來源于網路請求、檔案I/O等;在Windows下,這個回圈基于IOCP創建,而在*nix下則基于多執行緒創建;
- 請求物件:從JavaScript發起呼叫到內核執行完I/O操作的過渡程序中存在的一種中間產物
- 執行回呼:事件回圈、觀察者、請求物件、I/O執行緒池這四者共同構成Node異步I/O模型的基本要素;完成異步I/O的程序(Windows下通過IOCP向系統內核發送I/O呼叫和從內核獲取已完成的I/O操作,配以事件回圈;Linux下通過epoll實作;FreeBSD下通過kqueue實作;Solaris下通過Event ports實作)
3.4.非I/O的異步API
- 與I/O無關的異步API:setTimeout()、setInterval()、setImmedate()、process.nextTick()
- 定時器:實作原理與異步I/O比較類似,只是不需要I/O執行緒池的參與;問題在于并非精確的;
- process.nextTick():采用定時器需要動用紅黑樹,創建定時器物件和迭代等操作;定時器中采用紅黑樹的操作時間復雜度為O(lg(n)),nextTick()的時間復雜度為O(1);
- setImmediate():process.nextTick()中的回呼函式執行的優先級要高于setImmediate();process.nextTick()屬于idle觀察者,setImmediate()屬于check觀察者;
3.5.事件驅動與高性能服務器
- Node無需為每一個請求創建額外的對應執行緒
- 不受執行緒背景關系切換開銷的影響
- 一些知名的基于事件驅動的實作:Ruby的Event Machine;Perl的AnyEvent;Python的Twisted;
第四章 異步編程
- Node是首個將異步大規模帶到應用層面的平臺
4.1.函式式編程
- 高階函式:高階函式是可以把函式作為引數,或是將函式作為回傳值的函式
- 偏函式用法:偏函式用法是指創建一個呼叫另外一個部分引數或變數已經預置的函式的函式的用法;通過指定部分引數來產生一個新的定制函式的形式就是偏函式;
4.2.異步編程的優勢與難點
- 優勢:Node的異步模型和V8的高性能
- 難點:例外處理、函式嵌套、阻塞代碼、多執行緒編程、異步轉同步
4.3.異步編程解決方案
- 事件發布/訂閱模式:如果一個事件添加了超過10個偵聽器將會得到一條警告,使用emitter.setMaxListeners(0)去掉限制
- Promise/Deferred模式:Deferred主要是用于內部,用于維護異步模型的狀態;Promise則作用與外部,通過then()方法暴露給外部以添加自定義邏輯;Promise/Deferred模式將業務中不可變的部分封裝在了Deferred,將可變的部分交給Promise;
- 流程控制庫:尾觸發與Next,目前應用最多的地方是Connect的中間件;async,長期占據NPM依賴榜的前三名,series實作異步的串行執行,parallel實作異步的并行執行,waterfall實作異步呼叫的依賴處理;step,更輕量;wind,思路完全不同的異步編程方案;
4.4.異步并發控制
- 同步I/O,每個I/O都是彼此阻塞的,不會出現耗用檔案描述符太多的情況
- bagpip的解決方案:bagpipe模塊的解決思路(通過一個佇列來控制并發量;呼叫發起但未執行的異步呼叫量小于限定值,從佇列中取出執行;如果活躍呼叫達到限定值,呼叫暫時存放在佇列中;每一個異步呼叫結束時,從佇列中取出新的異步呼叫執行;);拒絕訪問;超時控制;
- async的解決方案:async中parallelLimit()用于處理異步呼叫的限制
第五章 記憶體控制
- 基于無阻塞、事件驅動建立的Node服務,具有記憶體消耗低的優點,非常適合處理海量的網路請求
5.1.V8的垃圾回識訓制與記憶體限制
- Node與V8
- V8的記憶體限制:只能使用部分記憶體(64位系統下約為1.4G,32位系統下約為0.7G)
- V8的物件分配:V8依然提供選項讓我們使用更多的記憶體;--max-old-space-size設定老生代記憶體空間的最大值;--max-new-space-size設定新生代記憶體空間大小;
- V8的垃圾回識訓制:垃圾回收策略主要基于分代式垃圾回識訓制;在分代的基礎上,新生代的物件主要通過Scavenge演算法進行垃圾回收;V8在老生代中主要采用Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收;
- 查看垃圾回收日志:在啟動時添加--trace-gc引數;node啟動時使用--prof引數,可以得到V8執行時的性能分析資料;提供linux-tick-processor工具用于統計日志資訊;
5.2.高效使用記憶體
- 作用域:能形成作用域的有函式呼叫、with以及全域作用域
- 閉包:實作外部作用域訪問內部作用域中變數的方法
- 無法立即回收的記憶體有閉包和全域變數參考這兩種情況
5.3.記憶體指標
- 查看記憶體使用情況:process.memoryUsage()可以看到Node行程的記憶體占用情況;os模塊的totalmem()和freemem()查看系統的總記憶體和閑置記憶體;
- 堆外記憶體:不通過V8分配的記憶體稱為堆外記憶體;利用堆外記憶體可以突破記憶體限制的問題;
5.4.記憶體泄露
- 造成記憶體泄露的原因幾個:快取、佇列消費不及時、作用域未釋放
- 慎將記憶體當作快取:在Node中任何試圖拿記憶體當快取的行為都應當被限制
- 關注佇列狀態:使任何異步呼叫的回呼都具備可控的回應時間
5.5.記憶體泄露排查
- 常見的用于定位Node應用記憶體泄露的工具:v8-profiler、node-headpdump、node-mtrace、dtrace、node-memwatch
- node-headdump
- node-memwatch
5.6.大記憶體應用
- Node提供了stream模塊用于處理大檔案
- 要小心,即使V8不限制堆記憶體的大小,物理記憶體依然有限制
第六章 理解Buffer
6.1.Buffer結構
- 模塊結構:Buffer是一個像Array的物件,但它主要用于操作物件
- Buffer物件:
buf[10]的元素值是一個0到255的隨機值 - Buffer記憶體分配:Buffer物件的記憶體分配不是在V8的堆記憶體中,而是在Node的C++層面實作記憶體的申請的;Node以8KB為界限來區分Buffer是大物件還是小物件;真正的記憶體是在Node的C++層面提供的,JavaScript層面只是使用它;
6.2.Buffer的轉換
- 目前支持的字串編碼型別:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex
- 字串轉Buffer
- Buffer轉字串
- Buffer不支持的編程型別:Buffer提供一個isEncoding()函式來判斷編碼是否支持轉換;iconv和iconv-lite兩個模塊可以支持更多的編碼型別轉換;
6.3.Buffer的拼接
- 亂碼是如何產生的
- setEncode()與string_decoder()
- 正確拼接Buffer:呼叫Buffer.concat()方法生成一個合并的Buffer物件
6.4.Buffer與性能
- Buffer在檔案I/O和網路I/O中運用廣泛
- Buffer是二進制資料,字串與Buffer之間存在編碼關系
第七章 網路編程
- Node提供了net、dgram、http、https這4個模塊,分別用于處理TCP、UDP、HTTP、HTTPS
7.1.構建TCP服務
- TCP
- 創建TCP服務器端
- TCP服務的事件:TCP套接字是可寫可讀的Stream物件,可以利用pipe()方法巧妙的實作管道操作
7.2.構建UDP服務
- 創建UDP套接字
- 創建UDP服務器端
- 創建UDP客戶端
- UDP套接字事件:UDP套接字只是一個EventEmitter的實體,而非Stream的實體
7.3.構建HTTP服務
- HTTP
- http模塊:TCP服務以connection為單位進行服務,HTTP服務以request為單位進行服務
- HTTP客戶端
7.4.構建WebSocket服務
- WebSocket握手
- WebSocket的資料傳輸
7.5.網路服務與安全
- Node在網路安全上提供了3個模塊,分別為crypto、tls、https
- TLS/SSL
- TLS服務
- HTTPS服務
第八章 構建Web應用
8.1.基礎功能
- 請求方法
- 路徑決議
- 查詢字串
- Cookie:Cookie處理的幾步(服務器向客戶端發送cookie;瀏覽器將Cookie保存;之后每次瀏覽器都會將Cookie發向服務器端)
- Session:常見的兩種實作方式(基于Cookie來實作用戶和資料的映射;通過查詢字串來實作瀏覽器端和服務器端資料的對應);Connect默認采用connect_uid,Tomcat會采用jsessionid等;
- 快取:提高性能,YSlow中提到的幾條關于快取的規則(添加Expires或Cache-Control到報文中;配置ETags;讓Ajax可快取)
- Basic認證:通過Base64加密后在網路中傳送,有太多的缺點
8.2.資料上傳
- 表單資料:通過報頭的Transfer-Encoding或Content-Length即可判斷請求中是否帶有內容
- 附件上傳:瀏覽器在遇到multipart/form-data表單提交時,構造的請求報文與普通表單完全不同;formidable,基于流式處理決議報文,將接收到的檔案寫入到系統的臨時檔案夾中,并回傳對應的路徑;
- 資料上傳與安全
8.3.路由決議
- 檔案路徑型
- MVC
- RESTful
8.4.中間件
- 例外處理
- 中間件與性能
- 從凌亂的發散狀態收斂成很規整的組織方式
8.5.頁面渲染
- 內容回應:不同的檔案型別具有不同的Mime值;Content-Disposition欄位影響的行為是客戶端會根據它的值判斷是應該將報文資料當作即時瀏覽的內容,還是可下載的附件;
- 視圖渲染
- 模板:實質就是將模板檔案和資料通過模板引擎生成最終的HTML代碼;形成模板技術4個要素(模板語言;包含模板語言的模板檔案;擁有動態資料的資料物件;模板引擎);mustache,弱邏輯的模板;最知名的有EJS、Jade等;
- Bigpipe:用于呼叫限流,解決重資料頁面的加載速度問題;解決思路是將頁面分割成多個部分,先向用戶輸出沒有資料的布局,將每個部分逐步輸出到前端,再最渲染填充框架,完成整個網頁的渲染;
第九章 玩轉行程
9.1.服務模型的變遷
- 石器時代:同步 - 只在一些無并發要求的應用中存在
- 青銅時代:復制行程 - 要復制較多的資料,啟動是較為緩慢的
- 白銀時代:多行程 - 時間將會被耗用在背景關系切換中
- 黃金時代:事件驅動 - 記憶體耗用的問題著名的C10k問題;單執行緒避免了不必要的記憶體開銷和背景關系切換開銷;
9.2.多行程架構
- child_process.fork()復制的都是一個獨立的行程,獨立而全新的V8實體
- 啟動多個行程只是為了充分將CPU資源利用起來,而不是為了解決并發問題
- 創建子行程
- 行程間通信:JavaScript主執行緒與UI渲染共用同一個執行緒;實作行程間通信的技術有很多,如命名管道、匿名管道、socket、信號量、共享記憶體、訊息佇列、Domain Socket等;作業系統的檔案描述符是有限的;
- 句柄傳遞:句柄是一種可以用來標識資源的參考,它的內部包含了指向物件的檔案描述符;檔案描述符實際上是一個整數值;Node行程之間只有訊息傳遞,不會真正的傳遞物件;
9.3.集群穩定之路
- 行程事件
- 自動重啟:創建新作業行程在前,退出例外行程在后
- 負載均衡:輪叫調度的作業方式是由主行程接受連接,將其依次分發給作業行程
- 狀態共享:解決資料共享最簡單、直接的方式就是通過第三方進行資料存盤;主動通知;
9.4.Cluster模塊
- Cluster作業原理:事實上是child_process和net模塊的組合應用
- Cluster事件
第十章 測驗
10.1.單元測驗
- 撰寫可測驗代碼的幾個原則:單一職責、介面抽象、層次分離;
- 單元測驗主要包含斷言、測驗框架、測驗用例、測驗覆寫率、mock、持續繼承等,由于Node的特殊性,還會加入異步代碼測驗和私有方法的測驗;
- JavaScript的斷言規范最早來自于CommonJS的單元測驗規范;
- 單元測驗風格主要有TDD(測驗驅動開發)和BDD(行為驅動開發)兩種;
- BDD對測驗用例的組織主要采用describe和it進行組織;
- TDD對測驗用例的組織主要采用suite和test完成;
- 單元測驗覆寫率方便我們定位沒有測驗到的代碼行;
- 私有方法的測驗(Java一類的語言,私有方法訪問可以通過反射的方式實作;巧妙利用閉包的訣竅,在eval()執行時,實作對模塊內部區域變數的訪問,從而可以將區域變數匯出給測驗用例呼叫執行;)
10.2.性能測驗
- 單元測驗主要用于檢測代碼的行為是否符合預期,性能測驗的范疇比較廣泛,包括負載測驗、壓力測驗和基準測驗
- 基準測驗
- 壓力測驗:最常用的工具是ab、siege、http_load等
- 基準測驗驅動開發
- 測驗資料與業務資料的轉換
第十一章 產品化
11.1.專案工程化
- 目錄結構
- 構建工具:在Web應用中通常會在Makefile檔案中撰寫一些構建任務來幫助提升效率;合并編譯、應用打包、運行測驗、清理目錄、掃描代碼等;
- 編碼規范:一種是檔案式的約定,一種是代碼提交時的強制檢查
- 代碼審查
11.2.部署流程
- 部署環境
- 部署操作
11.3.性能
- 幾個拆分原則:做專一的事;讓擅長的工具做擅長的事情;將模型簡化;將風險分離;
- 動靜分離
- 啟動快取
- 多行程架構
- 讀寫分離:進行資料庫的讀寫分離,將資料庫進行主從設計
11.4.日志
- 訪問日志
- 例外日志:log與info方法都將資訊輸出給標準輸出process.stdout,warn與error方法則將資訊輸出到標準錯誤process.stderr;console物件上有個Console屬性,它是console物件的建構式;回呼函式中產生的例外,交給全域的uncaughtException事件去捕獲;
- 日志與資料庫
- 分割日志
11.5.監控報警
- 監控:一種是業務邏輯型的監控,一種是硬體型的監控;主要指標:日志監控、回應時間、行程監控、磁盤監控、記憶體監控、CPU占用監控、CPU load監控、I/O負載、網路監控、應用狀態監控、DNS監控;
- 報警的實作
- 監控系統的穩定性
11.6.穩定性
- 典型的水平擴展方式就是多行程、多機器、多機房
寫在后面
- pdf書籍、筆記思維導圖、隨書代碼打包下載地址:https://pan.baidu.com/s/1OhLjjtfffjX3hv2_Pw7AHQ(提取碼:9m33)
- 紙質書京東購買地址:https://u.jd.com/wHmeh4(推薦購買紙質書來學習)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/85304.html
標籤:JavaScript
