Eloquent JavaScript #10# Modules

Notes

一、背景問題npm

理想的程序:相似於樂高玩具。它具備清晰的結構,工做方式很容易解釋,每一個部分都扮演着明確的角色。編程

現實的程序:有機地增加。隨着新需求的出現,新功能被添加。結構化和維持結構化是服務於將來的工做,所以很容易忽略它並逐漸讓程序的各個部分變得很是糾結。數組

形成的問題:首先,理解這樣的一個系統很困難。其次,沒法對系統的某部分功能重用,與其把該功能從上下文提取出來,不如重寫。簡而言之就是高度耦合。數據結構

 

二、模塊Modulesapp

目的:解決高度耦合的問題ide

模塊的定義:a piece of program,指明瞭自身所依賴的程序,以及它向外部所提供的功能(interface),其他部分保密。模塊化

模塊間的關係:依賴(dependencies)。當模塊的依賴被定義在它自身時,就可使用它來肯定須要存在哪些其餘模塊才能使用給定模塊並自動加載依賴項。函數

如何實現模塊化:首先須要程序員有這個意識,其次須要實際編程上的一些輔助措施。

 

三、軟件包Packages

定義:一大塊能夠發佈(複製和安裝)的代碼。它可能包含一個或多個模塊,而且包含有關其所依賴的其餘軟件包的信息。一個軟件包一般還附帶文檔來解釋它的功能,以便那些沒有編寫它的人仍然可使用它。

針對的問題:咱們能夠經過Copy代碼來複用一些函數、功能,可是當這些函數、功能更新,就不得不在每一處修改它。若是在程序包中發現問題或添加了新功能,則依賴它的程序(也多是包)只須要更新程序包就能夠了。

基礎設施:以這種方式工做須要基礎設施。咱們須要一個存儲和查找包的地方以及安裝和升級它們的便捷方式。在JavaScript世界中,此基礎結構由NPM(https://npmjs.org)提供。

 

四、簡易模塊

把js代碼放在不一樣文件中不能知足需求,不一樣的文件一樣共享相同的全局命名空間。它們之間會相互影響,而且代碼的依賴結構不夠清晰。

直到2015年js都沒有內建的模塊系統。因此最初的模塊系統都是本身設計的:

const weekDay = function() {
  const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
                 "Thursday", "Friday", "Saturday"];
  return {
    name(number) { return names[number]; },
    number(name) { return names.indexOf(name); }
  };
}();

console.log(weekDay.name(weekDay.number("Sunday")));
// → Sunday

如上,該模塊的接口包括weekDay.name和weekDay.number,局部綁定被隱藏在函數做用域裏。

這種方式提供了必定程度的獨立性,但它只指明瞭向外部提供的功能(interface),並無指明自身須要的依賴,只是指望於外部環境可以提供這些依賴,更別說什麼自動加載了。(固然這裏的例子並不須要其它依賴)

很長一段時間,這是Web編程中使用的主要方法,但如今它已通過時了。

理想的模塊系統應該相似於Java裏的包系統,能夠經過import指明所需的依賴以及控制依賴的加載。

 

五、Evaluating data as code

若是咱們但願依賴關係成爲代碼的一部分(能夠類比Java的import),就必須用代碼來控制依賴的加載。作到這一點須要可以把字符串(或者說數據)做爲代碼執行【???】

首先是一種不推薦方式——特殊運算符eval,它容易破壞scope中原有的一種屬性(就是缺少封閉性):

const x = 1;
function evalAndReturnX(code) {
  eval(code);
  return x;
}

console.log(evalAndReturnX("var x = 2"));
// → 2
console.log(x);
// → 1

採用Function的構造器是一種風險較低的方法,它把代碼包裝在函數值裏,這樣它就有本身的做用域(scope)而不會影響別的做用域了:

let plusOne = Function("n", "return n + 1;");
console.log(plusOne(4));
// → 5

這正是咱們的模塊系統所須要的,咱們能夠把模塊代碼包裝在一個函數裏,函數的做用域便是模塊的做用域。

 

六、CommonJS modules

CommonJS modules是最經常使用的用於規範化js模塊的模塊。

CommonJS modules的核心概念是一個叫require的函數,當你傳入一個模塊名並調用這個函數時,它確保模塊已經被加載並返回該模塊提供的接口。

下面是一個模塊實現示例(直接腦補成java裏的import package就很好懂了):

const ordinal = require("ordinal"); // 模塊的依賴
const {days, months} = require("date-names"); // 模塊的依賴

exports.formatDate = function(date, format) { // 模塊對外提供的接口,能夠是一個函數,也能夠是像date-names同樣多個函數
  return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
    if (tag == "YYYY") return date.getFullYear();
    if (tag == "M") return date.getMonth();
    if (tag == "MMMM") return months[date.getMonth()];
    if (tag == "D") return date.getDate();
    if (tag == "Do") return ordinal(date.getDate());
    if (tag == "dddd") return days[date.getDay()];
  });
};

模塊把它的接口函數綁定到exports上,以便其它模塊能夠訪問它:

const {formatDate} = require("./format-date"); // 訪問剛定義的formatDate模塊

console.log(formatDate(new Date(2017, 9, 13),
                       "dddd the Do"));
// → Friday the 13th

咱們能夠自定義一個輕量級的require:

require.cache = Object.create(null);

