webpack原理-webpack模板代码

·  阅读 1161

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 模板代码

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改