前端知识点——手写简单Webpack

写在前面

笔记内容大多出自于拉勾教育大前端高薪训练营的教程,因此也许会和其他文章雷同较多,请担待。

webpack

  • 打包后的文件就是一个自调用函数,当前函数调用时传入一个对象
  • 这个对象我们为了方便将之称为是模块定义,他就是一个键值对
  • 这个键名就是当前被加载模块的文件名和某个目录的拼接
  • 这个键值就是一个函数,和node.js里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
  • 这个函数在将来某个时间点上会被调用,同时接收一定的参数,利用这些参数就可以实现模块的加载操作
  • 默认commonJS规范
  • 两个模块的规范可以不同,但一个模块内只能用一种规范,否则会报错 -> Uncaught TypeError: Cannot assign to read only property ‘exports’ of object ‘#’
$ npm i webapck webpack-cli --dev
$ md webpack.config.js
// webpack.config.js
const path = require('path')
module.exports = {
  mode: 'development', // 打包模式 development / production
  entry: './src/index.js', // 打包入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.join(__dirname, 'dist'), // 输出路径,因为必须绝对路径,所以使用path来生成绝对路径
    publicPath: 'dist/' // 网站的根目录,因为在未打包index.html时已完成打包的资源文件会进入dist,但对于没有打包的index.html来说并不会改变引用路径,所以需要重新指定资源文件的路径,从dist下面寻找
  },
  module: {
    rules: [
      {
        test: /.css$/,
        loader: [
          'style-loader', // 将css文件打包成的js文件进行引用
          'css-loader' // 将css文件打包成js文件
        ]
      },
      /* ---------------- start ---------------- */
      // 将小文件交给url-loader去转换为Base64,优化加载速率
      // 将大文件交给file-loader去生成新资源并修改路径,因为大文件转换为Base64之后也许会更大
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: 'file-loader' // 该loader可以处理图片、音视频、字体的路径
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: {
          loader: 'url-loader', // 该loader可以处理图片、音视频、字体的本体,将其转换为Base64
          options: {
            limit: 10 * 1024 // 10kb
          }
        }
      }
      /* ---------------- end ---------------- */
    ]
  }
}

简单打包两个文件,其中index.js引用了header.js

webpack 原理

// dist/main.js
(function (modules) {
  // 当模块已被加载则会放入缓存
	var installedModules = {};
  // 核心作用是返回模块的exports
	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
	}
  // 将传入的模块缓存到属性 m 上 - modules
	__webpack_require__.m = modules
  // 将已加载模块缓存到属性 c 上 - cache
	__webpack_require__.c = installedModules
  // 判断对象身上是否有某属性 - hasOwnProperty
	__webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property)
  }
  // 为exports定义属性 - defineProperty
	__webpack_require__.d = function (exports, name, getter) {
		if (!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, {
        enumerable: true,
        get: getter
      })
		}
	}
  // 为exports挂载__esModule: true属性 - redefine
	__webpack_require__.r = function (exports) {
    // 判断是否是es6,简而言之就是为了判断是否是ES Module
    // Object.property.toString.call(exports) -> Module
		if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, {
        value: 'Module'
      })
		}
		Object.defineProperty(exports, '__esModule', {
      value: true
    })
	}
	__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
	}
	__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
	}
  // path
	__webpack_require__.p = ""
	return __webpack_require__(__webpack_require__.s = "./src/index.js")
})

index.js使用require导入模块/module.exports导出模块(CommonJS) <------> header.js使用module.export导出模块(CommonJS)

// index.js
const header = require('./header')
console.log('index 内容 -> next:', header)
module.exports = 'here entry'
// header.js
console.log('header 内容')
module.exports = 'here header'
// dist/main.js
({
  "./src/header.js":
    (function (module, exports) {
      console.log('header 内容')
      module.exports = 'here header'
    }),
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      const header = __webpack_require__("./src/header.js")
      console.log('index 内容 -> next:', header)
      module.exports = 'here entry'
    })
})

index.js使用require导入模块/module.exports导出模块(CommonJS) <------> header.js使用export default导出模块(ES Module)

// index.js
const header = require('./header')
console.log('index 内容 -> next:', header)
module.exports = 'here entry'
// header.js
console.log('header 内容')
export default 'here header'
// dist/main.js
({
  "./src/header.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      console.log('header 内容')
      __webpack_exports__["default"] = ('here header')
    }),
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      const header = __webpack_require__("./src/header.js")
      console.log('index 内容 -> next:', header)
      module.exports = 'here entry'
    })
})

