webpack原理-webpack模板代码

webpack 模板代码是什么

webpack 都打包项目后,我们可以看到在我们的业务代码外面被包裹在了一段代码里面,而这段代码就是 webpack 生成的 runtime 代码,这里我就简称为 webpack 模板代码了

webpack 没有代码切割时生成文件

  • 示例目录结构
.
├── README.md
├── dist # 打包生成文件
│   ├── 1.js # src/chunk.js => 1.js
│   ├── app.js # src/index.js => app.js
│   └── splitChunk.js # src/splitChunk.js => splitChunk.js
├── package.json
├── src
│   ├── chunk.js # 动态加载的chunk
│   ├── index.js # 非代码切割入口文件
│   └── splitChunk.js # 代码切割入口文件
├── webpack.config.js
└── yarn.lock
  • index.js
// index.js
console.log('app')
  • build 之后的文件
;(function(modules) {
  // webpackBootstrap
  // The module cache
  var installedModules = {}

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    // Create a new module (and put it into the cache)
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    })

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

    // Flag the module as loaded
    module.l = true

    // Return the exports of the module
    return module.exports
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules

  // expose the module cache
  __webpack_require__.c = installedModules

  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter })
    }
  }

  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
    }
    Object.defineProperty(exports, '__esModule', { value: true })
  }

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if (mode & 1) value = __webpack_require__(value)
    if (mode & 8) return value
    if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value
    var ns = Object.create(null)
    __webpack_require__.r(ns)
    Object.defineProperty(ns, 'default', { enumerable: true, value: value })
    if (mode & 2 && typeof value != 'string')
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function(key) {
            return value[key]
          }.bind(null, key)
        )
    return ns
  }

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            return module['default']
          }
        : function getModuleExports() {
            return module
          }
    __webpack_require__.d(getter, 'a', getter)
    return getter
  }

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
    return Object.prototype.hasOwnProperty.call(object, property)
  }

  // __webpack_public_path__
  __webpack_require__.p = ''

  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = 0))
})([
  /* 0 */
  function(module, exports) {
    /* 这里是index.js打包后的代码 */
    console.log('app')
  }
])
  • 可以看到,上面代码主要是生成__webpack_require__函数相关的内容,__webpack_require__是 webpack runtime 是加载内部模块的方法
  • 生成代码是立即执行函数表达式(IIFE),立即执行函数的调用参数是 webpack 处理的所有模块,一般情况下会一个文件为一个模块。modules 可为对象也可为数组,对象键值为模块路径或者模块 ID,值为模块的具体代码。为数组时,数组下标为模块 id
  • 可以看到模块在被加载时会被传入三个参数,分别是moduleexports__webpack_require__

简化一下以上代码可以如下

;(function(modules) {
  // 用于缓存已经加载过的模块
  var installedModules = {}

  // 加载模块的方法
  function __webpack_require__(moduleId) {
    // 如果模块存在于缓存中就直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    // 定义一个新的模块对象,并且存到缓存里
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    })

    // 加载模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

    // 模块加载完成标记
    module.l = true

    // 返回模块导出的对象
    return module.exports
  }

  // 加载入口模块
  return __webpack_require__(0)
})([
  /* 0 */
  function(module, exports) {
    /* 这里是index.js打包后的代码 */
    console.log('app')
  }
])

webpack 存在代码切割时生成文件

  • splitChunk.js
// splitChunk.js
import('./chunk').then(chunk => {
  chunk.default()
  console.log('done')
})
  • chunk.js
// chunk.js
export default () => {
  console.log('I am chunk')
}
  • build 后生成splitChunk.js文件
