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

當前主流 JS 模塊化方案

  • CommonJS 規範,nodejs 實現的規範
  • AMD 規範,requirejs 實現的規範
  • CMD 規範,seajs 實現的規範, seajs 與 requirejs 實現原理有不少類似的地方 u ES Modules,當前 js 標準模塊化方案

注意:cjs、amd、cmd、 ES Modules 都是隻規範,因此可能對應有多種實現css

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

無模塊化時代

一把梭

<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>
複製代碼

無模塊化時代的問題node

  • 污染全局做用域
  • 不便於拆分邏輯,維護成本高 • 依賴關係不明顯
  • 複用性差

CommonJS 規範

  • CommonJS 是由 node 實現的一套規範,關於 CommonJS 的提出可參考CommonJS 規範
  • require 源碼解讀可參考 require() 源碼解讀
  • 模塊包裝至關於執行以下代碼, compiledWrapper 是調用 node 封裝的 V8 原生建立函數的方法返回的一個函數
function compiledWrapper(exports, require, module, __filename, __dirname) {
  // 插入文件中的代碼
  // 返回導出對象
  return module.exports
}
compiledWrapper.call(exports, exports, require, module, filename, dirname)
複製代碼
  • CommonJS 模塊輸出的是一個值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值
  • 以下有兩個文件,執行命令node index.js,會有什麼結果?

lib.jsjquery

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

index.jswebpack

// index.js
const mod = require('./lib') // 此處輸出值?
console.log(mod.counter)
mod.incCounter() // 此處輸出值?
console.log(mod.counter)
複製代碼
  • equire 命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象,下次加載會直接從緩存中取數據
  • 如下是一個循環引用的例子,請問執行 node main.js 後會輸出什麼?

a.jsgit

// 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.jsgithub

// 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.jsweb

// 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 規範

  • AMD 是 Asynchronous Module Definition 的簡寫,即異步模塊定義
  • AMD 規範的完整定義可參考 github.com/amdjs/amdjs…
  • requirejs 是在瀏覽器中運行的,全部一些基礎庫須要先配置,以方便其餘庫調用,能夠理解爲 CommonJS 中的 node_modules 下的包。業務模塊也可定義在其中,可認爲是路徑別名。paths 中的路徑不能包含擴展名。
require.config({
  paths: {
    // 若是第一個加載失敗就會加載第二個
    jquery: ['lib/jquery.min', 'lib/jquery'],
    lodash: 'lib/lodash.min',
    main: './mian' // 入口文件
  }
})
複製代碼

定義模塊

/** * 定義模塊,當依賴加載完成後執行回調 * 回調可返回值,返回值會被導出到外部使用 * @param {String} id 模塊名稱,可省略 * @param {Array} dependencies 依賴的模塊 * @param {Function} factory 回調函數 */
define(id?, dependencies?, factory);

複製代碼
define(['jquery'], function($) {
  $('body').css({ background: 'red' })
  // 導出log函數
  return (...args) => console.log('自定義log', ...args)
})
複製代碼

加載模塊

/** * 加載模塊 * @param {Array} deps 要加載的模塊 * @param {Function} callback 加載成功回調,回調參數爲加載模塊導出對象 * @param {Function} errback 加載失敗回調 */
requirejs(deps, callback, errback)
複製代碼
require(['main'], log => {
  log('我成功加載了‘)
  // do something...,也能夠在這裏繼續require其餘js文件
})
複製代碼

requirejs 使用示例

  • 目錄結構
.
├── 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 規範

  • CMD 是 Common Module Definition 的簡寫,即通用模塊定義
  • CMD 規範的完整定義可參考https://github.com/seajs/seajs/issues/242
  • CMD 的主要表明是 seajs。CMD 推崇依賴就近,AMD 推崇依賴前置。即 AMD 在定義模塊的時候就必須把依賴包含進來,CMD 是在使用的時候再 require 對應的依賴
  • 當前主流的庫對 CMD 支持不是很友好,都須要額外的修改才能工做
  • AMD 與 CMD 寫法對好比下
// CMD
// 代碼寫起來有同步require的感受
define((require, exports, module) => {
  const $ = require('jquery‘)
  $('title').text('hello')
})
複製代碼
// AMD
// 明顯的異步風格
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" 爲函數參數!
function F(require) {} // 錯誤 - 在內嵌做用域內重定義了 "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 函數的特殊要求是因爲,seajs 原理致使的,seajs 的執行流程大體以下 api

seajs執行流程

seajs 使用示例

  • 目錄結構
.
├── 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 官方推出的標準
  • ES Modules 相比於其餘模塊規範是一個靜態化的模塊解決方案,其餘模塊化方案都是運行時才能肯定輸出內容,而 ES Modules 是編譯時就肯定了的。其餘模塊化方案導入文件都是整個導入模塊,而 ES Modules 能夠只導入須要的部分
  • ES Modules 會自動採用嚴格模式,不須要像 ES5 同樣在頭部加上」use strict」
  • ES Modules 可運行在服務端(node)和瀏覽器。目前主流瀏覽器都已經支持 ES Modules,node 使用 ES Modules 須要在執行時加上--experimental-modules,且要求編寫的 js 文件必須以.mjs 爲後綴
  • ES Modules 導出的是一個值得引用,即在模塊內改變了導出值,那麼下一次使用也會獲得新的值
  • 以下有兩個文件,執行命令node --experimental-modules index.mjs,會有什麼結果?

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)

複製代碼

循環引用

請問執行node --experimental-modules main.mjs後會輸出什麼內容 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'
複製代碼

循環依賴問題

  • 在全部的模塊規範中都存在循環依賴問題,解決依賴循環的方式都類似,幾乎都採用惰性導入的方式來解決。
  • 以下兩個文件存在循環引用,當執行 node --experimental-modules a.mjs 時,會報錯說 b 未定義,這就是因爲循環依賴致使的,若是不使用 b 則不會報錯,修改方案以下。其餘的模塊循環引用也可按照此方法進行修改。
  • CommonJS 也可使用先導出自身,再引入其餘模塊的方式盡心避免。同時也能夠把 require 放入到函數體中,即在調用的時後纔去加載依賴

循環依賴

相關連接

關於做者

歡迎關注 nashaofu

相關文章
相關標籤/搜索