从脚本男孩到工程师!
从脚本编写者到工程师
1995年webpack 复用,第一个JavaScript诞生了,用来在客户端做一些DOM的改变,减少用户的一些流量请求。 当时,HTML页面上的任何文本更改都需要从后台获取所有文件。 当然,最重要的是,在当时的带宽条件下,每一丝流量都极其罕见。
作为一种新生的脚本语言,它的地位较低,甚至连它的名字都是为了赶上Java的普及。 直到浏览器流行起来,成为你在线滑水的首选,JavaScript的地位也陡然上升。 随着网站量的不断增大,一个简单的JS文件可能无法满足大量复杂的需求。
无数开发者奋起,提出自己的新想法,开源自己的封装库,从原生JS到脚本库到模板句型再到框架,前端也从刀耕火种的耕种到了脚本语言到当今的组件化模块化工程。
一个前端开发人员从脚本师到工程师花了20年的时间。 在后端工程中,Rollup、Gulp、Webpack等打包工具是不可或缺的。 本系列将探讨最流行的后端打包解决方案 Webpack5。
Webpack5的模块化打包
模块化是前端工程最基本的部分。 源代码的划分和分层、组件的复用、项目模块的延迟加载都依赖于模块化的存在。
本文精心挑选Webpack最简单的模块化打包原理,带你第一步了解Webpack,帮助你从整个项目的角度理解后端工程!
PS:本文使用的Webpack配置如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
devtool: false, // 不要sourcemap
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename:'main.js',
},
plugins: [
new HtmlWebpackPlugin({
template:'./src/index.html'
})
]
}
CommonJS 规范
哪里有模块化,哪里就有模块化规范。 所谓规范就是大家约定好的代码句型,Webpack也会按照既定的句型来打包整个项目。 本质上就是对源代码进行正则匹配,然后替换,前端模块化规范有很多,这里只介绍最重要的两个:CommonJS规范和ESModule规范,其代表用户是NodeJS和ES6语法分别。
Webpack 对 CommonJS 规范的封装代码比较简单,其句型如该模拟文件所示。 模拟的文件结构如下:
// index.js 引用index2.js
let index2 = require('./index2.js')
console.log(index2)
// index2.js 导出一个字段
module.exports = 'index2'
那么Webpack是如何实现这样一个由两个模块组成的简单项目的打包呢? 说多了也没用,都在源码里了! 只有35行,可以自己做,打包后的源码全部注释:
// 打包结束后的main.js,自执行函数,script标记加载完后立即执行
(() => {
// key为路径,value为模块内容包裹成的函数
var __webpack_modules__ = ({
"./src/index2.js": ((module) => {
module.exports = 'index2'
})
});
// 如果缓存里有该模块,就使用缓存中的模块
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 创建一个新模块并将其放入缓存
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 执行模块时 传入三个参数, module module.exports 以及 require,
// 以供嵌套递归引用模块
__webpack_modules__[moduleId](module, module.exports,
__webpack_require__);
// 最终返回该模块的exports
return module.exports;
}
var __webpack_exports__ = {};
// 自执行函数,分割作用域,防止变量污染
(() => {
// 最终,入口文件的代码开始执行
let index2 = __webpack_require__("./src/index2.js")
console.log(index2)
})();
})()
解析:
当文件中存在 require() module.exports 等关键字时,Webpack 会将其识别为 CommonJS 模块规范。
打包后的main.js会在Webpack创建的index.html文件中添加标签,引用(() => {})()自执行函数,立即执行,起到划分模块作用域的作用,Webpack模块object 存放所有加载的模块,供模块之间互相require(),键是src的相对路径,值是模块内容转换成的函数。
Webpack require 替换文件中的 require(),接收密钥,从 Webpack 模块中读取运行的模块,并返回其导出。 上述规划完成后,执行入口文件代码,启动项目。 浏览器本身不支持模块化,通过函数来模拟模块化的功效。 对于node来说也是如此,尽管像requireexports这样的全局变量是函数的参数。
注意为什么(模块函数)有最里面的括号,函数后面直接取出来执行,而箭头函数需要这样写(()=>{})()才能自执行。
热身完毕,我们来看看最重要、最常用的ESModule——Webpack是如何打包的。
ES模块规格
ESModule规范的语法如下,也是这个仿真包的工程文件。
模拟文件结构:
// 入口文件index
import index2 from './index2'
console.log(index2)
// 被引用文件index2
const index2 = 'index2'
export default index2
话不多说,直接上源码,心急的同学可以先看后面的分析,再看源码,方便理解。
(为了Word排版,做了一定的折叠。)
打包后的全部源码带注释
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({
"./src/index2.js":
((__unused_webpack_module, __webpack_exports__,
__webpack_require__) => {
// 标记该模块为ESModule
__webpack_require__.r(__webpack_exports__);
// 通过d绑定要导出的数据到__webpack_exports__上
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
"test": () => (test)
});
// 模块内的代码执行
const __WEBPACK_DEFAULT_EXPORT__ = ('index2');
const test = 'test'
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 创建一个新模块并将其放入缓存
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 根据moduleId拿到模块,传入exports供挂载导出数据,执行该module
__webpack_modules__[moduleId](module, module.exports,
__webpack_require__);
return module.exports;
}
(() => {
// 绑定definition对象内的属性 到 exports上,即要导出的而数据
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
// definition: { "default": () => (...), "test": () => (test) }
// definition上有的属性,exports上没有的属性,就绑定上去
// 之后,外部模块使用如test属性时,实际上是调用`() => (test)`,
// 由于闭包原则,此时会拿到内部模块此test变量的最新值
// 这就是harmony exports
if (__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {
enumerable: true, get: definition[key]});
}
}
};
})();
(() => {
// 判断是否有某属性
__webpack_require__.o = (obj, prop) => (
Object.prototype.hasOwnProperty.call(obj, prop)
)
})();
(() => {
// 为该模块的export新增__esModule属性,
// 以供处理混合使用 ESModule 和 CommonJS 的情况
__webpack_require__.r = (exports) => {
// 如果浏览器支持 Symbol属性,就使用Symbol进行属性定义
// 注:Symbol可以在几乎所有框架内看到,用于作为独一无二的属性Key值
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
// 开始处理index模块,入口模块,整个程序开始运行
var __webpack_exports__ = {};
(() => {
// 标记其导出为ESModule
__webpack_require__.r(__webpack_exports__);
// 执行index2模块,拿到其exports
var _index2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/index2.js"
);
// 执行index主要代码
console.log(_index2__WEBPACK_IMPORTED_MODULE_0__["default"])
console.log(_index2__WEBPACK_IMPORTED_MODULE_0__.test)
})();
})()
可以看到与CommonJS规范 几乎相同 !!!
实现了 三个重要部分:
__webpack_modules__ 文件路径为key,包裹为函数的模块为value 的Map结构
__webpack_require__ 函数类型,实现导入功能(import)
__webpack_exports__ 对象类型,实现导出功能(export)
__webpack_require__,通过 key(文件路径) 从 Map__webpack_modules__
获取到 引用的 模块函数 并执行,执行时
传入__webpack_exports__对象 供 被引用的模块函数 导出数据
传入__webpack_require__函数 供 被引用的模块函数 递归调用 引用模块
三个工具函数:
__webpack_require__.o 判断某对象是否有某属性
__webpack_require__.r 将exports对象标记为ESModule
__webpack_require__.d 通过给exports对象设置getter属性,绑定要导出的数据
总结
不同的设置和不同的模块化规范会影响Webpack打包的代码,但这些都不会影响其模块化打包的核心逻辑。
实现导入:将Webpack的exports对象传递到模块函数中进行挂载,
实现导出:使用一个全局对象作为Map结构来存储所有模块,
Webpack的导入函数require手动检索Map下的模块并执行,
使用函数来模拟模块:只有当模块被引用时才执行模块代码,并隔离各个模块的作用域。
从入口模块开始执行,递归调用运行模块,启动整个项目。
最后,Webpack 将打包好的 main.js 打包为 Script 标签,并插入准备好的模板 HTML 文件。 试想webpack 复用,当用户访问网页时,nginx返回index.html,浏览器执行script标签时,向服务器请求main.js资源。 加载完成后,main.js开始执行,整个框架开始运行。 根据代码,在div中插入各种DOM结构,整个Web应用程序就这样运行了。
后记
Webpack为我们抹平了各种规范和书写风格的差异,让我们的编码更加简单,源码更加高贵。 本文讲解了比较简单、最基本、最可疑的模块化原理,而这只是 Webpack 学习帷幕的第一步。 很多源码都是这样的。 当你不明白的时候,你就会觉得深不可测。 经过搜索信息和研究,你会感受到通往简单的大道。
如果你特别喜欢我的Webpack系列,请点赞并支持。 未来可能会发布Webpack按需加载、热更新、AST、Tree-Shaking等原理的解释。
如果你更关心框架技术,欢迎在评论区留言,Vue和React的源码也可以拉下来和你聊聊。