Webpack原理-從前端模塊化開始

當(dāng)前主流 JS 模塊化方案

  • CommonJS 規(guī)范芽丹,nodejs 實(shí)現(xiàn)的規(guī)范
  • AMD 規(guī)范,requirejs 實(shí)現(xiàn)的規(guī)范
  • CMD 規(guī)范,seajs 實(shí)現(xiàn)的規(guī)范拙绊, seajs 與 requirejs 實(shí)現(xiàn)原理有很多相似的地方 u ES Modules双仍,當(dāng)前 js 標(biāo)準(zhǔn)模塊化方案

注意:cjs枢希、amd、cmd朱沃、 ES Modules 都是只規(guī)范苞轿,所以可能對應(yīng)有多種實(shí)現(xiàn)

下面就對各個模塊化方案做簡單說明

無模塊化時代

一把梭
<script src="jquery.js"></script>
<script src="jquery_scroller.js"></script>
<script src="main.js"></script>
<script src="other1.js"></script>
<script src="other2.js"></script>
<script src="other3.js"></script>

無模塊化時代的問題

  • 污染全局作用域
  • 不便于拆分邏輯,維護(hù)成本高 ? 依賴關(guān)系不明顯
  • 復(fù)用性差

CommonJS 規(guī)范

  • CommonJS 是由 node 實(shí)現(xiàn)的一套規(guī)范逗物,關(guān)于 CommonJS 的提出可參考CommonJS 規(guī)范
  • require 源碼解讀可參考 require() 源碼解讀
  • 模塊包裝相當(dāng)于執(zhí)行如下代碼搬卒, compiledWrapper 是調(diào)用 node 封裝的 V8 原生創(chuàng)建函數(shù)的方法返回的一個函數(shù)
function compiledWrapper(exports, require, module, __filename, __dirname) {
  // 插入文件中的代碼
  // 返回導(dǎo)出對象
  return module.exports
}
compiledWrapper.call(exports, exports, require, module, filename, dirname)
  • CommonJS 模塊輸出的是一個值的拷貝,也就是說翎卓,一旦輸出一個值契邀,模塊內(nèi)部的變化就影響不到這個值
  • 如下有兩個文件,執(zhí)行命令node index.js失暴,會有什么結(jié)果?

lib.js

// lib.js
let counter = 3
function incCounter() {
  counter++
}
module.exports = {
  counter,
  incCounter
}

index.js

// index.js
const mod = require('./lib') // 此處輸出值坯门?
console.log(mod.counter)
mod.incCounter() // 此處輸出值?
console.log(mod.counter)
  • equire 命令第一次加載該腳本逗扒,就會執(zhí)行整個腳本古戴,然后在內(nèi)存生成一個對象,下次加載會直接從緩存中取數(shù)據(jù)
  • 以下是一個循環(huán)引用的例子矩肩,請問執(zhí)行 node main.js 后會輸出什么现恼?

a.js

// a.js
console.log('a starting')
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')

b.js

// b.js
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')

main.js

// main.js
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done = %j, b.done = %j', a.done, b.done)

AMD 規(guī)范

  • AMD 是 Asynchronous Module Definition 的簡寫,即異步模塊定義
  • AMD 規(guī)范的完整定義可參考 https://github.com/amdjs/amdjs-api/wiki/AMD
  • requirejs 是在瀏覽器中運(yùn)行的,所有一些基礎(chǔ)庫需要先配置叉袍,以方便其他庫調(diào)用始锚,可以理解為 CommonJS 中的 node_modules 下的包。業(yè)務(wù)模塊也可定義在其中喳逛,可認(rèn)為是路徑別名瞧捌。paths 中的路徑不能包含擴(kuò)展名。
require.config({
  paths: {
    // 如果第一個加載失敗就會加載第二個
    jquery: ['lib/jquery.min', 'lib/jquery'],
    lodash: 'lib/lodash.min',
    main: './mian' // 入口文件
  }
})

定義模塊