;(function(modules) {
  // webpackBootstrap
  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {
    var chunkIds = data[0]
    var moreModules = data[1]

    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId,
      chunkId,
      i = 0,
      resolves = []
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i]
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0])
      }
      installedChunks[chunkId] = 0
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId]
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data)

    while (resolves.length) {
      resolves.shift()()
    }
  }

  // The module cache
  var installedModules = {}

  // object to store loaded and loading chunks
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // Promise = chunk loading, 0 = chunk loaded
  var installedChunks = {
    0: 0
  }

  // script path function
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.js'
  }

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    // Create a new module (and put it into the cache)
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    })

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

    // Flag the module as loaded
    module.l = true

    // Return the exports of the module
    return module.exports
  }

  // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = []

    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId]
    if (installedChunkData !== 0) {
      // 0 means "already installed".

      // a Promise means "currently loading".
      if (installedChunkData) {
        promises.push(installedChunkData[2])
      } else {
        // setup Promise in chunk cache
        var promise = new Promise(function(resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject]
        })
        promises.push((installedChunkData[2] = promise))

        // start chunk loading
        var script = document.createElement('script')
        var onScriptComplete

        script.charset = 'utf-8'
        script.timeout = 120
        if (__webpack_require__.nc) {
          script.setAttribute('nonce', __webpack_require__.nc)
        }
        script.src = jsonpScriptSrc(chunkId)

        // create error before stack unwound to get useful stacktrace later
        var error = new Error()
        onScriptComplete = function(event) {
          // avoid mem leaks in IE.
          script.onerror = script.onload = null
          clearTimeout(timeout)
          var chunk = installedChunks[chunkId]
          if (chunk !== 0) {
            if (chunk) {
              var errorType = event && (event.type === 'load' ? 'missing' : event.type)
              var realSrc = event && event.target && event.target.src
              error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'
              error.name = 'ChunkLoadError'
              error.type = errorType
              error.request = realSrc
              chunk[1](error)
            }
            installedChunks[chunkId] = undefined
          }
        }
        var timeout = setTimeout(function() {
          onScriptComplete({ type: 'timeout', target: script })
        }, 120000)
        script.onerror = script.onload = onScriptComplete
        document.head.appendChild(script)
      }
    }
    return Promise.all(promises)
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules

  // expose the module cache
  __webpack_require__.c = installedModules

  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter })
    }
  }

  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
    }
    Object.defineProperty(exports, '__esModule', { value: true })
  }

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if (mode & 1) value = __webpack_require__(value)
    if (mode & 8) return value
    if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value
    var ns = Object.create(null)
    __webpack_require__.r(ns)
    Object.defineProperty(ns, 'default', { enumerable: true, value: value })
    if (mode & 2 && typeof value != 'string')
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function(key) {
            return value[key]
          }.bind(null, key)
        )
    return ns
  }

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            return module['default']
          }
        : function getModuleExports() {
            return module
          }
    __webpack_require__.d(getter, 'a', getter)
    return getter
  }

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
    return Object.prototype.hasOwnProperty.call(object, property)
  }

  // __webpack_public_path__
  __webpack_require__.p = ''

  // on error function for async loading
  __webpack_require__.oe = function(err) {
    console.error(err)
    throw err
  }

  var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || [])
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray)
  jsonpArray.push = webpackJsonpCallback
  jsonpArray = jsonpArray.slice()
  for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i])
  var parentJsonpFunction = oldJsonpFunction

  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = 0))
})([
  /* 0 */
  function(module, exports, __webpack_require__) {
    __webpack_require__
      .e(1)
      .then(__webpack_require__.bind(null, 1))
      .then(chunk => {
        chunk.default()
        console.log('done')
      })
  }
])
  • 与前面没有代码切割时,主要多了webpackJsonpCallback__webpack_require__.e函数
  • 简化后代码如下
;(function(modules) {
  // js加载后执行的回调
  // 一个chunk钟可以包含很多module
  function webpackJsonpCallback(data) {}

  // 已经被加载的chunk
  // chunk值为0表示已经加载过了
  var installedChunks = {
    0: 0
  }

  // require模块
  function __webpack_require__(moduleId) {}
  // 加载js文件,插入script标签
  __webpack_require__.e = function requireEnsure(chunkId) {}

  // 读取已经在webpackJsonp中的模块,以在代码中使用
  var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || [])
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray)
  // push方法修改为本模块的回调
  jsonpArray.push = webpackJsonpCallback
  jsonpArray = jsonpArray.slice()
  for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i])
  var parentJsonpFunction = oldJsonpFunction

  return __webpack_require__((__webpack_require__.s = 0))
})([
  /* 0 */
  function(module, exports, __webpack_require__) {
    __webpack_require__
      .e(1) // 插入异步js的script标签
      // 加载的js加载成功后自动执行webpackJsonp.push方法
      // 把模块注册到modules上
      // 加载该模块
      .then(__webpack_require__.bind(null, 1))
      .then(chunk => {
        chunk.default()
        console.log('done')
      })
  }
])
  • 打包后1.js内容
;(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [1],
  [
    ,
    /* 0 */
    /* 1 */
    function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      __webpack_require__.r(__webpack_exports__)
      __webpack_exports__['default'] = () => {
        console.log('I am chunk')
      }
    }
  ]
])
  • 代码切割后不同文件共享模块实现是通过全局变量webpackJsonp来实现的
  • 异步的 js 文件加载成功后会制动执行代码,执行代码为window['webpackJsonp'].push方法,实际执行的函数为splitChunk.js中的webpackJsonpCallback方法
  • 简单执行流程如下


    chunk执行流程

提取 webpack runtime 到单独文件

