The last time, I have learned
【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。javascript
也是對本身的查缺補漏和技術分享。css
歡迎你們多多評論指點吐槽。html
系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見GitHub 地址: Nealyang/personalBlog。目錄和發文順序皆爲暫定
隨着互聯網的發展,前端開發也變的愈來愈複雜,從一開始的表單驗證到如今動不動上千上萬行代碼的項目開發,團隊協做就是咱們不可避免的工做方式,爲了更好地管理功能邏輯,模塊化的概念也就漸漸產生了。前端
好的書籍📚會分章節,好的代碼得分模塊。java
JavaScript 在早期的設計中就沒有模塊、包甚至類的概念,雖然 ES6
中有了 class
關鍵字,那也只是個語法糖。隨意隨着項目複雜度的增長,開發者必然須要模擬類的功能,來隔離、封裝、組織複雜的 JavaScript 代碼,而這種封裝和隔離,也被被咱們稱之爲模塊化。node
模塊就是一個實現特定功能的文件 or 代碼塊。隨着前端工程體系建設的愈發成熟,或許模塊化的概念已經在前端圈子裏已經耳熟能詳了。jquery
可是對於不少開發者而言,ES6 中的 export
、import
,nodejs
中的 require
、exports.xx
、module.exports
到底有什麼區別?爲何又有 CommonJS
,又有 AMD
,CMD
,UMD
?區別是什麼?甚至咱們在編寫 ts 文件的時候,還須要在配置文件裏面說明什麼模塊方式,在項目中使用的時候,咱們又是否真正知道,你用的究竟是基於哪種規範的模塊化?git
本文對你寫代碼沒有一點幫助,可是若是你還對上述的問題存有疑惑或者想了解JavaScript 模塊化的前世古今,那麼咱們開始吧~es6
公衆號回覆【xmind2】獲取源文件
所謂的模塊化,粗俗的講,就是把一大坨代碼,一鏟一鏟分紅一個個小小坨。固然,這種分割也必須是合理的,以便於你增減或者修改功能,而且不會影響總體系統的穩定性。github
我的認爲模塊化具備如下幾個好處:
npm
上找 package
的時候,是在幹啥?對於某一工程做業或者行爲進行定性的信息規定。主要是由於沒法精準定量而造成的標準,因此,被稱爲規範。在模塊化尚未規範肯定的時候,咱們都稱之爲原始模塊化。
回到咱們剛剛說的模塊的定義,模塊就是一個實現特定功能的文件 or 代碼塊(這是我本身給定義的)。專業定義是,在程序設計中,爲完成某一功能所需的一段程序或子程序;或指能由編譯程序、裝配程序等處理的獨立程序單位;或指大型軟件系統的一部分。而函數的一個功能就是實現特定邏輯的一組語句打包。而且 JavaScript 的做用域就是基於函數的。因此最原始之處,函數必然是做爲模塊化的第一步。
//函數1 function fn1(){ //statement } //函數2 function fn2(){ //statement }
其實就是把變量名塞的深一點。。。
let module1 = { let tag : 1, let name:'module1', fun1(){ console.log('this is fun1') }, fun2(){ console.log('this is fun2') } }
咱們在使用的時候呢,就直接
module1.fun2();
IIFE
就是當即執行函數,咱們能夠經過匿名閉包的形式來實現模塊化
let global = 'Hello, I am a global variable :)'; (function () { // 在函數的做用域中下面的變量是私有的 const myGrades = [93, 95, 88, 0, 55, 91]; let average = function() { let total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } let failing = function(){ let failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 控制檯顯示:'You failed 2 times.' // 控制檯顯示:'Hello, I am a global variable :)'
這種方法的好處在於,你能夠在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然可以訪問到全局變量
相似如上的 IIFE
,還有很是多的演進寫法
好比引入依賴:
// module.js文件 (function(window, $) { let data = 'www.baidu.com' //操做數據的函數 function foo() { //用於暴露有函數 console.log(`foo() ${data}`) $('body').css('background', 'red') } function bar() { //用於暴露有函數 console.log(`bar() ${data}`) otherFun() //內部調用 } function otherFun() { //內部私有的函數 console.log('otherFun()') } //暴露行爲 window.myModule = { foo, bar } })(window, jQuery)
// index.html文件 <!-- 引入的js必須有必定順序 --> <script type="text/javascript" src="jquery-1.10.1.js"></script> <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo() </script>
還有一種所謂的揭示模塊模式 Revealing module pattern
var myGradesCalculate = (function () { // 在函數的做用域中下面的變量是私有的 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // 將公有指針指向私有方法 return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
這和咱們以前的實現方法很是相近,除了它會確保,在全部的變量和方法暴露以前都會保持私有.
public
和 private
的概念上述的全部解決方案都有一個共同點:使用單個全局變量來把全部的代碼包含在一個函數內,由此來建立私有的命名空間和閉包做用域。
雖然每種方法都比較有效,但也都有各自的短板。
隨着大前端時代的到來,常見的 JavaScript 模塊規範也就有了:CommonJS
、AMD
、CMD
、UMD
、ES6
原生。
CommonJS
是 JavaScript 的一個模塊化規範,主要用於服務端Nodejs 中,固然,經過轉換打包,也能夠運行在瀏覽器端。畢竟服務端加載的模塊都是存放於本地磁盤中,因此加載起來比較快,不須要考慮異步方式。
根據規範,每個文件既是一個模塊,其內部定義的變量是屬於這個模塊的,不會污染全局變量。
CommonJS
的核心思想是經過 require
方法來同步加載所依賴的模塊,而後經過 exports
或者 module.exprots
來導出對外暴露的接口。
CommonJS
的規範說明,一個單獨的文件就是一個模塊,也就是一個單獨的做用域。而且模塊只有一個出口,module.exports
/exports.xxx
// lib/math.js const NAME='Nealayng'; module.exports.author = NAME; module.exports.add = (a,b)=> a+b;
加載模塊使用 require
方法,該方法讀取文件而且執行,返回文件中 module.exports
對象
// main.js const mathLib = require('./lib/math'); console.log(mathLib.author);//Nealyang console.log(mathLib.add(1,2));// 3
因爲瀏覽器不支持 CommonJS
規範,由於其根本沒有 module
、exports
、require
等變量,若是要使用,則必須轉換格式。Browserify是目前最經常使用的CommonJS格式轉換的工具,咱們能夠經過安裝browserify
來對其進行轉換.可是咱們仍然須要注意,因爲 CommonJS 的規範是阻塞式加載,而且模塊文件存放在服務器端,可能會出現假死的等待狀態。
npm i browserify -g
而後使用以下命令
browserify main.js -o js/bundle/main.js
而後在 HTML 中引入使用便可。
有一說一,在瀏覽器中使用 CommonJS 的規範去加載模塊,真的不是很方便。若是必定要使用,咱們可使用browserify編譯打包,也可使用require1k,直接在瀏覽器上運行便可。
其實在 nodejs 中模塊的實現並不是徹底按照 CommonJS 的規範來的,而是進行了取捨。
Node 中,一個文件是一個模塊->module
源碼定義以下:
function Module(id = '', parent) { this.id = id; this.path = path.dirname(id); this.exports = {}; this.parent = parent; updateChildren(parent, this, false); this.filename = null; this.loaded = false; this.children = []; }
//實例化一個模塊 var module = new Module(filename, parent);
CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。
{ id: '...', exports: { ... }, loaded: true, ... }
上面代碼就是 Node 內部加載模塊後生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略不介紹了。
之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。
再去深究具體的實現細節。。那就。。。下一篇分享吧~
Asynchronous Module Definition:異步模塊定義。
也就是解決咱們上面說的 CommonJS 在瀏覽器端致命的問題:假死。
CommonJS規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。AMD規範則是異步加載模塊,容許指定回調函數。
因爲其並不是原生 js 所支持的那種寫法。因此使用 AMD 規範開發的時候就須要大名鼎鼎的函數庫 require.js
的支持了。
https://github.com/requirejs/...關於 require.js 的更詳細使用說明能夠參考官網 api:https://requirejs.org/docs/ap...
require.js
主要解決兩個問題:
define(id,[dependence],callback)
id
,一個可選參數,說白了就是給模塊取個名字,可是倒是模塊的惟一標識。若是沒有提供則取腳本的文件名dependence
,以來的模塊數組callback
,工廠方法,模塊初始化的一些操做。若是是函數,應該只被執行一次。若是是對象,則爲模塊的輸出值require([moduleName],callback);
moduleName
,以來的模塊數組callback
,即爲依賴模塊加載成功以後執行的回調函數(前端異步的通用解決方案),<script src="scripts/require.js" data-main="scripts/app.js"></script>
data-main
指定入口文件,好比這裏指定 scripts
下的 app.js
文件,那麼只有直接或者間接與app.js
有依賴關係的模塊纔會被插入到html中。
經過這個函數能夠對requirejs
進行靈活的配置,其參數爲一個配置對象,配置項及含義以下:
baseUrl
——用於加載模塊的根路徑。paths
——用於映射不存在根路徑下面的模塊路徑。shims
——配置在腳本/模塊外面並無使用RequireJS的函數依賴而且初始化函數。假設underscore並無使用 RequireJS
定義,可是你仍是想經過RequireJS來使用它,那麼你就須要在配置中把它定義爲一個shimdeps
——加載依賴關係數組require.config({ //默認狀況下從這個文件開始拉去取資源 baseUrl:'scripts/app', //若是你的依賴模塊以pb頭,會從scripts/pb加載模塊。 paths:{ pb:'../pb' }, // load backbone as a shim,所謂就是將沒有采用requirejs方式定義 //模塊的東西轉變爲requirejs模塊 shim:{ 'backbone':{ deps:['underscore'], exports:'Backbone' } } });
|-js |-libs |-require.js |-modules |-article.js |-user.js |-main.js |-index.html
// user.js文件 // 定義沒有依賴的模塊 define(function() { let author = 'Nealyang' function getAuthor() { return author.toUpperCase() } return { getAuthor } // 暴露模塊 })
//article.js文件 // 定義有依賴的模塊 define(['user'], function(user) { let name = 'THE LAST TIME' function consoleMsg() { console.log(`${name} by ${user.getAuthor()}`); } // 暴露模塊 return { consoleMsg } })
// main.js (function() { require.config({ baseUrl: 'js/', //基本路徑 出發點在根目錄下 paths: { //映射: 模塊標識名: 路徑 article: './modules/article', //此處不能寫成article.js,會報錯 user: './modules/user' } }) require(['article'], function(alerter) { article.consoleMsg() }) })()
// index.html文件 <!DOCTYPE html> <html> <head> <title>Modular Demo</title> </head> <body> <!-- 引入require.js並指定js主文件的入口 --> <script data-main="js/main" src="js/libs/require.js"></script> </body> </html>
若是咱們須要引入第三方庫,則須要在 main.js 文件中引入
(function() { require.config({ baseUrl: 'js/', paths: { article: './modules/article', user: './modules/user', // 第三方庫模塊 jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會報錯 } }) require(['article'], function(alerter) { article.consoleMsg() }) })()
關於 require.js 的使用,仔細看文檔,其實仍是有不少知識點的。可是鑑於咱們着實如今使用很少(我也不熟),因此這裏也就參考網上優秀文章和本身實踐,拋磚引玉。
CMD是阿里的玉伯提出來的(大神的成長故事可在公衆號回覆【大佬】),js 的函數爲 sea.js
,它和 AMD 其實很是的類似,文件即爲模塊,可是其最主要的區別是實現了按需加載。推崇依賴就近的原則,模塊延遲執行,而 AMD 所依賴模塊式提早執行(requireJS 2.0
後也改成了延遲執行)
//AMD define(['./a','./b'], function (a, b) { //依賴一開始就寫好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依賴能夠就近書寫 var a = require('./a'); a.test(); ... //按需加載 if (status) { var b = requie('./b'); b.test(); } });
https://github.com/seajs/seajs
準確的說 CMD
是 SeaJS
在推廣過程當中對模塊定義的規範化產物。
也能夠說SeaJS
是一個遵循 CMD
規範的 JavaScript
模塊加載框架,能夠實現 JavaScript 的 CMD 模塊化開發方式。
SeaJS
只是實現 JavaScript的模塊化和按需加載,並未擴展 JavaScript 語言自己。SeaJS
的主要目的是讓開發人員更加專一於代碼自己,從繁重的 JavaScript 文件以及對象依賴處理中解放出來。
絕不誇張的說,咱們如今詳情頁就是 SeaJS+Kissy。。。(即將升級)
Seajs
追求簡單、天然的代碼書寫和組織方式,具備以下核心特性:
Sea.js
遵循 CMD
規範,能夠像 Node.js 通常書寫模塊代碼。Sea.js 還提供經常使用插件,很是有助於開發調試和性能優化,並具備豐富的可擴展接口。
examples/ |-- sea-modules 存放 seajs、jquery 等文件,這也是模塊的部署目錄 |-- static 存放各個項目的 js、css 文件 | |-- hello | |-- lucky | `-- todo `-- app 存放 html 等文件 |-- hello.html |-- lucky.html `-- todo.html
咱們從 hello.html 入手,來瞧瞧使用 Sea.js 如何組織代碼。
在 hello.html 頁尾,經過 script 引入 sea.js 後,有一段配置代碼
// seajs 的簡單配置 seajs.config({ base: "../sea-modules/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }) // 加載入口模塊 seajs.use("../static/hello/src/main")
sea.js 在下載完成後,會自動加載入口模塊。頁面中的代碼就這麼簡單。
這個小遊戲有兩個模塊 spinning.js 和 main.js,遵循統一的寫法:
// 全部模塊都經過 define 來定義 define(function(require, exports, module) { // 經過 require 引入依賴 var $ = require('jquery'); var Spinning = require('./spinning'); // 經過 exports 對外提供接口 exports.doSomething = ... // 或者經過 module.exports 提供整個接口 module.exports = ... });
上面就是 Sea.js 推薦的 CMD 模塊書寫格式。若是你有使用過 Node.js,一切都很天然。
以上實例,來源於官網 Example。更多 Demo 查看: https://github.com/seajs/examples
UMD 其實我我的仍是以爲很是。。。。不喜歡的。ifElse
就 universal
了。。。。
UMD
是 AMD
和 CommonJS
的綜合產物。如上所說,AMD
的用武之地是瀏覽器,非阻塞式加載。CommonJS 主要用於服務端 Nodejs 中使用。因此人們就想到了一個通用的模式 UMD
(universal module definition)。來解決跨平臺的問題。
沒錯!就是 ifElse
的寫法。
核心思想就是:先判斷是否支持Node.js的模塊(exports
)是否存在,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define
是否存在),存在則使用AMD
方式加載模塊。
(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... });
關於 UMD 更多的example 可移步github: https://github.com/umdjs/umd
若是你一直讀到如今,那麼恭喜你,咱們開始介紹咱們最新的模塊化了!
經過上面的介紹咱們知道,要麼模塊化依賴環境,要麼須要引入額外的類庫。說到底就是社區找到的一種妥協方案而後獲得了你們的承認。可是歸根結底不是官方呀。終於,ECMAScript 官宣了模塊化的支持,真正的規範。
在ES6中,咱們可使用 import
關鍵字引入模塊,經過 export
關鍵字導出模塊,功能較之於前幾個方案更爲強大,也是咱們所推崇的,可是因爲ES6目前沒法在全部瀏覽器中執行,因此,咱們還需經過babel將不被支持的import
編譯爲當前受到普遍支持的 require
。
ES6 的模塊化汲取了 CommonJS
和AMD
的優勢,擁有簡潔的語法和異步的支持。而且寫法也和 CommonJS 很是的類似。
關於 ES6 模塊的基本用法相比你們都比較熟悉了。這裏咱們主要和 CommonJS 對比學習。
兩大差別:
// lib/counter.js var counter = 1; function increment() { counter++; } function decrement() { counter--; } module.exports = { counter: counter, increment: increment, decrement: decrement }; // src/main.js var counter = require('../../lib/counter'); counter.increment(); console.log(counter.counter); // 1
在 main.js 當中的實例是和本來模塊徹底不相干的。這也就解釋了爲何調用了 counter.increment() 以後仍然返回1。由於咱們引入的 counter 變量和模塊裏的是兩個不一樣的實例。
因此調用 counter.increment() 方法只會改變模塊中的 counter .想要修改引入的 counter 只有手動一下啦:
counter.counter++; console.log(counter.counter); // 2
而經過 import 語句,能夠引入實時只讀的模塊:
// lib/counter.js export let counter = 1; export function increment() { counter++; } export function decrement() { counter--; } // src/main.js import * as counter from '../../counter'; console.log(counter.counter); // 1 counter.increment(); console.log(counter.counter); // 2
由於 CommonJS
加載的是一個對象(module.exports
),對象只有在有腳本運行的時候才能生成。而 ES6 模塊不是一個對象,只是一個靜態的定義。在代碼解析階段就會生成。
ES6 模塊是編譯時輸出接口,所以有以下2個特色:
// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; console.log('b.js 先執行'); // 執行結果: // b.js 先執行 // a.js
// a.js import { foo } from './b'; console.log('a.js'); export const bar = 1; export const bar2 = () => { console.log('bar2'); } export function bar3() { console.log('bar3'); } // b.js export let foo = 1; import * as a from './a'; console.log(a); // 執行結果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js
「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。
// a.js var b = require('b'); // b.js var a = require('a');
循環加載若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。
在 CommonJS 中,腳本代碼在 require
的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。
// a.js exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 執行完畢');
// b.js exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 執行完畢');
// main.js var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
輸出結果爲:
在 b.js 之中,a.done = false b.js 執行完畢 在 a.js 之中,b.done = true a.js 執行完畢 在 main.js 之中, a.done=true, b.done=true
從上面咱們能夠看出:
b.js
之中,a.js
沒有執行完畢,只執行了第一行。main.js
執行到第二行時,不會再次執行b.js
,而是輸出緩存的b.js
的執行結果,即它的第四行ES6 處理「循環加載」與 CommonJS 有本質的不一樣。ES6 模塊是動態引用,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。
// a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar); export let foo = 'foo'; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo); export let bar = 'bar';
運行結果以下:
b.mjs ReferenceError: foo is not defined
上面代碼中,執行a.mjs之後會報錯,foo變量未定義.
具體的執行結果以下:
解決這個問題的方法,就是讓b.mjs運行的時候,foo已經有定義了。這能夠經過將foo寫成函數來解決。
// a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar()); function foo() { return 'foo' } export {foo}; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo()); function bar() { return 'bar' } export {bar};
最後執行結果爲:
b.mjs foo a.mjs bar
關於 ES6 詳細的模塊的介紹,強烈推薦阮一峯的 ES6 入門和深刻理解 ES6 一書
公衆號【全棧前端精選】 | 我的微信【is_Nealyang】 | |
---|---|---|