index.js使用import导入模块/export default导出模块(ES Module) <-----> header.js使用module.export导出模块(CommonJS)

// index.js
import header from './header'
console.log('index 内容 -> next:', header)
export default 'here entry'
// header.js
console.log('header 内容')
module.exports = 'here header'
// dist/main.js
({
  "./src/header.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      console.log('header 内容')
      __webpack_exports__["default"] = ('here header')
    }),
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js")
      console.log('index 内容 -> next:', _header__WEBPACK_IMPORTED_MODULE_0__["default"])
      __webpack_exports__["default"] = ('here entry')
    })
})

index.js使用import导入模块/export const导出模块(ES Module) <-------> header.js使用export default导出模块(ES Module)

// index.js
import header from './header'
console.log('index 内容 -> next:', header)
export const message = 'here entry'
// header.js
console.log('header 内容')
export default 'here header'
// dist/main.js
({
  "./src/header.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      console.log('header 内容')
      __webpack_exports__["default"] = ('here header')
    }),
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      __webpack_require__.d(__webpack_exports__, "message", function () {
        return message
      })
      var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js")
      console.log('index 内容 -> next:', _header__WEBPACK_IMPORTED_MODULE_0__["default"])
      const message = 'here entry'
    })
})
  • import()可以实现模块的懒加载
  • 懒加载的核心原理就是jsonp
  • webpack_require.t方法可以针对内容进行不同的处理(取决于二进制位运算符的布尔值)

懒加载

webpack 原理

(function (modules) {
	function webpackJsonpCallback(data) {
		var chunkIds = data[0]
		var moreModules = data[1]
		var moduleId, chunkId, i = 0, resolves = [];
		for (; i < chunkIds.length; i++) {
			chunkId = chunkIds[i]
			if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && 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()()
		}
	}
	var installedModules = {}
  // 0 已加载
  // undefine 未加载
  // promise 加载中
	var installedChunks = {
		"main": 0
	}
	function jsonpScriptSrc(chunkId) {
		return __webpack_require__.p + "" + chunkId + ".main.js"
	}
	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
	}
	__webpack_require__.e = function requireEnsure(chunkId) {
		var promises = []
		var installedChunkData = installedChunks[chunkId]
		if (installedChunkData !== 0) {
			if (installedChunkData) {
				promises.push(installedChunkData[2])
			} else {
				var promise = new Promise(function (resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject]
				})
				promises.push(installedChunkData[2] = promise)
				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);
				var error = new Error()
				onScriptComplete = function (event) {
					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)
	};
	__webpack_require__.m = modules
	__webpack_require__.c = installedModules
	__webpack_require__.d = function (exports, name, getter) {
		if (!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, {
				enumerable: true,
				get: getter
			})
		}
	}
	__webpack_require__.r = function (exports) {
		if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, {
				value: 'Module'
			})
		}
		Object.defineProperty(exports, '__esModule', {
			value: true
		})
	}
	__webpack_require__.t = function (value, mode) {
    /*
      - 接收2个参数,value一般是moduleId,mode是一个二进制数值
      - 首先使用__webpack_require__加载模块覆盖value的值
      - 当(mode & 8)成立时直接返回value(CommonJS)
      - 当(mode & 4)成立时直接返回value(ES Module)
      - 当上述都不成立,则需要定义一个命名空间 ns {}
       + 如果value是一个可以直接使用的值,比如字符串/数值,则将它挂载到 ns的 default 属性上
       + 否则遍历value,将value的每个属性都挂载到 ns 上,然后返回 ns
     */
		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
	}
	__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
	}
	__webpack_require__.o = function (object, property) {
		return Object.prototype.hasOwnProperty.call(object, property)
	}
	__webpack_require__.p = "";
	__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
	return __webpack_require__(__webpack_require__.s = "./src/index.js")
})
<body>
  <div id="btn">buttondiv>
body>

index.js使用import()懒加载导入模块/module.exports导出模块(CommonJS) <------> header.js使用module.exports导出模块(CommonJS)