function require(name) {
    if(!(name in require.cache)) { // 防止重複加載
        let code = readFile(name); // readFile並不是標準函數,須要自定義
        let module = {
            exports: {}
        }; 
        require.cache[name] = module;
        let wrapper = Function("require, exports, module", code); // 加載代碼
        wrapper(require, module.exports, module); // 這樣模塊接口就被綁定到module.exports了
    }
    return require.cache[name].exports; // => module.exports
}

舉個例子(模擬):

const codeOfPlusOne = "exports.plusOne = n => n + 1;";
const readFile = (name) => {
    if (name == "plusOne") return codeOfPlusOne;
}

require.cache = Object.create(null);

function require(name) {
    if(!(name in require.cache)) { // 防止重複加載
        let code = readFile(name); // readFile並不是標準函數,須要自定義
        let module = {
            exports: {}
        }; 
        require.cache[name] = module;
        let wrapper = Function("require, exports, module", code); // 加載代碼
        wrapper(require, module.exports, module); // 這樣模塊接口就被綁定到module.exports了
    }
    return require.cache[name].exports; // => module.exports
}

const {plusOne} = require("plusOne");
console.log(plusOne(5));
/**
 * 一、檢查"plusOne"是不是require.cache的一個屬性
 * 二、若是是,直接返回require.cache["plusOne"].exports
 * 三、若是不是,經過readFile讀取plusOne模塊的代碼
 * 四、require.cache["plusOne"]綁定module(含有一個空對象的exports)
 * 五、wrapper實際變成下面那樣:加載-> 函數綁定到require.cache["plusOne"]
 */

對wrapper的分析:

// 1.構造wrapper,加載代碼
let wrapper = (require, exports, module) => {
    // 模塊內容↓
    // const ordinal = require("ordinal"); 
    exports.plusOne = n => n + 1;
}
// 2.調用wrapper,實際綁定
wrapper(require, module.exports, module);
// exports.plusOne = f(x);
// => ... module.exports = f(x);

 

七、ECMAScript modules(since 2015)

雖然CommonJS modules已經足夠好用,但仍是有那麼一點瑕疵,例如:你添加到exports的東西在局部做用域竟然不可用

這就是爲何js要推出本身的模塊系統:

import ordinal from "ordinal";
import {days, months} from "date-names";

export function formatDate(date, format) { /* ... */ }

主要概念保持不變,可是細節有些不一樣。符號如今已整合到語言中。您可使用特殊import關鍵字,而不是調用函數來訪問依賴項。

export的再也不是函數,而是一系列的綁定。

把模塊import到沒有{ }包圍的綁定時,返回模塊的default綁定(須要自定義):

export default ["Winter", "Spring", "Summer", "Autumn"];

還能夠對模塊進行重命名:

import {days as dayNames} from "date-names";

console.log(dayNames.length);

 

八、Building and bundling

不少js代碼其實不是用js寫的,而是其它語言編譯過來的。

由於單個文件傳輸比較快,所以程序員一般會在發佈代碼前用一種被叫作bundlers的工具把n個js文件壓縮成一個js文件。

除了文件數量,文件大小一樣影響傳輸速率,能夠經過叫minifiers的工具去除空格和註釋。

總而言之: Just be aware that the JavaScript code you run is often not the code as it was written.

 

九、模塊設計建議

  • 良好的程序設計是主觀的 - 涉及權衡和品味問題。
  • 模塊設計的一個方面是易於使用,這可能意味着遵循現有的慣例,模仿標準功能或普遍使用的軟件包是一個好主意。
  • 保持模塊功能單一。「Even if there’s no standard function or widely used package to imitate, you can keep your modules predictable by using simple data structures and doing a single, focused thing. 」,並且模塊的功能越是單1、通用,越是易於和其它模塊組合。
  • 有時定義狀態對象是有用的,但若是函數足夠,就用一個函數。「您​​首先建立一個對象,而後將該文件加載到您的對象中,最後使用專門的方法來得到結果。這種東西在面向對象的傳統中很常見,並且很糟糕。您不能調用單個函數並繼續前進,而是必須執行將對象移動到各類狀態的儀式。由於數據被包裝在一個專門的對象類型中,因此與它交互的代碼必須知道該類型,從而產生沒必要要的相互依賴性。」
  • 一般沒法避免定義新的數據結構,可是當一個數組足夠時,使用一個數組。

 

Exercises

① A modular robot

我會怎麼作:把各個函數變得更加通用、獨立。。

------- --------  ———— -- —— ——- --  -- - -- - -

② Roads module

// Add dependencies and exports
const {buildGraph} = require("./graph"); 

const roads = [
    "Alice's House-Bob's House", "Alice's House-Cabin",
    "Alice's House-Post Office", "Bob's House-Town Hall",
    "Daria's House-Ernie's House", "Daria's House-Town Hall",
    "Ernie's House-Grete's House", "Grete's House-Farm",
    "Grete's House-Shop", "Marketplace-Farm",
    "Marketplace-Post Office", "Marketplace-Shop",
    "Marketplace-Town Hall", "Shop-Town Hall"
];

exports.roadGraph = buildGraph(roads.map(r => r.split("-")));

------- --------  ———— -- —— ——- --  -- - -- - -

③ Circular dependencies

暫略。

相關文章
相關標籤/搜索