前言
后记:由于webpack的知识体系太庞大了,而且webpack是一个很有深度的框架,所以我们拆分一下,这节课先来讲一下打包的基本原理和历史,后面会尽可能深的介绍:(2)原理(3)实战&webpack优化 (4) Tapable (5) tree-shaking (6) sourceMap (7) HMR
前端打包、构建、gulp、grunt、webpack、rollup、一堆名词,之前没有好好的系统性学习过,这次抽空系统的捋一捋。。。
可以说随着node的出现,前端变得越来越复杂,因为js代码不再是只能运行在浏览器里面的弱鸡语言,随之带来的是同样在服务器上运行的能力。我认为带来最大的利好就是前端项目也可以“工程化”了,就像C一样,具备了:预处理、编译、汇编、链接的能力。当然javascript是一门解释型语言,所以就没有后面三步了,前端打包多少类似于预处理+模块化的过程。
理解前端模块化
为啥要模块化:难道都写在main.js里面?如何复用?如何协同开发?
但是js不像其他
作用域
全局作用域、局部作用域
全局: window, global
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script src="./moduleC.js"></script>
</body>
</html>
// moduleA.js
var a = 1;
// moduleB.js
var a = 2;
// moduleC.js
var b = a + 1;
console.log('b = ', b);
结果:b = 3
显然被覆盖了。怎么办呢?
命名空间
//moduleA
var a = {
value: 1,
};
//moduleB
var b = {
value: 1,
};
//moduleC
console.log('moduleA的value', a.value);
console.log('moduleB的value', b.value);
结果:
moduleA的value 1
moduleB的value 2
看上去解决了上面的问题,但是随之而来的问题:
a.value = 100;
这样很容易就改变内部的一个变量了 所以我们需要利用作用域和闭包来改造一把
var moduleA = (function() {
var name = 'Nolan';
return {
myNameIs: function() {
console.log('请叫我', name);
},
};
})();
这是一个立即执行函数。
moduleA.myNameIs();
// 请叫我Nolan
moduleA.name;
// undefined
很明显暴露了该暴露的、隐藏了该隐藏的。
接下来我们再优化一下写法
(function(window) {
var name = 'Nolan';
function myNameIs() {
console.log('请叫我', name);
}
window.moduleA = { myNameIs };
})(window)
如果你撸过webpack打包后的代码,对比一下,是不是很像了?
总结一下
所以我们看到一个技术是循序渐进出来的,想想手写一个js继承是不是也是一步一步解决问题,遇到新的问题,再解决问题,最终产生的。
优点:
- 作用域封装
- 重用性
- 解除耦合
模块化
History
- AMD
- COMMONJS
- ES6 MODULE
AMD
define('moduleName', ['lodash'], function(_) {
return function(a, b) {
console.log(a, b);
};
});
比如 requireJS 后来衍生出了 玉伯大神的成名作 sea.js(CMD)
COMMONJS
2009年推出,主要为了规范服务端开发,并不是针对浏览器的规范。所以后来Nodejs也引用了此标准。
const moduleA = require('./moduleA');
exports.getSum = function(a, b) {
console.log(a + b);
}
与AMD相同,强调了引入的模块。
ES6 MODULE
与COMMONS很像
import moduleA from './moduleA';
export function getName(name) {
return name;
}
期间诞生了很多可以打包的工具:
Gulp,Grunt是自动化构建工具,这里要强调自动化是因为他们不仅可以做打包,自动化是其核心目的。
而webpack的出现可以说是专注于打包。
webpack
先来看一个小例子
首先我们先创建一个webpack-test的工程
下面包括 index.html、src/index.js和src/util.js。 npm安装 webpack以及webpack-cli工具
目前使用的是4.x.x版本。v5对tree-shake进行了性能优化,所以构建出的结果会有所不同。后面我们会介绍tree-shake是什么。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Webpack Test</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
index.js
const num = require('./util');
function test() {
console.log('我是一个小测试!', num);
}
test();
util.js
exports.default = 123;
接下来在根目录执行
npx webpack 或者 ./node_module/.bin/webpack
会生成dist/main.js的文件,打开文件我们看下结构
! function(e) {
var t = {};
function n(r) {
if (t[r]) return t[r].exports;
var o = t[r] = {
i: r,
l: !1,
exports: {}
};
return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
}
n.m = e, n.c = t, n.d = function(e, t, r) {
n.o(e, t) || Object.defineProperty(e, t, {
enumerable: !0,
get: r
})
}, n.r = function(e) {
"undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
value: "Module"
}), Object.defineProperty(e, "__esModule", {
value: !0
})
}, n.t = function(e, t) {
if (1 & t && (e = n(e)), 8 & t) return e;
if (4 & t && "object" == typeof e && e && e.__esModule) return e;
var r = Object.create(null);
if (n.r(r), Object.defineProperty(r, "default", {
enumerable: !0,
value: e
}), 2 & t && "string" != typeof e)
for (var o in e) n.d(r, o, function(t) {
return e[t]
}.bind(null, o));
return r
}, n.n = function(e) {
var t = e && e.__esModule ? function() {
return e.default
} : function() {
return e
};
return n.d(t, "a", t), t
}, n.o = function(e, t) {
return Object.prototype.hasOwnProperty.call(e, t)
}, n.p = "", n(n.s = 0)
}([function(e, t, n) {
const r = n(1);
console.log("我是一个小测试!", r)
}, function(e, t) {
t.default = 123
}]);
前面那一大坨我们先不管
简化就是(function(module){})([index.js, util.js])
看看结构发现是不是就是一个立即执行函数!所以说,高大上的webpack也只不过是通过前面提到的立即执行函数来实现的。那前面那一大堆不是人写的代码是什么呢?为啥会变成这个鸟样?我们接下来先让代码变得可读一些。
webpack --help
可以看到--mode
这样一个参数,development
和production
两个值。默认是production
,我们在运行npx webpack
的时候也可以看到这样的输出:
接下来我们执行一把看看吧~npx webpack --mode=development
(function(modules) { // webpackBootstrap
// 定义一个缓存
var installedModules = {};
// 可以称之为webpack运行在浏览器上的require方法,参数就是立即执行函数的参数中的key值
function __webpack_require__(moduleId) {
// 有缓存就返回缓存中的数据
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 开开心心放入缓存,注意这里定义了exports对象
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行函数
// 思考一下,我们在IDE里面疯狂无脑写着import/require/exports/export这些模块
// 跑到浏览器上运行的时候,浏览器哪儿知道这些玩意儿是干蛋的,但是浏览器知道啥?
// 知道对象、知道函数,所以我们把模块的导出存在了module.exports里面,module.exports在前面刚被初始化干干净净的被call
// 再遇到require不怕了,其实就是__webpack_require__这个方法嘛
// 所以require()的参数是模块的路径也就是立即执行函数参数中的key
// 而exports的就是个对象
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 不重要
module.l = true;
// require(一个文件路径), 这个文件exports的那些玩意儿
return module.exports;
}
// 为内置的require对象添加依赖模块
__webpack_require__.m = modules;
// 为内置的require对象添加缓存
__webpack_require__.c = installedModules;
// exports对象添加一个getter方法
__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.prototype.toString.call(exports)的时候返回的是Module
// 感觉就是看上去好看
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 说实话,我也不太清楚这个方法是干啥的
// 但是我翻了下github上有人提问,解释是:“ESM CJS interop. import("commonjs") will use it.”
// 地址粘贴在下方: https://github.com/webpack/webpack/issues/11024
__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;
};
// 包含属性否
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// 不重要
__webpack_require__.p = "";
// require入口文件吧
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
// 入口方法
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const num = __webpack_require__(/*! ./util */ \"./src/util.js\");\n\nfunction test() {\n console.log('我是一个小测试!', num);\n}\n\ntest();\n\n\n//# sourceURL=webpack:///./src/index.js?");
}),
// 被引入的方法
"./src/util.js": (function(module, exports) {
eval("exports.default = 123;\n\n//# sourceURL=webpack:///./src/util.js?");
})
});
这次变得好阅读一些了。我把函数的作用写在注释上。
那我们再尝试一个ES6 MODULE和CJS混用的
我们修改一下index.js
import { num } from './util';
function test() {
console.log('我是一个小测试!', num);
}
test();
还有util.js
exports.num = 123;
让我们再次打包看下结果,重复的部分我们就不说了,主要集中在不同上
__webpack_require__.n
和__webpack_require__.r
还有eval里面
(function (modules) { // webpackBootstrap
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;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
// 说白了就是以后在调用exports.a的时候用我们传进来的getter,这个例子里面也就是getDefault
// 如果你在index.js里面这么用的 import X from './util';
// console.log(X)的话,其实就是.a的getter。打完收工。
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter
});
}
};
// 这个Symbol.toStringTag理解为当我们Object.prototype.toString.call(exports)时,返回[object Module]类型
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
// 为exports对象加了一个属性,标明这个文件是ES6MODULE的哟~
// 回到下面eval
Object.defineProperty(exports, '__esModule', {
value: true
});
};
// 传入的exports对象是不是ES6Module啊?
// 如果是,我们是不是会export default X一个默认值,然后import X就完了
// 在es6 module中我们export default X其实是这样一个操作 default = X;
// 没错就是个赋值,所以我们这样是会报错的 export default const X;
// 转化成default = const X绝逼有问题对吧
// 所以这里我们就用getDefault方法默认帮你返回exports.default值了
// .d方法其实就是重写getter方法。
// 我们看下三个参数getter现在就是 getDefault()这个方法了,'a'是我们命名的一个参数名
// 我们去.d方法里面瞅一眼
__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 = "";
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// 由于我们在index.js中使用了import所以,webpack贴心的在eval前面插入了一行代码调用了r方法
// 我们去看看r干了啥
// 回来了继续看下
// 调用了__webpack_require__.n方法,传递的是import进来的文件中的exports对象
// 所以我们要去瞅一眼import的文件是谁,没错是下面的./src/util.js
// 这个文件没有用es6 module所以并没有像这个方法一样插入__webpack_require__.r方法
// 所以他的exports对象上并没有一个叫__esModule的属性,我们继续
// 去看下.n方法
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ \"./src/util.js\");\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_util__WEBPACK_IMPORTED_MODULE_0__);\n\n\nfunction test() {\n console.log('我是一个小测试!', _util__WEBPACK_IMPORTED_MODULE_0__[\"num\"]);\n}\n\ntest();\n\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/util.js": (function (module, exports) {
eval("exports.num = 123;\n\n//# sourceURL=webpack:///./src/util.js?");
})
});
完球了。。。这下清楚了吧~
聪明的你肯定知道webpack的按需加载,如果这时候让你去实现你会怎么做呢?有几种天然的分隔文件方式呢?多入口?import()
?import(/* prefetch 还有 preload*/)
了解下?文件分开了,根据分开的文件创建html<link as="script" src="...." rel="prefetch">
是不是就可以呢?
原理
叽叽喳喳半天,通过上面的例子,其实我们已经知道了webpack的产物,我们也分析了打包后的产物,那么从原始文件到构建后的文件,这其中又经历了什么呢?
未完待续
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!