// index.js
console.log('index 内容')
document.getElementById('btn').addEventListener('click', () => {
  import(/* webpackChunkName: "button" */'./header').then(data => {
    console.log(data)
  })
})
module.exports = 'here entry'
// header.js
console.log('header 内容')
module.exports = 'here header'
// dist/button.main.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["button"], {
  "./src/header.js":
    (function (module, exports) {
      console.log('header 内容')
      module.exports = 'here header'
    })
}])
// dist/main.js
({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      console.log('index 内容')
      document.getElementById('btn').addEventListener('click', () => {
        __webpack_require__.e("button").then(__webpack_require__.t.bind(null, "./src/header.js", 7)).then(data => {
          console.log(data)
        })
      })
      module.exports = 'here entry'
    })
})

index.js使用import()懒加载导入模块/module.exports导出模块(CommonJS) <------> header.js使用export default导出模块(ES Module)

// index.js
console.log('index 内容')
document.getElementById('btn').addEventListener('click', () => {
  import(/* webpackChunkName: "button" */'./header').then(data => {
    console.log(data)
  })
})
module.exports = 'here entry'
// header.js
console.log('header 内容')
export default 'here header'
// dist/button.main.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["button"], {
  "./src/header.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__);
      console.log('header 内容')
      __webpack_exports__["default"] = ('here header')
    })
}])
// dist/main.js
({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      console.log('index 内容')
      document.getElementById('btn').addEventListener('click', () => {
        __webpack_require__.e("button").then(__webpack_require__.bind(null, "./src/header.js")).then(data => {
          console.log(data)
        })
      })
      module.exports = 'here entry'
    })
})

index.js使用import()懒加载导入模块/export default导出模块(ES Module) <-----> header.js使用module.exports导出模块(CommonJS)

// index.js
console.log('index 内容')
document.getElementById('btn').addEventListener('click', () => {
  import(/* webpackChunkName: "button" */'./header').then(data => {
    console.log(data)
  })
})
export default 'here entry'
// header.js
console.log('header 内容')
module.exports = 'here header'
// dist/button.main.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["button"], {
  "./src/header.js":
    (function (module, exports) {
      console.log('header 内容')
      module.exports = 'here header'
    })
}])
// dist/main.js
({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      console.log('index 内容')
      document.getElementById('btn').addEventListener('click', () => {
        __webpack_require__.e("button").then(__webpack_require__.t.bind(null, "./src/header.js", 7)).then(data => {
          console.log(data)
        })
      })
      __webpack_exports__["default"] = ('here entry')
    })
})

index.js使用import()懒加载导入模块/export const导出模块(ES Module) <-------> header.js使用export default导出模块(ES Module)

// index.js
console.log('index 内容')
document.getElementById('btn').addEventListener('click', () => {
  import(/* webpackChunkName: "button" */'./header').then(data => {
    console.log(data)
  })
})
export default 'here entry'
// header.js
console.log('header 内容')
export default 'here header'
// dist/button.main.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["button"], {
  "./src/header.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      console.log('header 内容')
      __webpack_exports__["default"] = ('here header')
    })
}])
// dist/main.js
({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      __webpack_require__.r(__webpack_exports__)
      __webpack_require__.d(__webpack_exports__, "message", function () {
        return message
      })
      console.log('index 内容')
      document.getElementById('btn').addEventListener('click', () => {
        __webpack_require__.e("button").then(__webpack_require__.bind(null, "./src/header.js")).then(data => {
          console.log(data)
        })
      })
      const message = 'here entry'
    })
})

手写Webpack

