面試官讓我解釋什麼是前端模塊化

先說說什麼是模塊化,就是將獨立的功能代碼封裝成一個獨立的文件,其餘模塊須要使用,在進行引用。javascript

模塊化有利於代碼的拆分和架構上的解耦,模塊化在服務端領域已經早已成熟,nodejs 也已經支持模塊化。html

而在瀏覽器上,js 腳本是異步載入的,腳本按照編碼順序依次執行,依賴關係只能按照編碼順序來控制。所以前端早早就有了模塊化技術,可天天醒來前端就多一個名詞多一個框架的,發展實在迅猛,就前端模塊化這些年的積累就有好幾種,咱們依次來看看。前端

commonjs

先看伴隨 nodejs 而誕生的 commonjs 規範。 commonjs 規範應用於 nodejs 應用中,在 nodejs 應用中每一個文件就是一個模塊,擁有本身的做用域,文件中的變量、函數都是私有的,與其餘文件相隔離。java

CommonJS規範規定,每一個模塊內部, module 變量表明當前模塊。這個變量是一個對象,它的 exports 屬性(即 module.exports )是對外的接口。加載某個模塊,實際上是加載該模塊的 module.exports 屬性。(引用阮一峯老師的描述)node

舉個栗子看看模塊化後的文件該怎麼寫webpack

// util\index.js
let name = 'now';
let age = 18;

let fun = () => {
    console.log('into fun');
    name = 'change'
}

module.exports = {
    name,
    fun
}
console.log(module)

// appJsBridge\index.js
var { name, fun } = require('./util/index.js')

複製代碼

上面這個文件有兩個變量,一個函數,經過 module.exports 暴露變量 name 和函數 fun ,age 這個變量就是私有的,外部沒法直接訪問,若是想讓 age 變量全局均可以訪問,那麼能夠改爲 global.age = 18 ,但這樣子會污染全局做用域,會致使意想不到的驚喜(嚇)。es6

咱們看看 util\index.js 打印出來的 moduleweb

commonjs

module 中有這些屬性json

module.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。 module.filename 模塊的文件名,帶有絕對路徑。 module.loaded 返回一個布爾值,表示模塊是否已經完成加載。 module.parent 返回一個module對象,表示調用該模塊的模塊,若是改該模塊沒有被引用,那麼 parent 就是 null module.children 返回一個module數組,表示該模塊要用到的其餘模塊。 module.exports 表示模塊對外輸出的值。 module.paths 這個用於 require 查找該文件的位置。gulp

在開發中咱們常使用的就是 module.exports , 經過 module.exports 輸出的對象就是引用方 require 出來的值

require

既然有 module.exports 導出,那麼就有與之相對應的 require 導入,以下

var { name, fun, object } = require('./util/index.js') // 不用解構,直接導出對象也能夠 使用 require 咱們最關心的就是文件路徑,這裏仍是引用阮一峯老師的解釋

根據參數的不一樣格式,require命令去不一樣路徑尋找模塊文件。

  1. 若是參數字符串以「/」開頭,則表示加載的是一個位於絕對路徑的模塊文件。好比,require('/home/marco/foo.js')將加載/home/marco/foo.js。

  2. 若是參數字符串以「./」開頭,則表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。好比,require('./circle')將加載當前腳本同一目錄的circle.js。

  3. 若是參數字符串不以「./「或」/「開頭,則表示加載的是一個默認提供的核心模塊(位於Node的系統安裝目錄中),或者一個位於各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。你們還記得 module.paths 吧,這裏就派上用場了。舉例來講,腳本/home/user/projects/foo.js執行了require('bar.js')命令,Node會依據 module.paths 路徑加上文件名稱,依次搜索。 這樣設計的目的是,使得不一樣的模塊能夠將所依賴的模塊本地化。

  4. 若是參數字符串不以「./「或」/「開頭,並且是一個路徑,好比require('example-module/path/to/file'),則將先找到example-module的位置,而後再以它爲參數,找到後續路徑。

  5. 若是指定的模塊文件沒有發現,Node會嘗試爲文件名添加.js、.json、.node後,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯後的二進制文件解析。因此文件名的後綴能夠省略。

  6. 若是想獲得require命令加載的確切文件名,使用require.resolve()方法。

module.exports 和 exports

咱們還能夠導出 exports 直接使用,但須要注意一點,exports 是已經定義的常量,在導出的時候不能在給它定義,以下

let exports = module.exports // 錯誤 #region exports Identifier 'exports' has already been declared
exports = 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.js
let object = {
    age: 10
}
let fun = function() {
    console.log('modules obj', object);
    object = { age: 99 }
}
module.exports = {
    fun,
    object
}

// index.js
var { 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 處理後的代碼,就截取了些關鍵部分。

broswervify

browserify1

我把核心代碼複製出來,大體的結構以下,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.js
let name = 'now';

let fun = () => {
    name = 'change'
}

export {
    name,
    fun
}
// app.js
import { name, fun } from "../util";
console.log('before fun', object)
fun()
console.log('end fun', object)
複製代碼

瀏覽器中使用

可是ES6模塊化在瀏覽器上的支持並非很好,大部分瀏覽器仍是不支持,因此須要作轉換

  1. 不使用 webpack ,使用 gulp 等構建流工具,那麼咱們須要使用babel將 es6 轉成 es5 語法

使用 babel 轉換,在babel 配置文件 .babelrc 寫上

{
"presets": ["es2015"]
}
複製代碼

在使用 browserify 對模塊規範進行轉換。

  1. 若使用 webpack ,webpack 是支持 es6 模塊化的,因此就只要引用 babel-loader ,對 es6 的語法作處理便可

模塊的導出是對象的引用

ES6模塊化下的導出是對象的引用,咱們看下面這個栗子

// util/index.js
let name = 'now';

let fun = () => {
    name = 'change';
}
let getName = function() {
    console.log('module:',name)
}

export {
    name,
    fun,
    getName
}
// app.js
import { name, fun, getName } from "../util";
console.log("before fun:", name);
fun();
console.log("after fun:", name);
name = "change again";
getName();
複製代碼

咱們看看輸出

before fun: now
after fun: change
module: 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) {
    // AMD CMD
    define(function() {
        return appJsBridge;
    })
} else if ("undefined" != typeof exports) {
    // commonjs
    module.exports = appJsBridge;
} else {
    // 沒有模塊化
    window.appJsBridge = appJsBridge;
}
複製代碼

參考連接

javascript.ruanyifeng.com/nodejs/modu… www.ruanyifeng.com/blog/2015/0… juejin.im/post/5aaa37…

相關文章
相關標籤/搜索