手把手教你寫一個 Javascript 框架:項目結構
過去幾個月中,RisingStack 的 JavaScript 工程師 Bertalan Miklos 編寫了新一代客戶端框架 NX 。Bertalan 將通過 編寫 JavaScript 框架 系列文章與我們分享他在編寫框架過程中的收獲:
本章將展示 NX 的項目結構,并講述如何解決可擴展性、依賴注入以及私有變量等方面的一些困難。
本系列章節如下:
- 項目結構(正是本文)
- 執行調度(Execution timing)
- 沙箱求值
- 數據綁定簡介
- ES6 Proxy 實現數據綁定
- 自定義元素
- 客戶端路由
項目結構
沒有放之四海而皆準的項目結構,但有一些基本準則。感興趣的同學可以看下我們的 Node Hero 系列中的《 Node.js 項目結構教程 》這一章。
NX 框架概覽
NX 的目標是成為一個開源社區驅動的易于擴展的項目。項目特點如下:
- 包含現代客戶端框架必須的所有特性;
- 除 polyfill 外,沒有任何外部依賴;
- 代碼總量 3000 行;
- 沒有代碼多于 300 行的模塊;
- 單個特性模塊依賴不超過 3 個。
項目各模塊依賴關系如下圖所示:

這種結構為典型框架開發難題提供了一種解決方案。
- 擴展性
- 依賴注入
- 私有變量
可擴展性實現
社區驅動項目必須易于擴展。故項目的核心部分應當小巧,并擁有一個預定義的依賴處理系統。前者確保項目易于理解,后者則保證框架穩定。
本節先聚焦于實現小巧的內核。
現代框架應當擁有的主要特性就是創建自定義元素并將其應用于 DOM 的能力。NX 的核心只有一個 component 函數,它的工作正在于此這個函數允許用戶配置、注冊一個新類型的元素。
component(config)
.register('comp-name')
注冊的 comp-name 是空組件類型,可以按照預期在 DOM 中實例化。
<comp-name></comp-name>
下一步是保證能使用新特性擴展組件。為保持簡潔、可擴展,這些新特性不應該污染核心部分。這時候使用依賴注入就很方便了。
利用中間件實現依賴注入(DI)
依賴注入是一種設計模式,在這種模式中,一個或多個依賴或服務被注入到或引用傳遞給一個獨立對象。
DI 解決了硬性依賴,卻引入了新問題。使用者需要知道如何配置、注入依賴。大多客戶端框架都將這些工作交給 DI 容器,幫助開發者完成。
DI 容器指的是知道如何實例化、配置對象的對象。
另外一種方式則是中間件 DI 模式,這在服務端得到廣泛應用(如 Express、Koa 等)。其中的奧秘在于,所有可注入的依賴(中間件)擁有相同的接口,以相同方式注入。這種方法則無需 DI 容器。
為保持簡潔,我決定采用中間件模式。若你曾使用過 Express,以下代碼自然不會陌生:
component()
.use(paint) // inject paint middleware
.use(resize) // inject resize middleware
.register('comp-name')
function paint (elem, state, next) {
// elem is the component instance, set it up or extend it here
elem.style.color = 'red'
// then call next to run the next middleware (resize)
next()
}
function resize (elem, state, next) {
elem.style.width = '100 px'
next()
}
中間件在新的組件實例插入 DOM 時執行,通常會給實例擴展一些新特性。如若不同庫擴展相同對象,則將導致名稱沖突。暴露私有變量會加劇問題,并可能被其他人意外利用。
公開 API 小巧玲瓏,其余部分隱身不見,正是避免問題的優秀方案。
處理私有變量
JavaScript 中需要通過函數作用域來實現私有變量。需要使用跨作用域私有變量時,人們習慣使用 _ 前綴來標志,并將其公開暴露。這可以避免意外使用,但無法解決命名沖突。更好的辦法是使用 ES6 的 Symbol 基本數據類型。
Symbol 是一種唯一的、不可變的數據類型,可用作對象屬性標識符。
下面的代碼展示了 symbol 的實際使用:
const color = Symbol()
// a middleware
function colorize (elem, state, next) {
elem[color] = 'red'
next()
}
這樣一來,通過 color symbol (以及元素 elem)就能獲取 red 。 red 的私有程度,可由 color symbol 暴露的不同程度控制。合理數量的私有變量,通過中心存儲讀取,是一種優雅的解決方案。
// symbols module
exports.private = {
color: Symbol('color from colorize')
}
exports.public = {}
index.js 如下:
// main module
const symbols = require('./symbols')
exports.symbols = symbols.public
在項目內部,所有模塊都可訪問 symbol 存儲對象,但私有部分不會對外暴露。公有部分則可用于對外部開發者暴露一些低層次特性。這就避免了意外使用,因為開發者需要明確引入需要使用的 symbol。此外,symbol 引用也不會像字符串一樣產出沖突,是故不會產生命名沖突。
以下幾點概括了不同場景下的用法:
1. 公有變量
正常使用.
function (elem, state, next) {
elem.publicText = 'Hello World!'
next()
}
2. 私有變量
項目私有的跨作用域變量,應當在私有 symbol 對象中加入一個 symbol key。
// symbols module
exports.private = {
text: Symbol('private text')
}
exports.public = {}
并在需要的地方引入。
const private = require('symbols').private
function (elem, state, next) {
elem[private.text] = 'Hello World!'
next()
}
3. 半私有變量
低層次 API 的變量,應當在公有 symbol 對象中加入一個 symbol key。
// symbols module
exports.private = {
text: Symbol('private text')
}
exports.public = {
text: Symbol('exposed text')
}
并在需要的地方引入。
const exposed = require('symbols').public
function (elem, state, next) {
elem[exposed.text] = 'Hello World!'
next()
}
來自:http://www.zcfy.cc/article/writing-a-javascript-framework-project-structuring-risingstack-1675.html