原理

  • 1.读取 webpack.config.js 文件配置 options,并且与内置的默认对象合并,将其作为参数实例化 Compiler 生成 compiler 对象,并将 options 也挂载到 compiler 身上
  • 2.调用 NodeEnvironmentPlugin 将 compiler 身上挂载上文件读写能力
  • 3.将 webpack.config.js 中的 plugins 遍历调用 apply ,将插件修改的内容同步到 compiler 身上
  • 4.将内部插件挂载到 compiler 身上
    • 4-1.实例化一个入口配置插件(EntryOptionPlugin),并在配置完就调用
    • 4-2.在 EntryOptionPlugin 插件中监听 make 钩子,当 make 钩子被触发时就意味着 compile 钩子已经被调用,这时候已经可以获取到 compilation 了
  • 5.返回 compiler 对象等待 run 方法的执行
  • 6.调用 compiler.run 方法
    • 6-1.按顺序执行 compiler 对象里 beforeRun 和 run 的钩子,让其中的 _x 执行,并在 run 钩子执行的回调中执行 compile 方法进入进行编译流程
    • 6-2.compile 方法中按顺序执行 compiler 对象里 beforeCompile 和 compile 的钩子,并在 compile 钩子执行的回调中实例化 Compilation 生成 compilation 对象,compilation 对象因为 constructor 中传入的是this,也就是当前 compiler 对象,这样生成的 compilation 对象也就会具备了文件读写能力
    • 6-3.然后触发 make 钩子的执行并将 compilation 通过钩子入参传到 make 钩子监听的方法中,也就是上面的第4步
      • 6-3 -> 4-3.通过 make 钩子传入的 compilation 调用 compilation 自身的 addEntry 方法创建入口模块
      • 6-3 -> 4-4.调用 compilation 对象中的 _addModuleChain 方法将入口文件的配置传过去,然后实例化 NormalModule 生成一个新模块对象 module,module身上挂在了文件入口路径模块id模块源码模块ast编译源码到AST语法树的方法以及该模块所依赖的模块这些成员变量,这些都是在实例化时传入的参数,然后通过 this 进行绑定的
      • 6-3 -> 4-5.调用 module 身上的 build 方法,并将 compilation 对象传入进去,这样就可以利用 compilation 所具备的文件读写能力进行源码的获取,因为 module 身上挂在了文件入口路径成员变量,所以可以通过这个地址就能找到源码,并且读取。
      • 6-3 -> 4-6.读取完源码之后可以通过 babylon 这个插件将源码编译成 AST语法树,因为在 6-3 -> 4-4 中提到过在实例化 NormalModule 时会传入一个编译方法,所以这里就调用这个方法(即this.parser,这个方法是实例化后的 babylon 的对象)
      • 6-3 -> 4-7.得到 AST语法树 后就可以将里面的内容进行替换,使用 @babel/traverse 定位到需要替换的内容进行替换,然后再通过 @babel/generator 将替换完成的语法树再转换成源代码存放到缓存中
      • 6-3 -> 4-8.在替换语法树期间可以进行判断,查看 AST语法树 中是否有 require/import 等字样,如果有则说明该模块是有依赖模块的,这时候将这些依赖模块的文件路径模块id记录下来,保存到自身的对象上
      • 6-3 -> 4-9.回到 6-3 -> 4-4 中提到的 _addModuleChain 方法中,这时候的新模块对象 module 已经被拿到了,判断 module 身上是否有上一步中记录的依赖模块,如果有则循环遍历这些依赖模块的信息,并从 6-3 -> 4-4 开始递归操作,创建所有的依赖模块自己的 module
      • 6-3 -> 4-10.判断该模块是否为入口文件,如果是则放入 compilation 对象的 _entries 成员变量中
      • 6-3 -> 4-11.当一个模块连同它的依赖模块都加载完毕后将这些模块都放入 compilation 对象身上的 modules 成员变量上
    • 6-4.这时候的 compilation 身上拥有了所有需要的属性( 文件路径、修改完毕的文件模块源码、修改完毕的AST语法树、依赖的模块信息、依赖的模块信息的修改完毕的文件模块源码和依赖的模块信息的修改完毕的AST语法树 ),调用 compilation 身上的 seal 方法进入 chunk 生成流程
  • 7.因为 chunk 的生成必然是有一个入口的,所以遍历 compilation 对象的 _entries 成员变量,拿到所有的入口文件信息,然后将入口文件模块对象当做参数实例化 Chunk 生成 chunk 对象,并将其缓存到 compilation 身上。
    • 7-1.6-3 -> 4-11 中说过,所有生成的模块都被存放到了 modules 身上,所以只要遍历一下 modules 就能找到与当前 chunk 同一个 moduleId 的模块数组,这样然后将其放到该 chunk 对象的 modules 属性中,意味着当前 chunk 依赖这些模块
    • 7-2.得到完整的 chunk 信息后就可以开始创建 chunk 文件了,通过 ejs 语法将 chunk 身上的信息覆写到 webpack 打包完成的调用函数中,并且将覆写完以后的源码的文件名和源码以键值对的方式保存到 compilation 身上的 assets 成员变量上
  • 8.一层一层回调往回走会走到 6-4 seal 的执行回调中,在这个回调中会继续返回上一层,即 compiler 对象的 run 钩子的执行,这时候调用文件生成方法,将 compilation 身上的 assets 成员变量进行遍历,会得到当前模块所有覆写完以后的源码的文件名和源码
    • 8-1.因为在 1 中将 options 同样挂载到 compiler 身上的原因,就可以通过 compiler 对象身上 output.path 找到文件需要输出的路径
    • 8-2.拼接 output.path 和源码的文件名得到文件生成的路径,使用 fs 模块的 writeFileSync 方法传入文件生成的路径和覆写完以后的源码,等待文件生成完毕返回 err 和 入参为 compilation 的 实例化Stats 对象,这样就可以在文件执行完看到整个流程最后所使用到的所有moduleschunksentries信息
