2018-09-25 學習手記:JavaScript Modules 和 Express Middleware
今天仍然是以學習公司後端專案程式碼為主,學習了有關 JavaScript Module 和 Express Middleware 的相關內容。誠如 9 月 19 日學習手記所言,由於後端程式碼模組化非常深,所以我這個渣渣學習起來比較費時間。不過,通過今天的學習,對於專案的架構有了更深入的認識,接下來先介紹下JavaScript Module 和 Express Middleware 兩個概念。
JavaScript Modules
In JavaScript, the word “modules” refers to small units of independent, reusable code.
ofollow,noindex">JavaScript ModulesA module encapsulates related code into a single unit of code.
Understanding Module Exports Exports Nodejs從上面的描述可以瞭解,對於 JavaScript 而言,模組 Module 其實就是一段程式碼,而這段程式碼可以是物件、函式、HTML 模版或是 CSS 樣式。有關更多模組化的內容可以想見本次推送的另一篇佳作《從 include到 require - 論 node require 設計》,這裡不做更多討論。
Express Middleware
Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls. Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.
Express 是一個路由和中介軟體 Web 框架,其自身只具有最低程度的功能:Express 應用程式基本上是一系列中介軟體函式呼叫。
中介軟體函式能夠訪問請求物件 (req)、響應物件 (res) 以及應用程式的請求/響應迴圈中的下一個中介軟體函式。下一個中介軟體函式通常由名為 next 的變數來表示。
Express Middleware 型別
Application-level middleware
使用 app.use() 和 app.METHOD() 函式將應用層中介軟體繫結到應用程式物件的例項,其中 METHOD 是中介軟體函式處理的請求的小寫 HTTP 方法(例如 GET、PUT 或 POST)。比如,下面就是利用 app 的應用層中介軟體 use 函式。
var app = Express(); var router = require('./router'); var models = require('./models'); var controllers = require('./controllers'); app.use(BodyParser.json()); app.use(router);
Router-level middleware
路由器層中介軟體的工作方式與應用層中介軟體基本相同,差異之處在於它繫結到 express.Router() 的例項。
var router = express.Router();
Error-handling middleware
錯誤處理中介軟體函式的定義方式與其他中介軟體函式基本相同,差別在於錯誤處理函式有四個自變數而不是三個,專門具有特徵符 (err, req, res, next).
app.use(function(err, req, res, next){ console.error(err.stack); res.status(500).send('Something broke!'); });
Built-in middleware
Express 內建中介軟體包括:
- express.static 提供靜態資源服務,例如 HTML 檔案、圖片等。
- express.json 解析收到的 JSON 負載請求。NOTE: Express 4.16.0+ 可用。
- express.urlencoded 解析收到的 URL 編碼負載的請求。 NOTE: Express 4.16.0+ 可用。
app.use(express.static('public')); app.use(express.static('uploads')); app.use(express.static('files'));
Third-party middleware
使用第三方中介軟體向 Express 應用程式新增功能。
var Router = require('express-promise-router'); var c = require('../controllers'); var r = Router(); /// test r.get('/', c.test.hello); module.exports = r;
專案實踐
我比著公司專案的葫蘆畫了個小葫蘆,專案檔案目錄如下。
. |____bin | |____www |____models | |____user.js | |____index.js |____public | |____images | |____javascripts | |____stylesheets | | |____style.css |____package.json |____errors.js |____helpers.js |____controllers | |____test.js | |____index.js |____views | |____error.jade | |____index.jade | |____layout.jade |____app.js |____router | |____index.js
其中,與 Node Express Modules 程式設計的相關目錄/檔案是 models/、 controllers/ 、 router/* 、 helpers.js 和 app.js 等檔案。
app.js
其中 app.js 檔案主要進行模組的組裝,將拆分後的 Model、 Controller、 Router 等模組組裝在一起。
var Express = require('express'); var BodyParser = require('body-parser'); var PromiseRouter = require('express-promise-router'); var helpers = require('./helpers'); Object.assign(Express.request, helpers.Request); Object.assign(Express.response, helpers.Response); var app = Express(); var router = require('./router'); var models = require('./models'); var controllers = require('./controllers'); app.use(BodyParser.json()); app.use(router); module.exports = app;
controllers
index.js
index.js
是 controllers 模組的入口檔案,主要功能是動態的載入模組,有關動態載入模組歡迎閱讀另一篇由土豪金大佬寫的文章。
var fs = require('fs'); var c = {}; fs.readdirSync(__dirname) .filter(f => f.match(/\.js$/) && !f.match(/^_/) && f !== 'index.js') .forEach(f => { c[f.replace(/\.js$/, '')] = require('./' + f); }); module.exports = c;
上面這段程式碼主要的功能是,建立一個物件 c ,並利用 fs 將 controllers 目錄下的 js 程式檔案都動態地匯入進來,然後將匯入的程式模組作為一個 c 的例項屬性,這樣就可以直接用模組名訪問了。這段程式碼真的被驚豔到了,驚歎於 js 的動態性如此靈活。
test.js
test.js
是 controllers 模組中其中一個測試的模組程式檔案,如果 controllers 有多個這樣的程式模組檔案,入口檔案index.js
種的程式都會將這些程式檔案作為不同字模組匯入到控制器 c 的例項中。
exports.hello = async function(req, res){ res.send('hello ' + req.protocol); };
上面這段程式碼,就是利用 Express 的 Response 物件傳送 HTTP 響應結果,並輸出 “hello” 和 Express 的 Request 物件的網路協議。
models 模組
models 模組在上一篇手機中簡單介紹了 Sequelize 這個 ORM 框架,本文不過多介紹。models 的主要用途就是將程式資料模型,對映到資料庫中的關係資料中。 models 模組的模組匯入方法和 controllers 是一樣的,這裡也不做詳細介紹了。
router
router 的入口檔案index.js
主要是做路由的繫結,利用三方的路由模組,將使用者訪問的路由繫結到相應的 controllers 的模組程式呼叫。
var Router = require('express-promise-router'); var c = require('../controllers'); var r = Router(); /// test r.get('/', c.test.hello); module.exports = r;
這段程式碼將/
路由繫結到了c.test.hello
程式呼叫上,這樣當訪問根路由的時候,網頁就能顯示 “hello” 和 Express 的 Request 物件的網路協議了。
以上程式碼都編寫完成後,就可以到程式根目錄執行npm start
檢視程式執行效果。
Architecture
在上面的介紹中,我們已經基本學習了 JavaScript Modules 和 Express Middleware,並編寫程式碼實踐了 JavaScript 模組化程式設計。根據本文所提供的程式碼案例,我們可以得到一個簡單的專案架構,Router 和 Controllers 進行繫結,使用者訪問路由並呼叫 Controllers 中相應的模組方法,而在 Controllers 的模組方法中,可以呼叫 Models 的模組方法,最終由 Models 層對資料庫資料進行更新等操作。模組化程式設計的好處是將路由、控制層和模型層的程式碼分離,這樣使程式架構變得更加清晰。
在本次分享的《從 include 到 require - 論 node require 設計》文章中,更加詳細的比較了 JavaScript ES6 的標準模組匯入import
和 Nodejs 提供的require
方式的優缺點。在這篇文章中有如下的一些描述:
而 require 則完全是相反的路子,依賴完全是動態載入的。就像是事物的兩面,require 解決了靜態依賴的痛點,但同時動態化也使得 ruby 的語法提示變得困難。打包器也不得不以來於一個獨立的 Gemfile 來描述依賴管理。
…
但是同樣是 require ,Ryan 卻從 ruby 裡充分的吸取了歷史教訓:node require 會返回一個變數而不是修改本地名字空間。
…
另外 node require 還做了一個極具前瞻性的事情,就是把依賴安裝到了專案目錄。
…
就像是我上面說的,動態方案的最大缺陷就在於無法被簡單的靜態分析。當瀏覽器載入頁面如果需要反覆的暫停,等待網路請求,的確會極大的影響使用者體驗。這個從表明上來說貌似是一個非常“合理”的理由。我們暫且認為這個理由是合理的。但 es 標準化委員會在 import 的設計中犯的低階錯誤簡直無法忍受。
…
如果去掉 webpack 的,讓瀏覽器直接 import,完全就是不切實際的做法。一個網頁通常只有上百個圖示。但一個 lodash 都有 631 個檔案。如果按照 100ms 的延遲、10 個執行緒併發、啟用 tcp fast open、禁用 https,在這麼極端的情況下仍然需要 6.4 秒。這僅僅是一個 lodash。未經壓縮的 jQuery 有多少檔案,未壓縮的 d3 有多少檔案?一個頁面到時候 10 分鐘載入夠嗎?
以上這幾段文字,是我摘錄的動態匯入模組require
和 ES6 標準制定的import
模組匯入的相關對比描述。從本文之前的介紹中,我們可以瞭解到require
可以動態的載入模組,這樣對於程式而言就非常靈活,動態性發揮到極致,並不用將模組方法寫死到程式碼中,甚至線上部署的時候可以利用變數來控制模組的載入。而import
則只能靜態匯入模組,並且要將全部的程式模組檔案一次性匯入,這樣做會大大浪費網路請求的時間,從而降低使用者體驗。
但除此之外,擴充套件去看,解釋型語言和編譯型語言相比,效能確實不如編譯型語言。比如,我們一個 iOS App 當經過編譯打包之後,就可以安裝到手機上,當程式載入到記憶體中後就可以直接運行了。而對於 Web App 我們要先下載 HTML、JavaScript 等相關資源,然後,等 JS 的資源下載完成後瀏覽器的 JS 引擎才開始進行程式的解釋並執行。從這個方面看,客戶端開發應該還暫時不會丟飯碗。但如果未來 5G 網路速度又有了更大提升呢?以後網路速度優化更好,說不定真的就不需要 Client 了。