<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端模塊化的前世今生(萬字實戰(zhàn)好文)

          共 18143字,需瀏覽 37分鐘

           ·

          2020-12-04 22:56

          前言

          隨著前端項目的越來越龐大,組件化的前端框架,前端路由等技術(shù)的發(fā)展,模塊化已經(jīng)成為現(xiàn)代前端工程師的一項必備技能。無論是什么語言一旦發(fā)展到一定地步,其工程化能力和可維護(hù)性勢必得到相應(yīng)的發(fā)展。

          模塊化這件事,無論在哪個編程領(lǐng)域都是相當(dāng)常見的事情,模塊化存在的意義就是為了增加可復(fù)用性,以盡可能少的代碼是實現(xiàn)個性化的需求。同為前端三劍客之一的 CSS 早在 2.1 的版本就提出了?@import?來實現(xiàn)模塊化,但是 JavaScript 直到 ES6 才出現(xiàn)官方的模塊化方案: ES Module (import、export)。盡管早期 JavaScript 語言規(guī)范上不支持模塊化,但這并沒有阻止 JavaScript ?的發(fā)展,官方?jīng)]有模塊化標(biāo)準(zhǔn)開發(fā)者們就開始自己創(chuàng)建規(guī)范,自己實現(xiàn)規(guī)范。

          CommonJS 的出現(xiàn)

          十年前的前端沒有像現(xiàn)在這么火熱,模塊化也只是使用閉包簡單的實現(xiàn)一個命名空間。2009 年對 JavaScript 無疑是重要的一年,新的 JavaScript 引擎 (v8) ,并且有成熟的庫 (jQuery、YUI、Dojo),ES5 也在提案中,然而 JavaScript 依然只能出現(xiàn)在瀏覽器當(dāng)中。早在2007年,AppJet 就提供了一項服務(wù),創(chuàng)建和托管服務(wù)端的 JavaScript 應(yīng)用。后來 Aptana 也提供了一個能夠在服務(wù)端運行 Javascript 的環(huán)境,叫做 Jaxer。網(wǎng)上還能搜到關(guān)于 AppJet、Jaxer 的博客,甚至 Jaxer 項目還在github上。

          Jaxer

          但是這些東西都沒有發(fā)展起來,Javascript ?并不能替代傳統(tǒng)的服務(wù)端腳本語言 (PHP、Python、Ruby) 。盡管它有很多的缺點,但是不妨礙有很多人使用它。后來就有人開始思考 JavaScript 要在服務(wù)端運行還需要些什么?于是在 2009 年 1 月,Mozilla 的工程師 Kevin Dangoor 發(fā)起了 CommonJS 的提案,呼吁 JavaScript 愛好者聯(lián)合起來,編寫 JavaScript 運行在服務(wù)端的相關(guān)規(guī)范,一周之后,就有了 224 個參與者。

          "[This] is not a technical problem,It's a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together."

          CommonJS 標(biāo)準(zhǔn)囊括了 JavaScript 需要在服務(wù)端運行所必備的基礎(chǔ)能力,比如:模塊化、IO 操作、二進(jìn)制字符串、進(jìn)程管理、Web網(wǎng)關(guān)接口 (JSGI) 。但是影響最深遠(yuǎn)的還是 CommonJS 的模塊化方案,CommonJS 的模塊化方案是JavaScript社區(qū)第一次在模塊系統(tǒng)上取得的成果,不僅支持依賴管理,而且還支持作用域隔離和模塊標(biāo)識。再后來 node.js 出世,他直接采用了?CommonJS?的模塊化規(guī)范,同時還帶來了npm (Node Package Manager,現(xiàn)在已經(jīng)是全球最大模塊倉庫了) 。

          CommonJS 在服務(wù)端表現(xiàn)良好,很多人就想將 CommonJS 移植到客戶端 (也就是我們說的瀏覽器) 進(jìn)行實現(xiàn)。由于CommonJS 的模塊加載是同步的,而服務(wù)端直接從磁盤或內(nèi)存中讀取,耗時基本可忽略,但是在瀏覽器端如果還是同步加載,對用戶體驗極其不友好,模塊加載過程中勢必會向服務(wù)器請求其他模塊代碼,網(wǎng)絡(luò)請求過程中會造成長時間白屏。所以從 CommonJS 中逐漸分裂出來了一些派別,在這些派別的發(fā)展過程中,出現(xiàn)了一些業(yè)界較為熟悉方案 AMD、CMD、打包工具(Component/Browserify/Webpack)。

          AMD規(guī)范:RequireJS

          RequireJS logo

          RequireJS 是 AMD 規(guī)范的代表之作,它之所以能代表 AMD 規(guī)范,是因為 RequireJS 的作者 (James Burke) 就是 AMD 規(guī)范的提出者。同時作者還開發(fā)了?amdefine,一個讓你在 node 中也可以使用 AMD 規(guī)范的庫。

          AMD 規(guī)范由 CommonJS 的 Modules/Transport/C 提案發(fā)展而來,毫無疑問,Modules/Transport/C 提案的發(fā)起者就是 James Burke。

          James Burke 指出了 CommonJS 規(guī)范在瀏覽器上的一些不足:

          1. 缺少模塊封裝的能力:CommonJS 規(guī)范中的每個模塊都是一個文件。這意味著每個文件只有一個模塊。這在服務(wù)器上是可行的,但是在瀏覽器中就不是很友好,瀏覽器中需要做到盡可能少的發(fā)起請求。
          2. 使用同步的方式加載依賴:雖然同步的方法進(jìn)行加載可以讓代碼更容易理解,但是在瀏覽器中使用同步加載會導(dǎo)致長時間白屏,影響用戶體驗。
          3. CommonJS 規(guī)范使用一個名為?export?的對象來暴露模塊,將需要導(dǎo)出變量附加到?export?上,但是不能直接給該對象進(jìn)行賦值。如果需要導(dǎo)出一個構(gòu)造函數(shù),則需要使用?module.export,這會讓人感到很疑惑。

          AMD 規(guī)范定義了一個?define?全局方法用來定義和加載模塊,當(dāng)然 RequireJS 后期也擴(kuò)展了?require?全局方法用來加載模塊 。通過該方法解決了在瀏覽器使用 CommonJS 規(guī)范的不足。

          define(id?,?dependencies?,?factory);
          1. 使用匿名函數(shù)來封裝模塊,并通過函數(shù)返回值來定義模塊,這更加符合 JavaScript 的語法,這樣做既避免了對?exports?變量的依賴,又避免了一個文件只能暴露一個模塊的問題。

          2. 提前列出依賴項并進(jìn)行異步加載,這在瀏覽器中,這能讓模塊開箱即用。

            define("foo",?["logger"],?function?(logger)?{
            ????logger.debug("starting?foo's?definition")
            ????return?{
            ????????name:?"foo"
            ????}
            })
          3. 為模塊指定一個模塊 ID (名稱) 用來唯一標(biāo)識定義中模塊。此外,AMD的模塊名規(guī)范是 CommonJS 模塊名規(guī)范的超集。

            define("foo",?function?()?{
            ????return?{
            ????????name:?'foo'
            ????}
            })

          RequireJS 原理

          在討論原理之前,我們可以先看下 RequireJS 的基本使用方式。

          • 模塊信息配置:

            require.config({
            ??paths:?{
            ????jquery:?'https://code.jquery.com/jquery-3.4.1.js'
            ??}
            })
          • 依賴模塊加載與調(diào)用:

            require(['jquery'],?function?($){
            ??$('#app').html('loaded')
            })
          • 模塊定義:

            if?(?typeof?define?===?"function"?&&?define.amd?)?{
            ??define(?"jquery",?[],?function()?{
            ????return?jQuery;
            ??}?);
            }

          我們首先使用?config?方法進(jìn)行了 jquery 模塊的路徑配置,然后調(diào)用?require?方法加載 jquery 模塊,之后在回調(diào)中調(diào)用已加載完成的?$?對象。在這個過程中,jquery 會使用?define?方法暴露出我們所需要的?$?對象。

          在了解了基本的使用過程后,我們就繼續(xù)深入 RequireJS 的原理。

          模塊信息配置

          模塊信息的配置,其實很簡單,只用幾行代碼就能實現(xiàn)。定義一個全局對象,然后使用?Object.assign?進(jìn)行對象擴(kuò)展。

          //?配置信息
          const?cfg?=?{?paths:?{}?}

          //?全局?require?方法
          req?=?require?=?()?=>?{}

          //?擴(kuò)展配置
          req.config?=?config?=>?{
          ??Object.assign(cfg,?config)
          }

          依賴模塊加載與調(diào)用

          require?方法的邏輯很簡單,進(jìn)行簡單的參數(shù)校驗后,調(diào)用?getModule?方法對?Module?進(jìn)行了實例化,getModule 會對已經(jīng)實例化的模塊進(jìn)行緩存。因為 require 方法進(jìn)行模塊實例的時候,并沒有模塊名,所以這里產(chǎn)生的是一個匿名模塊。Module 類,我們可以理解為一個模塊加載器,主要作用是進(jìn)行依賴的加載,并在依賴加載完畢后,調(diào)用回調(diào)函數(shù),同時將依賴的模塊逐一作為參數(shù)回傳到回調(diào)函數(shù)中。

          //?全局?require?方法
          req?=?require?=?(deps,?callback)?=>?{
          ??if?(!deps?&&?!callback)?{
          ????return
          ??}
          ??if?(!deps)?{
          ????deps?=?[]
          ??}
          ??if?(typeof?deps?===?'function')?{
          ????callback?=?deps
          ????deps?=?[]
          ??}
          ??const?mod?=?getModule()
          ??mod.init(deps,?callback)
          }

          let?reqCounter?=?0
          const?registry?=?{}?//?已注冊的模塊

          //?模塊加載器的工廠方法
          const?getModule?=?name?=>?{
          ??if?(!name)?{
          ????//?如果模塊名不存在,表示為匿名模塊,自動構(gòu)造模塊名
          ????name?=?`@mod_${++reqCounter}`
          ??}
          ??let?mod?=?registry[name]
          ??if?(!mod)?{
          ????mod?=?registry[name]?=?new?Module(name)
          ??}
          ??return?mod
          }

          模塊加載器是是整個模塊加載的核心,主要包括?enable?方法和?check?方法。

          模塊加載器在完成實例化之后,會首先調(diào)用?init?方法進(jìn)行初始化,初始化的時候傳入模塊的依賴以及回調(diào)。

          //?模塊加載器

          class?Module?{
          ??constructor(name)?{
          ????this.name?=?name
          ????this.depCount?=?0
          ????this.depMaps?=?[]
          ????this.depExports?=?[]
          ????this.definedFn?=?()?=>?{}
          ??}
          ??init(deps,?callback)?{
          ????this.deps?=?deps
          ????this.callback?=?callback
          ????//?判斷是否存在依賴
          ????if?(deps.length?===?0)?{
          ??????this.check()
          ????}?else?{
          ??????this.enable()
          ????}
          ??}
          }

          enable?方法主要用于模塊的依賴加載,該方法的主要邏輯如下:

          1. 遍歷所有的依賴模塊;

          2. 記錄已加載模塊數(shù) (this.depCount++),該變量用于判斷依賴模塊是否全部加載完畢;

          3. 實例化依賴模塊的模塊加載器,并綁定?definedFn?方法;

            definedFn?方法會在依賴模塊加載完畢后調(diào)用,主要作用是獲取依賴模塊的內(nèi)容,并將?depCount?減 1,最后調(diào)用?check?方法 (該方法會判斷?depCount?是否已經(jīng)小于 1,以此來界定依賴全部加載完畢);

          4. 最后通過依賴模塊名,在配置中獲取依賴模塊的路徑,進(jìn)行模塊加載。

          class?Module?{
          ??...
          ??//?啟用模塊,進(jìn)行依賴加載
          ??enable()?{
          ????//?遍歷依賴
          ????this.deps.forEach((name,?i)?=>?{
          ??????//?記錄已加載的模塊數(shù)
          ??????this.depCount++
          ??????
          ??????//?實例化依賴模塊的模塊加載器,綁定模塊加載完畢的回調(diào)
          ??????const?mod?=?getModule(name)
          ??????mod.definedFn?=?exports?=>?{
          ????????this.depCount--
          ????????this.depExports[i]?=?exports
          ????????this.check()
          ??????}
          ??????
          ??????//?在配置中獲取依賴模塊的路徑,進(jìn)行模塊加載
          ??????const?url?=?cfg.paths[name]
          ??????loadModule(name,?url)
          ????});
          ??}
          ??...
          }

          loadModule?的主要作用就是通過 url 去加載一個 js 文件,并綁定一個 onload 事件。onload 會重新獲取依賴模塊已經(jīng)實例化的模塊加載器,并調(diào)用?init?方法。

          //?緩存加載的模塊
          const?defMap?=?{}

          //?依賴的加載
          const?loadModule?=??(name,?url)?=>?{
          ??const?head?=?document.getElementsByTagName('head')[0]
          ??const?node?=?document.createElement('script')
          ??node.type?=?'text/javascript'
          ??node.async?=?true
          ??//?設(shè)置一個?data?屬性,便于依賴加載完畢后拿到模塊名
          ??node.setAttribute('data-module',?name)
          ??node.addEventListener('load',?onScriptLoad,?false)
          ??node.src?=?url
          ??head.appendChild(node)
          ??return?node
          }

          //?節(jié)點綁定的?onload?事件函數(shù)
          const?onScriptLoad?=?evt?=>?{
          ??const?node?=?evt.currentTarget
          ??node.removeEventListener('load',?onScriptLoad,?false)
          ??//?獲取模塊名
          ??const?name?=?node.getAttribute('data-module')
          ??const?mod?=?getModule(name)
          ??const?def?=?defMap[name]
          ??mod.init(def.deps,?def.callback)
          }

          看到之前的案例,因為只有一個依賴 (jQuery),并且 jQuery 模塊并沒有其他依賴,所以?init?方法會直接調(diào)用?check?方法。這里也可以思考一下,如果是一個有依賴項的模塊后續(xù)的流程是怎么樣的呢?

          define(?"jquery",?[]?/*?無其他依賴?*/,?function()?{
          ??return?jQuery;
          }?);

          check?方法主要用于依賴檢測,以及調(diào)用依賴加載完畢后的回調(diào)。

          //?模塊加載器
          class?Module?{
          ??...
          ??//?檢查依賴是否加載完畢
          ??check()?{
          ????let?exports?=?this.exports
          ????//如果依賴數(shù)小于1,表示依賴已經(jīng)全部加載完畢
          ????if?(this.depCount?1)?{?
          ??????//?調(diào)用回調(diào),并獲取該模塊的內(nèi)容
          ??????exports?=?this.callback.apply(null,?this.depExports)
          ??????this.exports?=?exports
          ??????//激活?defined?回調(diào)
          ??????this.definedFn(exports)
          ????}
          ??}
          ??...
          }

          最終通過?definedFn?重新回到被依賴模塊,也就是最初調(diào)用?require?方法實例化的匿名模塊加載器中,將依賴模塊暴露的內(nèi)容存入?depExports?中,然后調(diào)用匿名模塊加載器的?check?方法,調(diào)用回調(diào)。

          mod.definedFn?=?exports?=>?{
          ??this.depCount--
          ??this.depExports[i]?=?exports
          ??this.check()
          }

          模塊定義

          還有一個疑問就是,在依賴模塊加載完畢的回調(diào)中,怎么拿到的依賴模塊的依賴和回調(diào)呢?

          const?def?=?defMap[name]
          mod.init(def.deps,?def.callback)

          答案就是通過全局定義的?define?方法,該方法會將模塊的依賴項還有回調(diào)存儲到一個全局變量,后面只要按需獲取即可。

          const?defMap?=?{}?//?緩存加載的模塊
          define?=?(name,?deps,?callback)?=>?{
          ??defMap[name]?=?{?name,?deps,?callback?}
          }

          RequireJS 原理總結(jié)

          最后可以發(fā)現(xiàn),RequireJS 的核心就在于模塊加載器的實現(xiàn),不管是通過?require?進(jìn)行依賴加載,還是使用?define?定義模塊,都離不開模塊加載器。

          感興趣的可以在我的github上查看關(guān)于簡化版 RequrieJS 的完整代碼 。

          CMD規(guī)范:sea.js

          sea.js logo

          CMD 規(guī)范由國內(nèi)的開發(fā)者玉伯提出,盡管在國際上的知名度遠(yuǎn)不如 AMD ,但是在國內(nèi)也算和 AMD 齊頭并進(jìn)。相比于 AMD 的異步加載,CMD 更加傾向于懶加載,而且 CMD 的規(guī)范與 CommonJS 更貼近,只需要在 CommonJS 外增加一個函數(shù)調(diào)用的包裝即可。

          define(function(require,?exports,?module)?{
          ??require("./a").doSomething()
          ??require("./b").doSomething()
          })

          作為 CMD 規(guī)范的實現(xiàn) sea.js 也實現(xiàn)了類似于 RequireJS 的 api:

          seajs.use('main',?function?(main)?{
          ??main.doSomething()
          })

          sea.js 在模塊加載的方式上與 RequireJS 一致,都是通過在 head 標(biāo)簽插入 script 標(biāo)簽進(jìn)行加載的,但是在加載順序上有一定的區(qū)別。要講清楚這兩者之間的差別,我們還是直接來看一段代碼:

          RequireJS?:

          //?RequireJS
          define('a',?function?()?{
          ??console.log('a?load')
          ??return?{
          ????run:?function?()?{?console.log('a?run')?}
          ??}
          })

          define('b',?function?()?{
          ??console.log('b?load')
          ??return?{
          ????run:?function?()?{?console.log('b?run')?}
          ??}
          })

          require(['a',?'b'],?function?(a,?b)?{
          ??console.log('main?run')
          ??a.run()
          ??b.run()
          })
          requirejs result

          sea.js?:

          //?sea.js
          define('a',?function?(require,?exports,?module)?{
          ??console.log('a?load')
          ??exports.run?=?function?()?{?console.log('a?run')?}
          })

          define('b',?function?(require,?exports,?module)?{
          ??console.log('b?load')
          ??exports.run?=?function?()?{?console.log('b?run')?}
          })

          define('main',?function?(require,?exports,?module)?{
          ??console.log('main?run')
          ??var?a?=?require('a')
          ??a.run()
          ??var?b?=?require('b')
          ??b.run()
          })

          seajs.use('main')
          sea.js result

          可以看到 sea.js 的模塊屬于懶加載,只有在 require 的地方,才會真正運行模塊。而 RequireJS,會先運行所有的依賴,得到所有依賴暴露的結(jié)果后再執(zhí)行回調(diào)。

          正是因為懶加載的機(jī)制,所以 sea.js 提供了?seajs.use?的方法,來運行已經(jīng)定義的模塊。所有 define 的回調(diào)函數(shù)都不會立即執(zhí)行,而是將所有的回調(diào)函數(shù)進(jìn)行緩存,只有 use 之后,以及被 require 的模塊回調(diào)才會進(jìn)行執(zhí)行。

          sea.js 原理

          下面簡單講解一下 sea.js 的懶加載邏輯。在調(diào)用 define 方法的時候,只是將 模塊放入到一個全局對象進(jìn)行緩存。

          const?seajs?=?{}
          const?cache?=?seajs.cache?=?{}

          define?=?(id,?factory)?=>?{
          ??const?uri?=?id2uri(id)
          ??const?deps?=?parseDependencies(factory.toString())
          ??const?mod?=?cache[uri]?||?(cache[uri]?=?new?Module(uri))
          ??mod.deps?=?deps
          ??mod.factory?=?factory
          ??
          }

          class?Module?{
          ??constructor(uri,?deps)?{
          ????this.status?=?0
          ????this.uri????=?uri
          ????this.deps???=?deps
          ??}
          }

          這里的 Module,是一個與 RequireJS 類似的模塊加載器。后面運行的 seajs.use 就會從緩存取出對應(yīng)的模塊進(jìn)行加載。

          注意:這一部分代碼只是簡單介紹 use 方法的邏輯,并不能直接運行。

          let?cid?=?0
          seajs.use?=?(ids,?callback)?=>?{
          ??const?deps?=?isArray(ids)???ids?:?[ids]
          ??
          ??deps.forEach(async?(dep,?i)?=>?{
          ????const?mod?=?cache[dep]
          ????mod.load()
          ??})
          }

          另外 sea.js 的依賴都是在 factory 中聲明的,在模塊被調(diào)用的時候,sea.js 會將 factory 轉(zhuǎn)成字符串,然后匹配出所有的?require('xxx')?中的?xxx?,來進(jìn)行依賴的存儲。前面代碼中的?parseDependencies?方法就是做這件事情的。

          早期 sea.js 是直接通過正則的方式進(jìn)行匹配的:

          const?parseDependencies?=?(code)?=>?{
          ??const?REQUIRE_RE?=?/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
          ??const?SLASH_RE?=?/\\\\/g
          ??const?ret?=?[]

          ??code
          ????.replace(SLASH_RE,?'')
          ????.replace(REQUIRE_RE,?function(_,?__,?id)?{
          ??????if?(id)?{
          ????????ret.push(id)
          ??????}
          ????})
          ??return?ret
          }

          但是后來發(fā)現(xiàn)正則有各種各樣的 bug,并且過長的正則也不利于維護(hù),所以 sea.js 后期舍棄了這種方式,轉(zhuǎn)而使用狀態(tài)機(jī)進(jìn)行詞法分析的方式獲取 require 依賴。

          詳細(xì)代碼可以查看 sea.js 相關(guān)的子項目:crequire。

          sea.js 原理總結(jié)

          其實 sea.js 的代碼邏輯大體上與 RequireJS 類似,都是通過創(chuàng)建 script 標(biāo)簽進(jìn)行模塊加載,并且都有實現(xiàn)一個模塊記載器,用于管理依賴。

          主要差異在于,sea.js 的懶加載機(jī)制,并且在使用方式上,sea.js 的所有依賴都不是提前聲明的,而是 sea.js 內(nèi)部通過正則或詞法分析的方式將依賴手動進(jìn)行提取的。

          感興趣的可以在我的github上查看關(guān)于簡化版 sea.js 的完整代碼 。

          總結(jié)

          ES6 的模塊化規(guī)范已經(jīng)日趨完善,其靜態(tài)化思想也為后來的打包工具提供了便利,并且能友好的支持 tree shaking。了解這些已經(jīng)過時的模塊化方案看起來似乎有些無趣,但是歷史不能被遺忘,我們應(yīng)該多了解這些東西出現(xiàn)的背景,以及前人們的解決思路,而不是一直抱怨新東西更迭的速度太快。

          今生的背景

          眾所周知,早期 JavaScript 原生并不支持模塊化,直到 2015 年,TC39 發(fā)布 ES6,其中有一個規(guī)范就是?ES modules(為了方便表述,后面統(tǒng)一簡稱 ESM)。但是在 ES6 規(guī)范提出前,就已經(jīng)存在了一些模塊化方案,比如 CommonJS(in Node.js)、AMD。ESM 與這些規(guī)范的共同點就是都支持導(dǎo)入(import)和導(dǎo)出(export)語法,只是其行為的關(guān)鍵詞也一些差異。

          CommonJS

          //?add.js
          const?add?=?(a,?b)?=>?a?+?b
          module.exports?=?add
          //?index.js
          const?add?=?require('./add')
          add(1,?5)

          AMD

          //?add.js
          define(function()?{
          ??const?add?=?(a,?b)?=>?a?+?b
          ??return?add
          })
          //?index.js
          require(['./add'],?function?(add)?{
          ??add(1,?5)
          })

          ESM

          //?add.js
          const?add?=?(a,?b)?=>?a?+?b
          export?default?add
          //index.js
          import?add?from?'./add'
          add(1,?5)

          關(guān)于 JavaScript 模塊化出現(xiàn)的背景在上一章(《前端模塊化的前世》)已經(jīng)有所介紹,這里不再贅述。但是 ESM 的出現(xiàn)不同于其他的規(guī)范,因為這是 JavaScript 官方推出的模塊化方案,相比于 CommonJS 和 AMD 方案,ESM采用了完全靜態(tài)化的方式進(jìn)行模塊的加載。

          ESM規(guī)范

          模塊導(dǎo)出

          模塊導(dǎo)出只有一個關(guān)鍵詞:export,最簡單的方法就是在聲明的變量前面直接加上 export 關(guān)鍵詞。

          export?const?name?=?'Shenfq'

          可以在 const、let、var 前直接加上 export,也可以在 function 或者 class 前面直接加上 export。

          export?function?getName()?{
          ??return?name
          }
          export?class?Logger?{
          ?log(...args)?{
          ????console.log(...args)
          ??}
          }

          上面的導(dǎo)出方法也可以使用大括號的方式進(jìn)行簡寫。

          const?name?=?'Shenfq'
          function?getName()?{
          ??return?name
          }
          class?Logger?{
          ?log(...args)?{
          ????console.log(...args)
          ??}
          }

          export?{?name,?getName,?Logger?}

          最后一種語法,也是我們經(jīng)常使用的,導(dǎo)出默認(rèn)模塊。

          const?name?=?'Shenfq'
          export?default?name

          模塊導(dǎo)入

          模塊的導(dǎo)入使用import,并配合?from?關(guān)鍵詞。

          //?main.js
          import?name?from?'./module.js'

          //?module.js
          const?name?=?'Shenfq'
          export?default?name

          這樣直接導(dǎo)入的方式,module.js?中必須使用?export default,也就是說 import 語法,默認(rèn)導(dǎo)入的是default模塊。如果想要導(dǎo)入其他模塊,就必須使用對象展開的語法。

          //?main.js
          import?{?name,?getName?}?from?'./module.js'

          //?module.js
          export?const?name?=?'Shenfq'
          export?const?getName?=?()?=>?name

          如果模塊文件同時導(dǎo)出了默認(rèn)模塊,和其他模塊,在導(dǎo)入時,也可以同時將兩者導(dǎo)入。

          //?main.js
          import?name,?{?getName?}?from?'./module.js'

          //module.js
          const?name?=?'Shenfq'
          export?const?getName?=?()?=>?name
          export?default?name

          當(dāng)然,ESM 也提供了重命名的語法,將導(dǎo)入的模塊進(jìn)行重新命名。

          //?main.js
          import?*?as?mod?from?'./module.js'
          let?name?=?''
          name?=?mod.name
          name?=?mod.getName()

          //?module.js
          export?const?name?=?'Shenfq'
          export?const?getName?=?()?=>?name

          上述寫法就相當(dāng)于于將模塊導(dǎo)出的對象進(jìn)行重新賦值:

          //?main.js
          import?{?name,?getName?}?from?'./module.js'
          const?mod?=?{?name,?getName?}

          同時也可以對單獨的變量進(jìn)行重命名:

          //?main.js
          import?{?name,?getName?as?getModName?}

          導(dǎo)入同時進(jìn)行導(dǎo)出

          如果有兩個模塊 a 和 b ,同時引入了模塊 c,但是這兩個模塊還需要導(dǎo)入模塊 d,如果模塊 a、b 在導(dǎo)入 c 之后,再導(dǎo)入 d 也是可以的,但是有些繁瑣,我們可以直接在模塊 c 里面導(dǎo)入模塊 d,再把模塊 d 暴露出去。

          模塊關(guān)系
          //?module_c.js
          import?{?name,?getName?}?from?'./module_d.js'
          export?{?name,?getName?}

          這么寫看起來還是有些麻煩,這里 ESM 提供了一種將 import 和 export 進(jìn)行結(jié)合的語法。

          export?{?name,?getName?}?from?'./module_d.js'

          上面是 ESM 規(guī)范的一些基本語法,如果想了解更多,可以翻閱阮老師的 《ES6 入門》。

          ESM 與 CommonJS 的差異

          首先肯定是語法上的差異,前面也已經(jīng)簡單介紹過了,一個使用?import/export?語法,一個使用?require/module?語法。

          另一個 ESM 與 CommonJS 顯著的差異在于,ESM 導(dǎo)入模塊的變量都是強(qiáng)綁定,導(dǎo)出模塊的變量一旦發(fā)生變化,對應(yīng)導(dǎo)入模塊的變量也會跟隨變化,而 CommonJS 中導(dǎo)入的模塊都是值傳遞與引用傳遞,類似于函數(shù)傳參(基本類型進(jìn)行值傳遞,相當(dāng)于拷貝變量,非基礎(chǔ)類型【對象、數(shù)組】,進(jìn)行引用傳遞)。

          下面我們看下詳細(xì)的案例:

          CommonJS

          //?a.js
          const?mod?=?require('./b')

          setTimeout(()?=>?{
          ??console.log(mod)
          },?1000)

          //?b.js
          let?mod?=?'first?value'

          setTimeout(()?=>?{
          ??mod?=?'second?value'
          },?500)

          module.exports?=?mod
          $?node?a.js
          first?value

          ESM

          //?a.mjs
          import?{?mod?}?from?'./b.mjs'

          setTimeout(()?=>?{
          ??console.log(mod)
          },?1000)

          //?b.mjs
          export?let?mod?=?'first?value'

          setTimeout(()?=>?{
          ??mod?=?'second?value'
          },?500)
          $?node?--experimental-modules?a.mjs
          #?(node:99615)?ExperimentalWarning:?The?ESM?module?loader?is?experimental.
          second?value

          另外,CommonJS 的模塊實現(xiàn),實際是給每個模塊文件做了一層函數(shù)包裹,從而使得每個模塊獲取?require/module、__filename/__dirname?變量。那上面的?a.js?來舉例,實際執(zhí)行過程中?a.js?運行代碼如下:

          //?a.js
          (function(exports,?require,?module,?__filename,?__dirname)?{
          ?const?mod?=?require('./b')
          ??setTimeout(()?=>?{
          ????console.log(mod)
          ??},?1000)
          });

          而 ESM 的模塊是通過?import/export?關(guān)鍵詞來實現(xiàn),沒有對應(yīng)的函數(shù)包裹,所以在 ESM 模塊中,需要使用?import.meta?變量來獲取?__filename/__dirname。import.meta?是 ECMAScript 實現(xiàn)的一個包含模塊元數(shù)據(jù)的特定對象,主要用于存放模塊的?url,而 node 中只支持加載本地模塊,所以 url 都是使用?file:?協(xié)議。

          import?url?from?'url'
          import?path?from?'path'
          //?import.meta:?{?url:?file:///Users/dev/mjs/a.mjs?}
          const?__filename?=?url.fileURLToPath(import.meta.url)
          const?__dirname?=?path.dirname(__filename)

          加載的原理

          步驟:

          1. Construction(構(gòu)造):下載所有的文件并且解析為module records。
          2. Instantiation(實例):把所有導(dǎo)出的變量入內(nèi)存指定位置(但是暫時還不求值)。然后,讓導(dǎo)出和導(dǎo)入都指向內(nèi)存指定位置。這叫做『linking(鏈接)』。
          3. Evaluation(求值):執(zhí)行代碼,得到變量的值然后放到內(nèi)存對應(yīng)位置。

          模塊記錄

          所有的模塊化開發(fā),都是從一個入口文件開始,無論是 Node.js 還是瀏覽器,都會根據(jù)這個入口文件進(jìn)行檢索,一步一步找到其他所有的依賴文件。

          //?Node.js:?main.mjs
          import?Log?from?'./log.mjs'

          <script?type="module"?src="./log.js">script>

          值得注意的是,剛開始拿到入口文件,我們并不知道它依賴了哪些模塊,所以必須先通過 js 引擎靜態(tài)分析,得到一個模塊記錄,該記錄包含了該文件的依賴項。所以,一開始拿到的 js 文件并不會執(zhí)行,只是會將文件轉(zhuǎn)換得到一個模塊記錄(module records)。所有的 import 模塊都在模塊記錄的?importEntries?字段中記錄,更多模塊記錄相關(guān)的字段可以查閱tc39.es。

          模塊記錄

          模塊構(gòu)造

          得到模塊記錄后,會下載所有依賴,并再次將依賴文件轉(zhuǎn)換為模塊記錄,一直持續(xù)到?jīng)]有依賴文件為止,這個過程被稱為『構(gòu)造』(construction)。

          模塊構(gòu)造包括如下三個步驟:

          1. 模塊識別(解析依賴模塊 url,找到真實的下載路徑);
          2. 文件下載(從指定的 url 進(jìn)行下載,或從文件系統(tǒng)進(jìn)行加載);
          3. 轉(zhuǎn)化為模塊記錄(module records)。

          對于如何將模塊文件轉(zhuǎn)化為模塊記錄,ESM 規(guī)范有詳細(xì)的說明,但是在構(gòu)造這個步驟中,要怎么下載得到這些依賴的模塊文件,在 ESM 規(guī)范中并沒有對應(yīng)的說明。因為如何下載文件,在服務(wù)端和客戶端都有不同的實現(xiàn)規(guī)范。比如,在瀏覽器中,如何下載文件是屬于 HTML 規(guī)范(瀏覽器的模塊加載都是使用的 script 標(biāo)簽)。

          雖然下載完全不屬于 ESM 的現(xiàn)有規(guī)范,但在?import?語句中還有一個引用模塊的 url 地址,關(guān)于這個地址需要如何轉(zhuǎn)化,在 Node 和瀏覽器之間有會出現(xiàn)一些差異。簡單來說,在 Node 中可以直接 import 在 node_modules 中的模塊,而在瀏覽器中并不能直接這么做,因為瀏覽器無法正確的找到服務(wù)器上的 node_modules 目錄在哪里。好在有一個叫做 import-maps 的提案,該提案主要就是用來解決瀏覽器無法直接導(dǎo)入模塊標(biāo)識符的問題。但是,在該提案未被完全實現(xiàn)之前,瀏覽器中依然只能使用 url 進(jìn)行模塊導(dǎo)入。

          <script?type="importmap">
          {
          ??"imports":?{
          ???"jQuery":?"/node_modules/jquery/dist/jquery.js"
          ??}
          }
          script>
          <script?type="module">
          ?import?$?from?'jQuery'
          ??$(function?()?{
          ????$('#app').html('init')
          ??})
          script>

          下載好的模塊,都會被轉(zhuǎn)化為模塊記錄然后緩存到?module map?中,遇到不同文件獲取的相同依賴,都會直接在?module map?緩存中獲取。

          //?log.js
          const?log?=?console.log
          export?default?log

          //?file.js
          export?{?
          ??readFileSync?as?read,
          ??writeFileSync?as?write
          }?from?'fs'
          module map

          模塊實例

          獲取到所有依賴文件并建立好?module map?后,就會找到所有模塊記錄,并取出其中的所有導(dǎo)出的變量,然后,將所有變量一一對應(yīng)到內(nèi)存中,將對應(yīng)關(guān)系存儲到『模塊環(huán)境記錄』(module environment record)中。當(dāng)然當(dāng)前內(nèi)存中的變量并沒有值,只是初始化了對應(yīng)關(guān)系。初始化導(dǎo)出變量和內(nèi)存的對應(yīng)關(guān)系后,緊接著會設(shè)置模塊導(dǎo)入和內(nèi)存的對應(yīng)關(guān)系,確保相同變量的導(dǎo)入和導(dǎo)出都指向了同一個內(nèi)存區(qū)域,并保證所有的導(dǎo)入都能找到對應(yīng)的導(dǎo)出。

          模塊連接

          由于導(dǎo)入和導(dǎo)出指向同一內(nèi)存區(qū)域,所以導(dǎo)出值一旦發(fā)生變化,導(dǎo)入值也會變化,不同于 CommonJS,CommonJS 的所有值都是基于拷貝的。連接到導(dǎo)入導(dǎo)出變量后,我們就需要將對應(yīng)的值放入到內(nèi)存中,下面就要進(jìn)入到求值的步驟了。

          模塊求值

          求值步驟相對簡單,只要運行代碼把計算出來的值填入之前記錄的內(nèi)存地址就可以了。到這里就已經(jīng)能夠愉快的使用 ESM 模塊化了。

          ESM的進(jìn)展

          因為 ESM 出現(xiàn)較晚,服務(wù)端已有 CommonJS 方案,客戶端又有 webpack 打包工具,所以 ESM 的推廣不得不說還是十分艱難的。

          客戶端

          我們先看看客戶端的支持情況,這里推薦大家到 Can I Use 直接查看,下圖是?2019/11的截圖。

          Can I use

          目前為止,主流瀏覽器都已經(jīng)支持 ESM 了,只需在?script?標(biāo)簽傳入指定的?type="module"?即可。

          <script?type="module"?src="./main.js">script>

          另外,我們知道在 Node.js 中,要使用 ESM 有時候需要用到 .mjs 后綴,但是瀏覽器并不關(guān)心文件后綴,只需要 http 響應(yīng)頭的MIME類型正確即可(Content-Type: text/javascript)。同時,當(dāng)?type="module"?時,默認(rèn)啟用?defer?來加載腳本。這里補(bǔ)充一張 defer、async 差異圖。

          img

          我們知道瀏覽器不支持?script?的時候,提供了?noscript?標(biāo)簽用于降級處理,模塊化也提供了類似的標(biāo)簽。

          <script?type="module"?src="./main.js">script>
          <script?nomodule>
          ??alert('當(dāng)前瀏覽器不支持?ESM??。?!')
          script>

          這樣我們就能針對支持 ESM 的瀏覽器直接使用模塊化方案加載文件,不支持的瀏覽器還是使用 webpack 打包的版本。

          <script?type="module"?src="./src/main.js">script>
          <script?nomodule?src="./dist/app.[hash].js">script>

          預(yù)加載

          我們知道瀏覽器的 link 標(biāo)簽可以用作資源的預(yù)加載,比如我需要預(yù)先加載?main.js?文件:

          <link?rel="preload"?href="./main.js">link>

          如果這個?main.js?文件是一個模塊化文件,瀏覽器僅僅預(yù)先加載單獨這一個文件是沒有意義的,前面我們也說過,一個模塊化文件下載后還需要轉(zhuǎn)化得到模塊記錄,進(jìn)行模塊實例、模塊求值這些操作,所以我們得想辦法告訴瀏覽器,這個文件是一個模塊化的文件,所以瀏覽器提供了一種新的 rel 類型,專門用于模塊化文件的預(yù)加載。

          <link?rel="modulepreload"?href="./main.js">link>

          現(xiàn)狀

          雖然主流瀏覽器都已經(jīng)支持了 ESM,但是根據(jù) chrome 的統(tǒng)計,有用到?

          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    秋霞网址| 操的好爽视频 | 国产午夜精品一区二区 | 美女激情操逼网站 | 操一区 |