$ node run
// root/run.js
const webpack = require('./pack/lib/webpack')
const options = require('./webpack.config.js')
const compiler = webpack(options)
compiler.run((err, stats) => {
  console.log(err)
  console.log(stats)
})
// lib/temp/temp.ejs
(function (modules) {
  let installedModules = {};
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    let module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  __webpack_require__.o = function (obj, name) {
    return Object.prototype.hasOwnProperty(obj, name);
  }
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        enumerable: true,
        get: getter
      });
    }
  }
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== undefined && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, {
        value: 'Module'
      });
    }
    Object.defineProperty(exports, '__esModule', {
      value: true
    });
  }
  let installedChunks = {
    "main": 0
  }
  function webpackJsonpCallback(data) {
    const chunkIds = data[0];
    const moreModules = data[1];
    let resolves = [];
    for (let i = 0; i < chunkIds.length; i++) {
      const chunkId = chunkIds[i]; if (installedChunks[chunkId] &&
        Object.prototype.hasOwnProperty.call(installedChunks, chunkId)) { resolves.push(installedChunks[chunkId][0]); }
      installedChunks[chunkId] = 0;
    } for (let moduleId in moreModules) { modules[moduleId] = moreModules[moduleId]; } while
      (resolves.length) { resolves.shift()(); }
  } __webpack_require__.e = function (chunkId) {
    let promises = []; let
      installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0) {
        if (installedChunkData) {
          promises.push(installedChunkData[2]);
        } else {
          let promise = new Promise((resolve, reject) => {
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
          })
          promises.push(installedChunkData[2] = promise);
          let script = document.createElement('script');
          script.src = jsonpScriptSrc(chunkId);
          document.head.appendChild(script);
        }
      }
    return Promise.all(promises);
  }
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + '' + chunkId + '.main.js';
  }
  let jsonpArray = window['webpackJsonp'] = window['webpackJsonp'] = [];
  let originJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  let parentJsonpFunction = originJsonpFunction;
  jsonpArray.forEach(jsonp => {
    webpackJsonpCallback(jsonp);
  })
  __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;
    let namespace = Object.create(null);
    Object.defineProperty(namespace, 'default', {
      enumerable: true,
      value
    });
    if (mode & 2 && typeof value !== 'string') {
      for (let key in value) {
        __webpack_require__.d(namespace, key, (key => value[key]).bind(null, key));
      }
    }
    return namespace;
  }
  __webpack_require__.p = '';
  return __webpack_require__(__webpack_require__.s = '<%-entryModuleId%>');
})
  ({
  <%for (let module of modules) {%>
    "<%-module.moduleId%>":
    (function (module, exports, __webpack_require__) {
      <%-module._source %>
    }),
  <%}%>
  })
// lib/webpack.js
const Compiler = require("./Compiler")
const WebpackOptionsApply = require("./WebpackOptionsApply")
const NodeEnviromentPlugin = require("./node/NodeEnviromentPlugin")

const webpack = options => {
  // 实例化 Compiler 对象
  let compiler = new Compiler(options.context)
  compiler.options = options
  // 初始化 NodeEnviromentPlugin 让compiler对象具有可读写能力
  new NodeEnviromentPlugin().apply(compiler)
  // 挂载所有plugins至compiler对象身上
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler)
    }
  }
  // 挂载所有 webpack 内置插件
  // compiler.options = new WebpackOptionsApply().process(options, compiler)
  new WebpackOptionsApply().process(options, compiler)
  return compiler
}

module.exports = webpack
// lib/node/NodeEnviromentPlugin.js
const fs = require('fs')