/**
* 定義模塊艺配,當(dāng)依賴加載完成后執(zhí)行回調(diào)
* 回調(diào)可返回值察郁,返回值會被導(dǎo)出到外部使用
* @param {String} id 模塊名稱,可省略
* @param {Array} dependencies 依賴的模塊
* @param {Function} factory 回調(diào)函數(shù)
*/
define(id?, dependencies?, factory);

define(['jquery'], function($) {
  $('body').css({ background: 'red' })
  // 導(dǎo)出log函數(shù)
  return (...args) => console.log('自定義log', ...args)
})

加載模塊

/**
 * 加載模塊
 * @param {Array} deps 要加載的模塊
 * @param {Function} callback 加載成功回調(diào)转唉,回調(diào)參數(shù)為加載模塊導(dǎo)出對象
 * @param {Function} errback 加載失敗回調(diào)
 */
requirejs(deps, callback, errback)
require(['main'], log => {
  log('我成功加載了‘)
  // do something...,也可以在這里繼續(xù)require其他js文件
})

requirejs 使用示例

  • 目錄結(jié)構(gòu)
.
├── index.html
└── js
    ├── lib
    │   ├── jquery.js
    │   ├── lodash.js
    │   └── require.js
    ├── main.js
    └── time.js
  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>requirejs-demo</title>
  </head>
  <body>
    <h1 id="time"></h1>
    <script src="./js/lib/require.js" data-main="./js/main.js"></script>
  </body>
</html>
  • main.js
requirejs.config({
  baseUrl: '/js/‘,
  paths: {
    jquery: './lib/jquery‘,
    lodash: './lib/lodash‘
  }
})?require(['jquery', './js/time.js'], ($, time) => {
  $('#time').text('TIME: ' + time.getTime())
  setInterval(() => {
   $('#time').text('TIME: ' + time.getTime())
  }, 1000)
})
  • time.js
define(['jquery', 'lodash'], ($, _) => ({
  getTime() {
    const time = new Date()
    const year = time.getFullYear()
    const month = _.padStart(time.getMonth() + 1, 2, '0‘)
    const date = _.padStart(time.getDate(), 2, '0‘)
    const hour = _.padStart(time.getHours(), 2, '0‘)
    const minute = _.padStart(time.getMinutes(), 2, '0‘)
    const second = _.padStart(time.getSeconds(), 2, '0‘)
    return `${year}/${month}/${date} ${hour}:${minute}:${second}`
  }
}))

CMD 規(guī)范

  • CMD 是 Common Module Definition 的簡寫皮钠,即通用模塊定義
  • CMD 規(guī)范的完整定義可參考https://github.com/seajs/seajs/issues/242
  • CMD 的主要代表是 seajs。CMD 推崇依賴就近赠法,AMD 推崇依賴前置麦轰。即 AMD 在定義模塊的時候就必須把依賴包含進(jìn)來,CMD 是在使用的時候再 require 對應(yīng)的依賴
  • 當(dāng)前主流的庫對 CMD 支持不是很友好砖织,都需要額外的修改才能工作
  • AMD 與 CMD 寫法對比如下
// CMD
// 代碼寫起來有同步require的感覺
define((require, exports, module) => {
  const $ = require('jquery‘)
  $('title').text('hello')
})
// AMD
// 明顯的異步風(fēng)格
define(['jquery'], $ => {
  $('title').text('hello')
})

seajs 中 require 書寫約定

  1. 正確拼寫 require
// 錯誤款侵!
define(function(req) {
  // ...
}) // 正確!
define(function(require) {
  // ...
})
  1. 使用直接量
// 錯誤侧纯!
require(myModule) // 錯誤新锈!
require('my-' + 'module') // 錯誤!
require('MY-MODULE'.toLowerCase()) // 正確眶熬!
require('my-module')
  1. 不要修改 require
// 錯誤 - 重命名 "require"妹笆!
var req = require,
  mod = req('./mod') // 錯誤 - 重定義 "require"!
require = function() {} // 錯誤 - 重定義 "require" 為函數(shù)參數(shù)!
function F(require) {} // 錯誤 - 在內(nèi)嵌作用域內(nèi)重定義了 "require"娜氏!
function F() {
  var require = function() {}
}

seajs 隱藏坑

  • 如下代碼輸出$為 null
function func(require, exports, module) {
  const $ = require('jquery‘)
  console.log($)
}
func.toString = () => '() => {}'
define(func)

seajs 對于 require 和 define 函數(shù)的特殊要求是由于拳缠,seajs 原理導(dǎo)致的,seajs 的執(zhí)行流程大致如下


seajs執(zhí)行流程

seajs 使用示例

  • 目錄結(jié)構(gòu)
.
├── index.html
└── js
    ├── lib
    │   ├── jquery.js
    │   ├── lodash.js
    │   └── sea.js
    ├── main.js
    └── time.js
  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="“UTF-8”" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>seajs-demo</title>
  </head>
  <body>
    <h1 id="time"></h1>
    <script src="./js/lib/sea.js" data-main="./js/main.js"></script>
    <script>
       seajs.config({
         base: '/js/‘,
         alias: {
           jquery: './lib/jquery‘,
           lodash: './lib/lodash‘
         }
      })
      // 加載入口模塊
      seajs.use('./js/main.js')
    </script>
  </body>
</html>
  • main.js
define((require, exports, module) => {
  const $ = require('jquery‘)
  const time = require('./time.js‘)
  $('#time').text('TIME: ' + time.getTime())
  setInterval(() => {
    $('#time').text('TIME: ' + time.getTime())
  }, 1000)
})
  • time.js
define((require, exports, module) => {
  module.exports = {
    getTime() {
      const $ = require('jquery‘)
      const _ = require('lodash‘)
      const time = new Date()
      const year = time.getFullYear()
      const month = _.padStart(time.getMonth() + 1, 2, '0‘)
      const date = _.padStart(time.getDate(), 2, '0‘)
      const hour = _.padStart(time.getHours(), 2, '0‘)
      const minute = _.padStart(time.getMinutes(), 2, '0‘)
      const second = _.padStart(time.getSeconds(), 2, '0‘)
      return `${year}/${month}/${date} ${hour}:${minute}:${second}`
    }
  }
})

ES Modules

  • ES Modules 是 ECMAScript modules 的簡寫贸弥,也可寫為 ESM窟坐。 ES Modules 是 js 官方推出的標(biāo)準(zhǔn)
  • ES Modules 相比于其他模塊規(guī)范是一個靜態(tài)化的模塊解決方案,其他模塊化方案都是運(yùn)行時才能確定輸出內(nèi)容绵疲,而 ES Modules 是編譯時就確定了的哲鸳。其他模塊化方案導(dǎo)入文件都是整個導(dǎo)入模塊,而 ES Modules 可以只導(dǎo)入需要的部分
  • ES Modules 會自動采用嚴(yán)格模式最岗,不需要像 ES5 一樣在頭部加上”use strict”
  • ES Modules 可運(yùn)行在服務(wù)端(node)和瀏覽器帕胆。目前主流瀏覽器都已經(jīng)支持 ES Modules,node 使用 ES Modules 需要在執(zhí)行時加上--experimental-modules般渡,且要求編寫的 js 文件必須以.mjs 為后綴
  • ES Modules 導(dǎo)出的是一個值得引用,即在模塊內(nèi)改變了導(dǎo)出值,那么下一次使用也會得到新的值
  • 如下有兩個文件驯用,執(zhí)行命令node --experimental-modules index.mjs脸秽,會有什么結(jié)果?

lib.mjs

// lib.mjs
export let counter = 3

export function incCounter() {
  counter++
}

index.mjs

// index.mjs
import * as mod from './lib’

// 此處輸出值蝴乔??console.log(mod.counter)
mod.incCounter()

// 此處輸出值记餐?
console.log(mod.counter)

循環(huán)引用

請問執(zhí)行node --experimental-modules main.mjs后會輸出什么內(nèi)容
a.mjs

// a.mjs
import { bar } from './b.mjs'
console.log('a.mjs')
console.log(bar)
export let foo = 'foo'

b.mjs

// b.mjs
import { foo } from './a.mjs'
console.log('b.mjs')
console.log(foo)
export let bar = 'bar'

main.mjs

// main.mjs
import './a.mjs'

循環(huán)依賴問題

  • 在所有的模塊規(guī)范中都存在循環(huán)依賴問題,解決依賴循環(huán)的方式都相似薇正,幾乎都采用惰性導(dǎo)入的方式來解決片酝。
  • 如下兩個文件存在循環(huán)引用,當(dāng)執(zhí)行 node --experimental-modules a.mjs 時挖腰,會報錯說 b 未定義雕沿,這就是由于循環(huán)依賴導(dǎo)致的,如果不使用 b 則不會報錯猴仑,修改方案如下审轮。其他的模塊循環(huán)引用也可按照此方法進(jìn)行修改。
  • CommonJS 也可以使用先導(dǎo)出自身辽俗,再引入其他模塊的方式盡心避免疾渣。同時也可以把 require 放入到函數(shù)體中,即在調(diào)用的時后才去加載依賴
循環(huán)依賴

相關(guān)鏈接

關(guān)于作者

歡迎關(guān)注 nashaofu

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末朱浴,一起剝皮案震驚了整個濱河市吊圾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赊琳,老刑警劉巖街夭,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異躏筏,居然都是意外死亡板丽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門趁尼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埃碱,“玉大人,你說我怎么就攤上這事酥泞⊙獾睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵芝囤,是天一觀的道長似炎。 經(jīng)常有香客問我辛萍,道長,這世上最難降的妖魔是什么羡藐? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任贩毕,我火速辦了婚禮,結(jié)果婚禮上仆嗦,老公的妹妹穿的比我還像新娘辉阶。我一直安慰自己,他們只是感情好瘩扼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布谆甜。 她就那樣靜靜地躺著,像睡著了一般集绰。 火紅的嫁衣襯著肌膚如雪规辱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天倒慧,我揣著相機(jī)與錄音按摘,去河邊找鬼。 笑死纫谅,一個胖子當(dāng)著我的面吹牛炫贤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播付秕,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼兰珍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了询吴?” 一聲冷哼從身側(cè)響起掠河,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猛计,沒想到半個月后唠摹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奉瘤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年勾拉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盗温。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡藕赞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卖局,到底是詐尸還是另有隱情斧蜕,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布砚偶,位于F島的核電站批销,受9級特大地震影響洒闸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜风钻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一顷蟀、第九天 我趴在偏房一處隱蔽的房頂上張望酒请。 院中可真熱鬧骡技,春花似錦、人聲如沸羞反。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昼窗。三九已至是趴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澄惊,已是汗流浹背唆途。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掸驱,地道東北人肛搬。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像毕贼,于是被迫代替她去往敵國和親温赔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Javascript模塊化編程章郁,已經(jīng)成為一個迫切的需求枉氮。理想情況下,開發(fā)者只需要實(shí)現(xiàn)核心的業(yè)務(wù)邏輯驱犹,其他都可以加載...
    zhoulujun閱讀 2,940評論 0 14
  • 模塊通常是指編程語言所提供的代碼組織機(jī)制嘲恍,利用此機(jī)制可將程序拆解為獨(dú)立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,170評論 0 0
  • 前言在 JavaScript 發(fā)展初期就是為了實(shí)現(xiàn)簡單的頁面交互邏輯雄驹,寥寥數(shù)語即可佃牛;如今 CPU、瀏覽器性能得到了...
    前端一菜鳥閱讀 835評論 0 9
  • 第一部分 如果時光定格在那一刻医舆,大概就能看見這位三旬怒父和七八歲的抽泣小女孩俘侠,還有一個舊鐘象缀,它因女孩生氣而被摔得...
    子愚小魯閱讀 237評論 5 0
  • 拖了2個月的文兒終于啟封了,算是自己決定用文字記錄生活爷速,記錄真實(shí)的自己的第一步吧~ 從知道吉祥薩迦紅城寺法會到最終...
    zoe不惑閱讀 305評論 0 0