前兩篇文章中,有小夥伴給我留言說怎麼沒有模塊化相關的知識點,模塊化在面試中被問到的概率非常大,但因為前幾篇文章篇幅實在太長了些,所以模塊化知識點單獨這篇文章給大家聊聊。
先說說什麼是模塊化,就是將獨立的功能代碼封裝成一個獨立的文件,其他模塊需要使用,在進行引用。
模塊化有利於代碼的拆分和架構上的解耦,模塊化在服務端領域已經早已成熟,nodejs 也已經支持模塊化。
而在瀏覽器上,js 腳本是異步載入的,腳本按照編碼順序依次執行,依賴關係只能按照編碼順序來控制。因此前端早早就有了模塊化技術,可每天醒來前端就多一個名詞多一個框架的,發展實在迅猛,就前端模塊化這些年的積累就有好幾種,我們依次來看看。
commonjs先看伴隨 nodejs 而誕生的 commonjs 規範。commonjs 規範應用於 nodejs 應用中,在 nodejs 應用中每個文件就是一個模塊,擁有自己的作用域,文件中的變量、函數都是私有的,與其他文件相隔離。
CommonJS規範規定,每個模塊內部, module 變量代表當前模塊。這個變量是一個對象,它的 exports 屬性(即 module.exports )是對外的接口。加載某個模塊,其實是加載該模塊的 module.exports 屬性。(引用阮一峰老師的描述)
舉個慄子看看模塊化後的文件該怎麼寫
let name = 'now';let age = 18;
let fun = () => { console.log('into fun'); name = 'change'}
module.exports = { name, fun}console.log(module)
var { name, fun } = require('./util/index.js')上面這個文件有兩個變量,一個函數,通過 module.exports 暴露變量 name 和函數 fun ,age 這個變量就是私有的,外部無法直接訪問,如果想讓 age 變量全局都可以訪問,那麼可以改成 global.age = 18 ,但這樣子會汙染全局作用域,會導致意想不到的驚喜(嚇)。
我們看看 util\index.js 列印出來的 module
commonjsmodule 中有這些屬性
module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名。module.filename 模塊的文件名,帶有絕對路徑。module.loaded 返回一個布爾值,表示模塊是否已經完成加載。module.parent 返回一個module對象,表示調用該模塊的模塊,如果改該模塊沒有被引用,那麼 parent 就是 null module.children 返回一個module數組,表示該模塊要用到的其他模塊。module.exports 表示模塊對外輸出的值。module.paths 這個用於 require 查找該文件的位置。
在開發中我們常使用的就是 module.exports , 通過 module.exports 輸出的對象就是引用方 require 出來的值
require既然有 module.exports 導出,那麼就有與之相對應的 require 導入,如下
var { name, fun, object } = require('./util/index.js') // 不用解構,直接導出對象也可以使用 require 我們最關心的就是文件路徑,這裡還是引用阮一峰老師的解釋
根據參數的不同格式,require命令去不同路徑尋找模塊文件。
如果參數字符串以「/」開頭,則表示加載的是一個位於絕對路徑的模塊文件。比如,require('/home/marco/foo.js')將加載/home/marco/foo.js。
如果參數字符串以「./」開頭,則表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。比如,require('./circle')將加載當前腳本同一目錄的circle.js。
如果參數字符串不以「./「或」/「開頭,則表示加載的是一個默認提供的核心模塊(位於Node的系統安裝目錄中),或者一個位於各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。大家還記得 module.paths 吧,這裡就派上用場了。舉例來說,腳本/home/user/projects/foo.js執行了require('bar.js')命令,Node會依據 module.paths 路徑加上文件名稱,依次搜索。這樣設計的目的是,使得不同的模塊可以將所依賴的模塊本地化。
如果參數字符串不以「./「或」/「開頭,而且是一個路徑,比如require('example-module/path/to/file'),則將先找到example-module的位置,然後再以它為參數,找到後續路徑。
如果指定的模塊文件沒有發現,Node會嘗試為文件名添加.js、.json、.node後,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯後的二進位文件解析。所以文件名的後綴可以省略。
如果想得到require命令加載的確切文件名,使用require.resolve()方法。
module.exports 和 exports我們還可以導出 exports 直接使用,但需要注意一點,exports 是已經定義的常量,在導出的時候不能在給它定義,如下
let exports = module.exports // 錯誤 #region exports Identifier 'exports' has already been declaredexports = module.exports; // 正確的使用 exports 我們可以這麼導出對象,但需要注意一點,在導出對象前不能修改 exports 的指向,若修改 exports 就與 module.exports 不是一個東西了,當然你可以在導出對象後隨意修改,這時候就不會影響導出。
exports = module.exports// exports = ()=>{} 不能修改exports.fun = () => { console.log('into fun'); name = 'change'}exports.name = 'now';// exports = ()=>{} 隨你改單獨使用 exports 和 module.exports 其實沒啥區別,個人建議還是使用 module.exports ,畢竟這才是常規穩妥的寫法。
隔離性commonjs 規範是在運行時加載的,在運行時導出對象,導出的對象與原本模塊中的對象是隔離的,簡單的說就是克隆了一份。看下面這個慄子
// util\index.jslet object = { age: 10}let fun = function() { console.log('modules obj', object); object = { age: 99 }}module.exports = { fun, object}
// index.jsvar { name, fun, object } = require('./util/index.js')console.log('before fun', object)fun()console.log('end fun', object)執行 node index.js 看看列印
before fun { age: 10 }modules obj { age: 10 }end fun { age: 10 }引用方調用了導出的 fun 方法,fun 方法改變了模塊中的 object 對象,可是在 index.js 中導出的 object 對象並沒有發生改變,所以可見 commonjs 規範下模塊的導出是深克隆的。
在瀏覽器中使用 commonjs 規範 browserify因為瀏覽器中缺少 module exports require global 這個四個變量,所以在瀏覽器中沒法直接使用 commonjs 規範,非要使用就需要做個轉換,使用 browserify ,它是常用的 commonjs 轉換工具,可以搭配 gulp webpack 一起使用。看下經過 browserify 處理後的代碼,就截取了些關鍵部分。
broswervifybrowserify1我把核心代碼複製出來,大致的結構如下,browserify 給每一個模塊都設置了一個唯一 id ,通過模塊路徑來映射模塊id,以此來找到各個模塊。、
原本模塊中的代碼被有 require module exports 這三個參數的函數所包裹,其中 require 用來加載其他模塊,exports 用來導出對象。
!function e(t, n, r) { function s(o, u) { if (!n[o]) { if (!t[o]) { var a = "function" == typeof require && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = "MODULE_NOT_FOUND", f } var l = n[o] = { exports: {} }; t[o][0].call(l.exports, function(e) { var n = t[o][1][e]; return s(n || e) }, l, l.exports, e, t, n, r) } return n[o].exports } for (var i = "function" == typeof require && require, o = 0; o < r.length; o++) s(r[o]); return s}({ 1:[function(require, module, exports) { "use strict" },{"babel-runtime/helpers/classCallCheck": 2},[3,4]}, 2: [function(require, module, exports) { "use strict"; exports.__esModule = !0, exports["default"] = function(instance, Constructor) { if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function") } } , {}]},{},[])ES6 模塊化ECMA推出了官方標準的模塊化解決方案,使用 export 導出,import 導入,編碼簡潔,從語義上更加通俗易懂。
ES6 支持異步加載模塊 的模塊不是對象,而是在編譯的時候就完成模塊的引用,所以是編譯時才加載的。
個人認為,ES6模塊化是以後的主流。
還是上面的慄子,用ES6模塊化改寫,改動上並不大,幾個關鍵字做下修改即可
// util/index.jslet name = 'now';
let fun = () => { name = 'change'}
export { name, fun}// app.jsimport { name, fun } from "../util";console.log('before fun', object)fun()console.log('end fun', object)瀏覽器中使用但是ES6模塊化在瀏覽器上的支持並不是很好,大部分瀏覽器還是不支持,所以需要做轉換
不使用 webpack ,使用 gulp 等構建流工具,那麼我們需要使用babel將 es6 轉成 es5 語法使用 babel 轉換,在babel 配置文件 .babelrc 寫上
在使用 browserify 對模塊規範進行轉換。
若使用 webpack ,webpack 是支持 es6 模塊化的,所以就只要引用 babel-loader ,對 es6 的語法做處理即可模塊的導出是對象的引用ES6模塊化下的導出是對象的引用,我們看下面這個慄子
// util/index.jslet name = 'now';
let fun = () => { name = 'change';}let getName = function() { console.log('module:',name)}
export { name, fun, getName}// app.jsimport { name, fun, getName } from "../util";console.log("before fun:", name);fun();console.log("after fun:", name);name = "change again";getName();我們看看輸出
before fun: nowafter fun: changemodule: change可見,模塊內部函數改變了模塊內的對象,外部導出使用的對象也跟著發生了變化,這一點是和 commonjs 規範區別最大的地方,這個特性可用於狀態提升。
ES6 模塊規範和 commonjs 規範 運行機制的區別CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口
運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,然後再從這個對象上面讀取方法,這種加載稱為「運行時加載」。
編譯時加載: ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,import時採用靜態命令的形式。即在import時可以指定加載某個輸出值,而不是加載整個模塊,這種加載稱為「編譯時加載」。
CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
AMD-require.js 和 CMD-sea.js聊到 AMD 和 CMD 這兩個規範都離不開 require.js 和 sea.js,這是早些年,為了解決瀏覽器異步加載模塊而誕生的方案。隨著打包工具的發展,commonjs和es6都可以在瀏覽器上運行了,所以 AMD、CMD 將逐漸被替代。
AMD規範的模塊化:用 require.config()指定引用路徑等,用define()定義模塊,用require()加載模塊。
CMD規範的模塊化:用define()定義模塊, seajs.use 引用模塊。
模塊兼容處理我們開發插件時可能需要對各種模塊做支持,我們可以這麼處理
const appJsBridge = function(){};if ("function" === typeof define) { define(function() { return appJsBridge; })} else if ("undefined" != typeof exports) { module.exports = appJsBridge;} else { window.appJsBridge = appJsBridge;}小結歡迎小夥伴們分享點讚收藏三連~
因這個號沒有留言功能,小夥伴們可以在公號對話框留言,或者添加我微信,與大夥共同學習,向高級前端邁進~
參考連結http://javascript.ruanyifeng.com/nodejs/module.html#toc2 http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html https://juejin.im/post/5aaa37c8f265da23945f365c#heading-5