module.exports = class {
  constructor(options) {
    this.options = options || {}
  }
  apply(compiler) {
    compiler.inputFileSystem = fs
    compiler.outputFileSystem = fs
    return compiler
  }
}
// lib/WebpackOptionsApply.js
const EntryOptionPlugin = require("./EntryOptionPlugin")

module.exports = class {
  process(options, compiler) {
    new EntryOptionPlugin().apply(compiler)
    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}
// lib/Compiler.js
const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')
const path = require('path')
const mkdirp = require('mkdirp')
const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')

module.exports = class extends Tapable {
  constructor(context) {
    super()
    this.context = context
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),
      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),
      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),
      emit: new AsyncParallelHook(['compilation'])
    }
  }
  run(callback) {
    console.log('run 执行')
    const finalCallback = (err, stats) => {
      callback(err, stats)
    }
    const onCompiled = (err, compilation) => {
      console.log('onCompiled')
      finalCallback(err, new Stats(compilation))
      // 将处理好的 chunk 写入到指定文件
      this.emitAssets(compilation, err => {
        finalCallback(err, new Stats(compilation))
      })
    }
    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        this.compile(onCompiled)
      })
    })
  }
  emitAssets(compilation, callback) {
    // 创建 dist 目录
    // 定义一个工具方法用于执行文件的生成
    const emitFiles = err => {
      const assets = compilation.assets
      const outputPath = this.options.output.path
      for (let file in assets) {
        let source = assets[file]
        let targetPath = path.posix.join(outputPath, file)
        this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
      }
      callback(err)
    }
    // 创建目录之后启动文件写入
    this.hooks.emit.callAsync(compilation, err => {
      console.log(this.options);
      mkdirp.sync(this.options.output.path)
      emitFiles(err)
    })
  }
  newCompilationParams() {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params
  }
  newCompilation(params) {
    const compilation = this.createCompilation(params)
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }
  createCompilation() {
    return new Compilation(this)
  }
  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params)
      const compilation = this.newCompilation(params)
      this.hooks.make.callAsync(compilation, err => {
        console.log('make 钩子触发')
        compilation.seal(err => {
          this.hooks.afterCompile.callAsync(compilation, err => {
            callback(err, compilation)
          })
        })
      })
    })
  }
}
// lib/Compilation.js
const path = require('path')
const ejs = require('ejs')
const async = require('neo-async')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser')
const Chunk = require('./Chunk')
const { SyncHook, Tapable } = require('tapable')
const parser = new Parser()

module.exports = class extends Tapable {
  constructor(compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    // 让 compilation 具备文件读写能力
    this.inputFileSystem = compiler.inputFileSystem
    this.outputFileSystem = compiler.outputFileSystem
    this._entries = []
    this.modules = []
    this.chunks = []
    this.assets = []
    this.files = []
    this.hooks = {
      successedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook(),
    }
  }
  addEntry(context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module)
    })
  }
  _addModuleChain(context, entry, name, callback) {
    this.createModule({
      parser,
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, entry),
    }, entryModule => {
      this._entries.push(entryModule)
    }, callback)
  }
  createModule(data, doAddEntry, callback) {
    let module = new NormalModuleFactory().create(data)
    const afterBuild = (err, module) => {
      // 在 afterBuild 当中需要判断当前次 module 加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        // 当前逻辑表示 module 有需要加载的模块
        this.processDependencies(module, err => {
          callback(err, module)
        })
      } else {
        callback(err, module)
      }
    }
    this.buildModule(module, afterBuild)
    // 完成 build 之后将 module 保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }
  buildModule(module, callback) {
    module.build(this, err => {
      // 当前 module 完成编译
      this.hooks.successedModule.call(module)
      callback(err, module)
    })
  }
  processDependencies(module, callback) {
    // 实现一个依赖模块的递归加载
    // 由于不知道有几个依赖模块,所以使用 neo-async 实现,当模块都加载完毕再调用 callback
    let dependencies = module.dependencies
    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done)
    }, callback)
  }
  seal(callback) {
    this.hooks.seal.call()
    this.hooks.beforeChunks.call()
    // 所有的入口模块都被存放在了 _entries 中
    for (const entryModule of this._entries) {
      // 创建模块加载已有模块的内容,同时记录模块信息
      const chunk = new Chunk(entryModule)
      this.chunks.push(chunk)
      chunk.modules = this.modules.filter(module => module.name === chunk.name)
    }
    // chunk 流程梳理之后就进入到 chunk 代码处理环节(模板文件 + 模块源代码 -> chunk.js)
    this.hooks.afterChunks.call(this.chunks)
    // 生成代码内容
    this.createChunkAssets()
    callback()
  }
  createChunkAssets() {
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i]
      const fileName = chunk.name + '.js'
      chunk.files.push(fileName)
      // 生成具体chunk内容
      // 获取模板文件路径
      let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
      // 获取模板源码
      let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
      // 获取渲染函数
      let tempRender = ejs.compile(tempCode)
      // 按ejs的语法渲染
      let source = tempRender({
        entryModuleId: chunk.entryModule.moduleId,
        modules: chunk.modules
      })
      // 输出文件
      this.emitAssets(fileName, source)
    }
  }
  emitAssets(fileName, source) {
    this.assets[fileName] = source
    this.files.push(fileName)
  }
}
// lib/EntryOptionPlugin.js
const SingleEntryPlugin = require("./SingleEntryPlugin")