设置 webpack 配置optimization.runtimeChunktrue,即可把 webpack 生成的模板文件到单独的文件里去,其他所有文件都会分离为 jsonp 形式的文件

实现一个简单的 webpack 模块加载功能

实现__webpack_require__window['webpackJsonp'].push方法,具体代码在webpack-require中查看

  • index.js
;(function(modules) {
  // 缓存已经被require的模块
  const installedModules = {}
  // 加载模块
  function __webpack_require__(id) {
    if (installedModules[id]) {
      return installedModules[id].exports
    }
    const module = (installedModules[id] = {
      loaded: false,
      exports: {}
    })
    modules[id].call(module.exports, module, module.exports, __webpack_require__)
    // 标记模块已经被加载完成
    module.loaded = true
    return module.exports
  }

  // 缓存已经被加载的chunk
  const installedChunks = {}

  // js chunk文件加载成功后执行的回调
  function webpackJsonpCallback(data) {
    // js文件中存在的chunk名称
    const chunkIds = data[0]
    // chunk中导出的模块
    const moreModules = data[1]

    const resolves = []

    // 让包含在一个js文件中的所有chunk都完成加载
    // 一个js文件可能包含多个打包前的文件
    for (i = 0; i < chunkIds.length; i++) {
      const chunkId = chunkIds[i]
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0])
      }
      installedChunks[chunkId] = 0
    }

    // 拷贝chunk中的模块
    for (let moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId]
      }
    }

    // 让__webpack_require__.e执行返回的promise全部resolve
    while (resolves.length) {
      resolves.shift()()
    }
  }

  // 加载已经存在于window.webpackJsonp中的模块
  const jsonpArray = (window.webpackJsonp = window.webpackJsonp || [])
  jsonpArray.push = webpackJsonpCallback
  for (let i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i])

  // 插入script标签
  __webpack_require__.e = function(chunkId) {
    const promises = []
    let installedChunkData = installedChunks[chunkId]
    // 已经缓存就直接返回
    if (installedChunkData === 0) return Promise.all(promises)
    if (installedChunkData) {
      // 加载中的情况
      promises.push(installedChunkData[2])
    } else {
      // 全新加载
      const promise = new Promise((resolve, reject) => {
        installedChunkData = installedChunks[chunkId] = [resolve, reject]
      })
      installedChunkData[2] = promise
      promises.push(promise)
      const script = document.createElement('script')

      // 根据chunkId转换为url
      const chunksSrc = {
        './chunk.js': './chunk.js',
        './chunks-1.js': './chunks.js',
        './chunks-2.js': './chunks.js'
      }
      script.src = chunksSrc[chunkId]

      // js加载完成事件
      onScriptComplete = function(event) {
        clearTimeout(timeout)
        // 避免IE内存泄漏
        script.onerror = script.onload = null
        // 没有在已经完成的chunk里面就认为加载失败
        const chunk = installedChunks[chunkId]
        if (chunk !== 0) {
          if (chunk) {
            chunk[1](`${chunkId}加载失败`)
          }
          installedChunks[chunkId] = undefined
        }
      }
      script.onerror = script.onload = onScriptComplete
      // 超时设置
      const timeout = setTimeout(() => onScriptComplete(), 120000)
      document.head.appendChild(script)
    }
    return Promise.all(promises)
  }

  // 加载入口文件
  return __webpack_require__('./index.js')
})({
  './index.js': function(module, exports, __webpack_require__) {
    console.log('I am index.js')

    // 加载./chunk.js
    __webpack_require__
      .e('./chunk.js')
      .then(() => __webpack_require__('./chunk.js'))
      .then(data => {
        data.default()
      })

    // 加载./chunks-1.js
    __webpack_require__
      .e('./chunks-1.js')
      .then(() => __webpack_require__('./chunks-1.js'))
      .then(data => {
        data.default()
      })

    // 加载./chunks-2.js
    __webpack_require__
      .e('./chunks-2.js')
      .then(() => __webpack_require__('./chunks-2.js'))
      .then(data => {
        data.default()
      })
  }
})
  • chunk.js
;(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  ['./chunk.js'],
  {
    './chunk.js': function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      __webpack_exports__['default'] = () => {
        console.log('I am chunk')
      }
    }
  }
])
  • chunks.js
// 一个文件包含多个chunk
;(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  ['./chunks-1.js', './chunks-2.js'],
  {
    './chunks-1.js': function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      __webpack_exports__['default'] = () => {
        console.log('I am chunks-1')
      }
    },
    './chunks-2.js': function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      __webpack_exports__['default'] = () => {
        console.log('I am chunks-2')
      }
    }
  }
])

链接

本文同步发布到 github,里面的示例代码可在 github 上查看 webpack 模板代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容