我曾經作過js講師,在個人任教過程當中,模塊系統一直是學生們的薄弱點。有一個充分的理由能夠解釋這個問題:模塊在javascript中有一段奇怪且不穩定的歷史。這篇文章咱們將討論這段歷史,而且,你講了解過去的模塊的相關知識,以更好的理解當前模塊的工做原理。
在學習如何在js中建立模塊以前,首先須要明白,模塊是什麼以及爲何會存在模塊。環顧你的周邊,你會發現,不少複雜的東西都是有一個個分離的部件組合在一塊兒構成,進而造成一個完整的東西。javascript
以一隻手錶爲例:html
能夠看到,一隻手錶由成百上千的內部部件組成,每個內部部件都有特定的功能和清晰地邊界以方便與其餘部件協做。把這些部件組合在一塊兒,就組成了這隻完整的手錶。我不是一個手錶製造業的專家,可是我由於這種方法的優勢是很是直觀的。java
若是你仔細的觀察一些上圖中的構造,你會發現有不少部件都是重複的。因爲這種模塊化爲中心的設計,手錶中的不一樣功能也能夠用到相同的部件。這種可複用部件的能力簡化的工做製造流程,而且提升的利潤。node
這種設計是可組合性的很是直觀的案例。經過制定每一個部件清晰地邊界,可以很好地組合每個部件,以創造一個功能齊全的手錶。react
設想一下製造過程,公司不會製造手錶,他們只是將這些部件拼接起來以產出一隻完整的手錶。他們能夠本身製做這些部件,也能夠將這些部件外包給其餘工廠,這不重要,重要的是這些部件組合在一塊兒就是一隻完整的手錶,而這些部件來自於哪是可有可無的。jquery
明白手錶的整個系統是很困難的,由於它由不少小而複雜的,功能專注的部件組成,每一個部件均可以單獨考慮,構造和修復。這種隔離性容許人們單獨工做,不會成爲彼此的負擔。而且,若是一個部件循環,僅僅須要更換這個部件看,而不是更換這隻表。webpack
組織是每一個獨立的擁有清晰邊界的部件爲了與其餘部件組合的副產品,伴隨着模塊化,天然就會出現這種狀況。git
隨着手錶這樣的結構不斷產出,咱們能夠愈來愈清晰地認識到模塊化的好處,那麼,若是咱們換成軟件領域呢?實際上是同樣的。就像手錶的設計同樣,軟件也應該被設計,分割成不一樣的具備特定功能的部件,而且具備爲了與其餘部件組合的清晰邊界。不過,在軟件中,這種部件被叫作模模塊。到如今爲止,模塊給咱們的感受可能與react組件和函數截然不同。那模塊到底包含什麼呢?angularjs
每個模塊都具備三部分:依賴,代碼內容還有導出。es6
當一個模塊須要其餘模塊的功能,它能夠import
這個模塊做爲依賴,例如,不管什麼什麼時候,你想建立一個react組件,你只須要import react
模塊,若是你想使用lodash
,你也只須要impiort lodash
模塊。
肯定好你的模塊須要的依賴以後,你就能夠開始編寫這個模塊
exports
是當前模塊的接口
,引入這個模塊的開發者可使用你導出的一切功能。
說了這麼多概念,下面讓咱們來點實際的代碼。
先來看一個react router的例子,方便起見,能夠看一下react提供的模塊目錄,在react router中合理的利用模塊,事實證實,在大多數狀況下,他們直接映射react組件到模塊,在react項目中分離組件是頗有意義的,從新審查上面的手錶結構,將部件換成組件一樣有意義。
來看一下MemoryRouter
模塊的代碼,如今不要關心代碼的含義,只須要集中在代碼的結構上。
// imports import React from 'react'; import { createMemoryHistory } from 'history'; import Router from './Router'; // code class MemoryRouter extends React.Component { history = createMemoryHistory(this.props); render() { return ( <Router history={this.history} children={this.props.children} />; ) } } //exports export default MemoryRouter;
你能夠注意到這個模塊的頂部定義了依賴,和一些使當前模塊正產工做的必需的模塊。接下來,能夠看到一些代碼。在這個例子中,建立了一個叫作MemoryRouter的新的react組件,最後,在底部定義了對外導出:MemoryRouter,也就是說,任何導入該模塊的模塊都會獲得MemoryRouter這個組件。
如今,咱們對軟件中的模塊有了一個淺顯的認識,讓咱們回顧一些手錶設計帶來的好處,在相同設計的軟件中有哪些能夠能夠直接應用。
由於模塊能夠在任何須要它的地方import
,因此模塊的複用性很強,若是模塊在程序中用處不少,你能夠單首創建一個包。這個包能夠包含一個或多個其餘模塊,而且上傳到npm
開源。reacrt
、lodash
還有jquery
都是能夠從npm上下載的npm包。
因爲模塊定義了導入和導出,因此很容易組合起來,不只如此。一個軟件好的設計應該是低耦合,模塊增長了代碼的靈活性。
npm上有世界上數量最多的免費模塊,超過七十萬個,若是你須要某個功能的包,就去npm上找吧。
這裏使用手錶的描述也是合適的。不在贅述。
模塊最大的好處也許是組織化了,模塊帶來的分離,正如你所見的,幫助你避免污染全局命名空間,減小命名衝突。
如今你大概瞭解了模塊的結構和優勢。是時候正式構建模塊了。對此咱們的方法是很是有條理的。緣由是以前提到的,javascript中的模塊有很是奇怪的歷史,即便有更新的方法在javascript中建立模塊,你也會時不時的看到一些老的建立方式。若是模塊從2018年開始,這個可能沒有一點用處,也就是說,咱們會回到2010年的模塊
時代。那時,angularjs剛剛發佈,jquery還在大範圍使用。大部分公司使用javascript去構建複雜的web應用,而管理這些複雜的工具就是--模塊。
建立模塊的第一個想法可能就是用文件分離代碼。
// users.js var users = ['Tyler', 'Sarah', 'Dan']; function getUsers() { return users; } // dom.js function addUserToDom(name) { var node = document.createElement('li'); var text = document.createTextNode(name); node.appendChild(text); document.getElementById('users').appendChild(node); } document.getElementById('submit') .addEventListener('click', function() { var input = document.getElementById('input'); addUserToDom(input.value); input.value = ''; }); var users = window.getUsers(); for (var i = 0; i < users.length; i++) { addUserToDom(users[i]); }
<!-- index.html --> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"></ul> <input id="input" type="text" placeholder="New User"> </input> <button id="submit">Submit</button> <script src="users.js"></script> <script src="dom.js"></script> </body> </html>
這裏查看所有源代碼
ok,咱們成功的將app分離成不一樣的功能文件,是否是意味着咱們已經實現了模塊?不,絕對沒有。咱們作的只不過是分離代碼所在的位置。在js中,只有建立函數才能生成新的做用域。咱們未在函數中生命的變量,全都在全局對象上。也就是說,你能夠訪問他們經過window
對象。你會注意到咱們能夠訪問到,這是糟糕的。由於當咱們更改一些方法時,其實就是在改變咱們整個app。咱們沒有分離咱們的代碼到模塊,只是在物理位置上分離了代碼。若是剛開始學習javascript,這個結果可能令你驚訝,不過,這多是你能想到在js中如何實現模塊化的第一個想法。
那麼,若是分享分離沒有給咱們提供模塊的功能,那咱們要怎麼作呢?重複強調一下模塊的優勢:複用性、組合型、利用性、隔離性還有可組織性。js有沒有原始的特性以供咱們創造模塊,以達到上面說的優勢?常規函數?當你思考函數的特色,它的特色和模塊優勢類似。因此,接下來該怎麼作呢?若是咱們暴露一個對象來替代直接把整個app暴露在全局對象下,而且命名這個對象爲app
,咱們能夠吧全部咱們app須要用到的方法,掛在在這個app
對象下。這樣會防止咱們污染全局變量。咱們能夠在裏面放置任何東西,這樣對於其餘應用來講依然是不可見得。
// users.js function usersWrapper () { var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } APP.getUsers = getUsers } usersWrapper() // dom.js function domWrapper() { function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = APP.getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) } } domWrapper()
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"></ul> <input id="input" type="text" placeholder="New User"> </input> <button id="submit">Submit</button> <script src="app.js"></script> <script src="users.js"></script> <script src="dom.js"></script> </body> </html>
這裏查看所有源代碼
如今你查看window對象,相比於,只有咱們的app
對象,和咱們的包裹函數:userWrapper
、domWrapper
。更重要的是,app中很是重要的代碼(好比users
)變得不可更改了。由於它不在在全局環境下了。
讓咱們更進一步。有沒有辦法能夠丟棄包裹函數?咱們只是定義了它們,而後當即調用。給他們一個全局命名的惟一緣由就是咱們以後能夠當即調用它們。若是咱們沒有給他們全局命名,有沒有辦法直接直接調用沒有名字(匿名)的函數。不賣關子了,固然有了,就是Immediately Invoked Function Expression
,簡寫爲IIFE
。
它看起來像下面這樣:
(function() { console.log('Pronounced IF-EE'); })()
注意,這僅僅是一個被小括號()
包起來的匿名函數。
(function() { console.log('Pronounced IF-EE'); })
而後,就像其餘函數同樣,爲了調用函數,咱們增長了一對小括號在函數而最後。
(function() { console.log('Pronounced IF-EE'); })()
如今,爲了放棄醜陋的包裹函數和乾淨的全局命名空間讓咱們來使用IIFE
來更新一下代碼。
// users.js (function () { var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } APP.getUsers = getUsers })() // dom.js (function () { function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = APP.getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) } })()
這裏查看所有源代碼
麼麼噠。如今你在查看window對象,你會發現,咱們僅僅掛在了一個app對象在上面,他將做爲全局方法的命名空間。
這就是IIFE模塊模式。
IIFE模塊模式有什麼優勢呢?首先,最重要的一點是,咱們沒有污染全局命名空間,這避免了變量衝突,而且提供代碼私有性。有利就有弊,咱們仍然有一個全局app變量,若是其餘框架使用了相同的代碼,咱們就有麻煩了。第二點,你可能主要到了html文件中的script的順序,若是順序不對,那麼app直接會掛掉。
不過,就算這不是最完美的。咱們依然進步了一大塊。咱們知道了IIFE模塊模式的優勢和缺點。若是咱們用咱們的標準建立並管理模塊,它有哪些特性呢?
早些時候,咱們對模塊分離的第一感受每一個文件都是一個新的模塊。就算這種想法在js中不是開箱就用的。我認爲對模塊來講這是一個很是顯著的分離。每一個文件就是一個單獨的模塊,而後咱們須要一個特性是每一個文件(模塊)都能定義本身的導入和導出。並可在其餘文件(模塊)中導入。
如今,咱們明確了咱們想要的標準,讓咱們開始開發api。咱們須要定義的看起來像是imports
和exports
,從exports開始。爲了保證更好理解,任何和module相關的咱們都稱之爲module
對象。而後,咱們想從模塊導出的內容都放在module.exports
上,就像下面這樣:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports.getUsers = getUsers
也能夠這樣:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports = { getUsers: getUsers }
無論有多少個方法,咱們均可以添加到exports
對象上:
// users.js var users = ["Tyler", "Sarah", "Dan"] module.exports = { getUsers: function () { return users }, sortUsers: function () { return users.sort() }, firstUser: function () { return users[0] } }
好了,咱們解決了如何從模塊導出,接下來咱們須要解決如何導入。一樣一切從簡,首先假設咱們有一個叫作require
的函數,它接受一個字符串路徑做爲第一個參數,而後返回從這個路徑下導出的全部內容。接着上面的user.js文件,引入的方式像這樣:
var users = require('./users') users.getUsers() // ["Tyler", "Sarah", "Dan"] users.sortUsers() // ["Dan", "Sarah", "Tyler"] users.firstUser() // ["Tyler"]
哦耶~ 利用假象的module.exports
和require
語法,咱們不只保留了模塊的全部優勢,還擺脫了IIFE模塊模式的缺點。舒服。
看完這個標準,有沒有靈光一現?這tm不就是commonjs嗎?
commonjs小組定義了模塊模式去解決js做用域問題,以確保每一個模塊在他們本身的命名空間執行。經過模塊明確導出那些變量來實現,經過其餘模塊定義的require來正確工做。
若是你以前使用過node,conmonjs你會很熟悉。使用node,你能夠開箱即用的使用require和module.exports語法,不過,瀏覽器並未支持。事實上,就算瀏覽器支持,瀏覽器也不會使用commonjs,由於它不是異步加載模塊。衆所周知,瀏覽器是單線程。異步纔是王道。
簡單總結一下,commonjs有兩個問題,首先瀏覽器不支持,第二,瀏覽器就算支持了也會由於commonjs的同步加載形成很糟糕的用戶體驗。若是咱們能修復這兩個問題,這也許是一個好的方案。不過,花費不少的時間去考慮研究commonjs是否對瀏覽器足夠友好有沒有意義呢?無論怎麼樣,這有一個新的解決方案,它叫作模塊打包器。
模塊打包器的做用是檢查你的代碼庫。尋找全部的imports和exports,而後解析打包成瀏覽器能夠明白的代碼到一個單獨的新文件。並且你再也不用當心翼翼的引入全部script,你應該直接引入打包好的那個文件。
app.js ---> |
users.js -> | Bundler | -> bundle.js
dom.js ---> |
因此,模塊打包器到底作了什麼捏?這個問題很大,我也不能所有解釋清楚,不過,這有一個經過webpack打包以後的輸出,你能夠本身領悟領悟,哈哈。
這裏查看全部源代碼,你也能夠下載下來,執行 npm install,而後執行webpack
(function(modules) { // webpackBootstrap // 模塊緩存 var installedModules = {}; // require函數 function __webpack_require__(moduleId) { // 檢查module是否有緩存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 建立一個module並緩存 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 執行module modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); // 設置module爲已load module.l = true; // 返回模塊的導出 return module.exports; } // 暴露模塊對象 __webpack_require__.m = modules; // 暴露模塊緩存 __webpack_require__.c = installedModules; // 定義getter函數 __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty( exports, name, { enumerable: true, get: getter } ); } }; // 在導出中定義__esModule __webpack_require__.r = function(exports) { if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; // 建立假的命名空間對象 // mode & 1: value是模塊id,經過它引入 // mode & 2: 合併全部屬性到ns對象上 // mode & 4: ns已經存在時,直接返回 // mode & 8|1: 行爲和require同樣 __webpack_require__.t = function(value, mode) { if(mode & 1) value = __webpack_require__(value); if(mode & 8) return value; if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value: value }); if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./dom.js"); }) /************************************************************************/ ({ /***/ "./dom.js": /*!****************!*\ !*** ./dom.js ***! \****************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { eval(` var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n function addUserToDOM(name) {\n const node = document.createElement(\"li\")\n const text = document.createTextNode(name)\n node.appendChild(text)\n\n document.getElementById(\"users\")\n .appendChild(node)\n}\n\n document.getElementById(\"submit\")\n .addEventListener(\"click\", function() {\n var input = document.getElementById(\"input\")\n addUserToDOM(input.value)\n\n input.value = \"\"\n})\n\n var users = getUsers()\n for (var i = 0; i < users.length; i++) {\n addUserToDOM(users[i])\n }\n\n\n//# sourceURL=webpack:///./dom.js?` );}), /***/ "./users.js": /*!******************!*\ !*** ./users.js ***! \******************/ /*! no static exports found */ /***/ (function(module, exports) { eval(` var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n function getUsers() {\n return users\n}\n\nmodule.exports = {\n getUsers: getUsers\n }\n\n//# sourceURL=webpack:///./users.js?`);}) });
你能夠注意到有不少奇奇怪怪的代碼,你能夠閱讀註釋來簡單瞭解一下到底發生了什麼。可是,一個頗有趣的事是,打包後的代碼用一個IIFE包裹起來了。也就是說,他們使用了IIFE模塊模式獲得了一個相對來講最完美的方案。
javascript的將來是一個活生生的,豐滿的語言。TC-31標準委員會,一年內屢次討論如何潛在改善提升javascript語言。換言之,模塊是編寫可伸縮性、可維護的js代碼的關鍵特性。在2013年甚至更早以前,這種說法很顯然是不存在的。js須要一種模塊的標準。一種內建的可處理模塊的解決方法,這也拉開了實現js模塊化的序幕。
如你如今所知道的。若是你以前接受過建立js系統模塊的任務,這個模塊最終看起來將是什麼樣的?commonjs?每一個文件以一種很清晰的方式定義導入和導出,很顯然,這個是重中之重。可是有個問題,commonjs加載模塊是同步的。雖然這對服務端沒有壓力,可是對瀏覽器不是很友好。一個改變是讓commonjs支持異步加載,另外一種咱們使用語言本身的模塊化,也就是import
和export
。
此次,咱們不須要再假想這種實現了,TC-39標準委員會提出了精確的設計和描述,也就是"ES Modules"。下面讓咱們以這種標準化的模塊建立javascript模塊。
正如上面所說的,爲了指定你要導出的模塊,你須要使用export
關鍵字。
// utils.js // Not exported function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // Exported export function first (arr) { return arr[0] } // Exported export function last (arr) { return arr[arr.length - 1] }
有幾種方式能夠導入first
和last
方法,一種是導入全部從urils.js
導出的。
import * as utils from './utils' utils.first([1,2,3]) // 1 utils.last([1,2,3]) // 3
若是咱們不想導入所有導出呢?在這個例子中,若是你只想引入first
方法,你能使用一種叫作命名導入的辦法(看起來很想解構,但其實不是哈)。
import { first } from './utils' first([1,2,3]) // 1
還有呢,不只僅能夠指定多個導出,你還能夠指定一個default
導出。
// leftpad.js export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
當你使用default
導出這種方式,你的導入方式也會發生變化,代替使用*或者使用命名導入,你可使用import name from './patn'
import leftpad from './leftpad'
如今,若是你有默認導出,也有其餘格式的導出怎麼辦呢?這不是問題,按照正確的語法寫就能夠了,ES Module沒有這種限制。
// utils.js function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // regular export export function first (arr) { return arr[0] } // regular export export function last (arr) { return arr[arr.length - 1] } // default export export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
那導入語法看起來是什麼樣的?我以爲你能夠想象獲得。
import leftpad, { first, last } from './utils'
仍是挺爽的是吧?leftpad
是默認導出,first
和last
是常規導出。
ES Modules的關鍵點在於,它是js語言的一部分,而且現代瀏覽器已經支持這種寫法了。如今,讓咱們回到一開始的app,不過此次咱們使用ES Modules來改寫一遍。
這裏查看全部源代碼
// users.js var users = ["Tyler", "Sarah", "Dan"] export default function getUsers() { return users } // dom.js import getUsers from './users.js' function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) }
使用IIFE模式,咱們須要使用script引入每一個js文件。使用commonjs,咱們須要使用webpack等打包器處理咱們的代碼,而後引入打包後的文件。而ES Modules中,在一些如今瀏覽器中,咱們僅僅須要使用script標籤引入咱們的未被處理過的入口文件,而後爲script標籤增長屬性:typr='module'
。
<!DOCTYPE html> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"> </ul> <input id="input" type="text" placeholder="New User"></input> <button id="submit">Submit</button> <script type=module src='dom.js'></script> </body> </html>
到這裏,還有一個commonjs與ES Modules的不一樣沒有介紹。
commonjs中,你能夠在任何地方引入模塊,甚至經過判斷。
if (pastTheFold === true) { require('./parallax') }
ES Modules須要靜態解析(參考js詞法解析,也會有提高的效果)的,import語句必須在模塊頂部,也就是說,他不能再判斷語句中或者其餘相似的語句中使用。
if (pastTheFold === true) { import './parallax' // "import' and 'export' may only appear at the top level" }
這是由於加載器會進行模塊樹的靜態解析。找到那些真正被用到的,丟棄那些未被使用到的。這是一個很大的話題。換句話說,這也是爲何ES Modules但願你聲明import語句在模塊頂部,這樣打包器會更快的解析的你依賴樹,解析完畢,他纔會去真正的工做。
對了,其實你可使用import()
來動態導入。請自行查找。
但願經過這篇文章能夠幫到你。
type='module'
的加載模式都是defer
。標準 | 變量問題 | 依賴 | 動態/懶 加載 | 靜態分析 |
---|---|---|---|---|
IIFE | ✔ | × | × | × |
AMD | ✔ | ✔ | ✔ | × |
CMD | ✔ | ✔ | ✔ | × |
commonjs | ✔ | ✔ | ✔ | × |
es6 | ✔ | ✔ | ✔ | ✔ |
再強調一點:es6的模塊是值的引用,commonjs是值的拷貝。參考文章