const itemToPlugin = (context, entry, name) => {
  return new SingleEntryPlugin(context, entry, name)
}

module.exports = class {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      itemToPlugin(context, entry, 'main').apply(compiler)
    })
  }
}
// lib/SingleEntryPlugin.js
module.exports = class {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }
  apply(compiler) {
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const {
        context,
        entry,
        name
      } = this
      console.log('make 钩子监听')
      compilation.addEntry(context, entry, name, callback)
    })
  }
}
// lib/NormalModuleFactory.js
const NormalModule = require('./NormalModule')

module.exports = class {
  create(data) {
    return new NormalModule(data)
  }
}
// lib/Normal.js
const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default

module.exports = class {
  constructor(data) {
    this.name = data.name
    this.context = data.context
    this.rawRequest = data.rawRequest
    this.resource = data.resource
    this.moduleId = data.moduleId
    this.parser = data.parser
    this._source // 存放某个模块源代码
    this._ast // 存放某个模块语法树
    this.dependencies = [] // 定义一个空数组用于保存被依赖加载的模块信息
  }
  build(compilation, callback) {
    // 从文件中读取到将来需要被加载的 module 内容
    // 如果当前文件不是js,则需要使用 loader 处理,并返回js
    // 上述操作完成后就可以将js代码转为ast语法树
    // 当前js模块内部可能又引用了其他模块,所以需要递归
    this.doBuild(compilation, err => {
      this._ast = this.parser.parse(this._source)
      traverse(this._ast, {
        CallExpression: (nodePath) => {
          let node = nodePath.node
          if (node.callee.name === 'require') {
            // 获取原始请求路径
            let modulePath = node.arguments[0].value // './header'
            // 取出当前被加载的模块名称
            let moduleName = modulePath.split(path.posix.sep).pop()
            let extName = moduleName.indexOf('.') === -1 ? '.js' : ''
            moduleName += extName // header.js
            let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
            // 定于当前模块的id
            let depModuleId = './' + path.posix.relative(this.context, depResource) // ./src/header.js
            // 记录当前被依赖模块的信息,方便后面递归加载
            this.dependencies.push({
              name: this.name,
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            })
            // 替换内容
            node.callee.name = '__webpack_require__'
            node.arguments = [types.stringLiteral(depModuleId)]
          }
        }
      })
      let { code } = generator(this._ast)
      this._source = code
      callback(err)
    })
  }
  doBuild(compilation, callback) {
    this.getSource(compilation, (err, source) => {
      this._source = source
      callback(err)
    })
  }
  getSource(compilation, callback) {
    compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)
  }
}
// lib/Parser.js
const { Tapable } = require('tapable')
const babylon = require('babylon')

module.exports = class extends Tapable {
  parse(source) {
    return babylon.parse(source, {
      sourceType: 'module',
      plugins: ['dynamicImport']
    })
  }
}
// lib/Chunk.js
module.exports = class {
  constructor(entryModule) {
    this.entryModule = entryModule
    this.name = entryModule.name
    this.files = [] // 存放文件名
    this.modules = [] // 记录 chunk 包含的 module
  }
}
// lib/Stats
module.exports = class {
  constructor(compilation) {
    this.entries = compilation._entries
    this.modules = compilation.modules
    this.chunks = compilation.chunks
  }
  toJson() {
    return this
  }
}

你可能感兴趣的:(不当切图仔,webpack,前端,javascript,node.js)