preface
Usually out of habit, see the website like to open debugging others, learn how others are to write code. But modern packaging tools tend to compress and optimize code. So how to debug webpack as a representative of this kind of website, to see how others home code is written out of logic? And explore the logical flow of the whole site, is the topic of our article today.
This article will be longer, and I will do a lot of preamping (which I think is important). If you are good at basics and need to know the key debugging methods, please skip to the end of the article “Front-end Debugging Ideas”.
Webpack is a static module packaging tool for modern JavaScript applications. When WebPack works with an application, it internally builds a dependency graph that maps to each module required for the project and generates one or more bundles.
This is the official introduction to WebPack. Here we come up with several pieces of information:
- Essentially a tool for packaging code, no magic.
- The dependency graph is the key, keeping track of the relationships between all the modules.
- It generates one or more code “packages.”
Start with a single entry
There are many modes of packaging. Each mode is packaged differently. But about the same, mainly for the single entrance not unpacking situation!
Here are the tool versions used:
- Webpack v5.26.3
- Webpack – cli v4.5.0
A profound
Let’s warm up with a little bit of code.
//index.js
console.log('This is a test script! Used to analyze webpack packed code. ');
Copy the code
When packaged using Webpack –mode=development mode, the following code is generated (I removed the unnecessary comments) :
//bundle.js
(() = > {
var __webpack_modules__ = ({
"./src/index.js": (() = > {
eval("Console. log(' This is a test script! Used to analyze webpack packed code. ');"); })});var __webpack_exports__ = {};// Ignore it for now.
__webpack_modules__["./src/index.js"] (); }) ();Copy the code
The code is relatively simple, let’s analyze it first:
- A self-executing anonymous function runs on the outside, launching the entire code block.
- Defines a
__webpack_modules__
Variable, which stores a key object, the key is the relative path to the file, and it’s an anonymous function, and it calls the code that we wrote with eval. - It calls the entry file name (key value) that we saved, and the whole thing runs.
The official start of the
Add complexity by creating three new files: a.js b.js c.js and import them with index.js!
//index.js
import { a } from './a.js';
import { b } from './b.js';
import { c } from './c.js';
var obj = { a, b, c }
function main() {
obj.a()
obj.b()
obj.c()
}
main();
Copy the code
//a.js
function a() {
console.log('a');
}
export { a }
Copy the code
//b.js
function b() {
console.log('b');
}
export { b }
Copy the code
//c.js
function c() {
console.log('c');
}
export { c }
Copy the code
Once these files are ready, we try to package them and also remove the uncommented code.
The amount of code this time may surprise you, but don’t worry, let’s walk through the steps. I first wrote down the Chinese comments for easy reference (the first reading of the code here takes about 10 minutes to understand).
//bundle.js
(() = > { // The entire file is a self-executing function
"use strict";
var __webpack_modules__ = ({
"./src/a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
eval("__webpack_require__.r(__webpack_exports__); \n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"a\": () => (/* binding */ a)\n/* harmony export */ }); \nfunction a() {\r\n console.log('a'); \r\n}\r\n\r\n\n\n//# sourceURL=webpack://test/./src/a.js?"); }),...As mentioned above, this variable stores the packaged key and the corresponding code. * /
});
var __webpack_module_cache__ = {};
/ SRC /index.js":{* exports: exposed variables, * id: module ID * loaded: loaded or not loaded *} *} */
function __webpack_require__(moduleId) {// key function!! This generates a load function for us!
/** * Start with the basics: scope Here __webpack_module_cache__ is defined outside the function, so it is equivalent to a global variable inside the entire self-executing function. * Check if the module has been cached (loaded before). If so, there is no need to handle it because __webpack_module_cache__ has exports property to hold the read. * /
var cachedModule = __webpack_module_cache__[moduleId];
if(cachedModule ! = =undefined) {
/** ** ** ** ** ** ** ** ** ** Saving computing performance (which you can easily imagine). * 2. Prevent dependency loops (important). * Example: Module A is an entry, importing module B, which in turn imports module A. According to the ESM specification (see ruan Yifong ES6 tutorial), the internal B module will print undefined. The reason is very simple: module A calls module B, A is not finished at the moment, module B accesses module A at the moment, the state is undefined. * We can see that the __webpack_require__ function, if it encounters such a loop-loaded dependency, infinitely nested calls, soon the javascript call stack will explode! When caching is used, results can be returned directly from the cache without calling __webpack_require__. Avoid the problem of stack burst! * /
return cachedModule.exports;// Return the exposed object if there is one.
}
// Create a module and cache it in __webpack_module_cache__ and assign it to the module variable
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}};/** * The first round executes the entry function and passes in three arguments: * module (the cache object that was just generated). Module. Exports (same object, exports used). * __webpack_require__ (important loading function, passed itself, this function will be executed and called repeatedly, notice!) . * /
__webpack_modules__[moduleId](module.module.exports, __webpack_require__);
// Exports is an exported object.
return module.exports;
}
/* webpack/runtime/define property getters */
(() = > {
/ / call something like this: __webpack_require__. D (__webpack_exports__, {" a ": (a) = > a});
__webpack_require__.d = (exports, definition) = > {
for (var key in definition) {
// Exports is exported from exports. // Exports is exported from exports.
if(__webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)) {
/** * Change the key when the condition is met. When the property is retrieved, the corresponding function is called. * Why do you do this? I guess it's to prevent the function from being deleted by mistake. After this step, the return value of key is defined twice. Because properties defined using Object.defineProperty() cannot be modified, enumerated, or deleted by default. * * /
Object.defineProperty(exports, key, { enumerable: true.get: definition[key] }); }}}; }) ();/** * As we all know, the in operator cannot tell if an attribute is in it because it will look up the scope chain until it finds it. HasOwnProperty = XX. HasOwnProperty = XX. HasOwnProperty = XX. HasOwnProperty = XX. * The code is easier to read because of the way the parameter is passed. * Give this method a short name to make it easier to use later. * /
(() = > {
__webpack_require__.o = (obj, prop) = > (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/make namespace object */
/** * Exports: exports: exports: exports: exports: exports: exports * __esModule attribute to true * when use Object on exports in the prototype. The toString, call () detected result is that the Module * /
(() = > {
// define __esModule on exports
__webpack_require__.r = (exports) = > {
if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
Symbol. ToStringTag = Symbol. ToStringTag = Symbol.
Object.defineProperty(exports.Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports.'__esModule', { value: true}); }; }) ();/** * this is where the call entry starts! * /
var __webpack_exports__ = __webpack_require__("./src/index.js"); }) ();Copy the code
aboutSymbol.toStringTag
extended
Object.defineProperty(exports.Symbol.toStringTag, { value: 'Module' });
Copy the code
The meaning of the code itself is that when the Object is carried out on it. The prototype. ToString. Call when judging (Object) type analysis, can be concluded that [Object Module] type. The reason is that the Symbol. ToStringTag attribute controls the resulting type.
Check out MDN: Symbol.toStringTag
Other syntax implementations of interception:
- Proxy
var a = {};
var aa = new Proxy(a,{
get () {
console.log(arguments); }})Object.prototype.toString.call( aa );
Copy the code
We are prompted to read the Symbol(symbol.toStringTag) property and conclude that the key is being accessed here.
var a = {};
var aa = new Proxy(a,{
get (obj,props) {
if(props === 'Symbol(Symbol.toStringTag)')console.log('xz');
return 'xz'; }})Object.prototype.toString.call( aa );// The modification succeeded
Copy the code
- Class
class ValidatorClass {
get [Symbol.toStringTag]() {
return "Validator"; }}Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"
Copy the code
Modification of operations on objects
There are three ways to modify the MDN:
- Object.defineProperty[ES5]
- Object.defineProperties[ES5]
- Reflect.defineProperty[ES6]
- Proxy[ES6]
The main differences between them are as follows:
proxy
Returns a new object, the first parameter being the object to be brokered, and the second parameter being an object in which interception operations are configured (very rich).Object.defineProperty
Modify the original object. The second parameter is an object in which the interception operation is configured. Attributes defined using Object.defineProperty() cannot be modified, enumerated, or deleted by default.Reflect.defineProperty
The first parameter is the modified object, and the second parameter is the one that needs to be modifiedkey
The third parameter is to configure the interception operation. The difference inObject
.defineProperty
Returns an object, or throws one if the property has not been successfully definedTypeError
. By contrast,Reflect.defineProperty
Method returns only oneBoolean
To indicate whether the property was successfully defined.Object.defineProperties
The first parameter is an object, and the second parameter is an object that can be configured differently for each key valueoptions
.
Why does WebPack need eval to generate code?
The eval API itself is not recommended, but webPack is now widely used.
Asked a lot of small friends, even asked to go down all temporarily speechless. First, my conclusion (which may not be absolutely correct) : to locate the Source map for easy debugging.
We’ll start with a random code block, add the SourceURL identifier, and when we print, the corresponding text will appear on the right side of the console.
//# sourceURL= test code
console.log(111)
Copy the code
In WebPack, because each module needs to be indexed, such as the __webpack_modules__ variable, each code block has a different sourceURL identifier built into eval to facilitate debugging. If you do not use the sourcemap function, the evel statement is no longer in the packaged code.
Multi-entry unpacking file analysis
In the case of multiple entry and unpacking, Webpack handles this:
- Put all dependencies into an array of global objects (similar to webpackXXX naming), and the code for the separate dependency package is like this, constantly pushing into the array of global objects.
- A single file entry similar to the previous analysis of the main code, has a main call started, adds them all, and then starts analyzing the code situation.
- Since the push method in the global object webpackXXX has been overwritten, it can magically sense some new dependencies. As new code content is pushed in, the WebPack-defined Require function starts the analysis process and executes the code until the end. Main entry file:
!function(e) {
function t(t) {// This is a push mode defined by WebPack itself, into which all code assets go when loaded
for (var n, l, a = t[0], f = t[1], i = t[2], c = 0, s = []; c < a.length; c++)
l = a[c],
Object.prototype.hasOwnProperty.call(o, l) && o[l] && s.push(o[l][0]),
o[l] = 0;
for (n in f)
Object.prototype.hasOwnProperty.call(f, n) && (e[n] = f[n]);
for (p && p(t); s.length; )
s.shift()();
return u.push.apply(u, i || []),
r()// This r function is used to perform code loading. This is where the main execution starts.
}
function r() {
for (var e, t = 0; t < u.length; t++) {
for (var r = u[t], n = !0, a = 1; a < r.length; a++) {
var f = r[a];
0! == o[f] && (n = !1)
}
n && (u.splice(t--, 1),
e = l(l.s = r[0]))// This is the official execution analysis, the initial entry function starts execution, after the assignment returns the result of the assignment begins to call the l function, which is our webpack Require function.
}
return e
}
var n = {}// Familiar webpack cache variables
, o = {
1: 0
}
, u = [];
function l(t) {// Start reiqure dependencies
if (n[t])
return n[t].exports;
var r = n[t] = {
i: t,
l:!1.exports: {}};return e[t].call(r.exports, r, r.exports, l),// Execute code! Pass in three parameters and proceed as single-file analysis!
r.l = !0,
r.exports
}
l.m = e,
l.c = n,
l.d = function(e, t, r) {
l.o(e, t) || Object.defineProperty(e, t, {
enumerable:!0.get: r
})
}
,
l.r = function(e) {
"undefined"! =typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
value: "Module"
}),
Object.defineProperty(e, "__esModule", {
value:!0
})
}
,
l.t = function(e, t) {
if (1 & t && (e = l(e)),
8 & t)
return e;
if (4 & t && "object"= =typeof e && e && e.__esModule)
return e;
var r = Object.create(null);
if (l.r(r),
Object.defineProperty(r, "default", {
enumerable:!0.value: e
}),
2 & t && "string"! =typeof e)
for (var n in e)
l.d(r, n, function(t) {
return e[t]
}
.bind(null, n));
return r
}
,
l.n = function(e) {
var t = e && e.__esModule ? function() {
return e.default
}
: function() {
return e
}
;
return l.d(t, "a", t),
t
}
,
l.o = function(e, t) {
return Object.prototype.hasOwnProperty.call(e, t)
}
,
l.p = ". /";
var a = this["webpackJsonpantd-demo"] = this["webpackJsonpantd-demo"] || []
, f = a.push.bind(a);
a.push = t,// The key operation push is changed!
a = a.slice();
for (var i = 0; i < a.length; i++)
t(a[i]);
var p = f;
r()
}([])
Copy the code
Other documents:
// The push method here is actually modified.
(this["webpackJsonpantd-demo"] = this["webpackJsonpantd-demo"] || []).push([[2], [function(e, t, n) {
"use strict";
e.exports = n(195)},function(e, t, n) {
"use strict";
n.d(t, "a", (function() {
returna } )); . omitCopy the code
Front-end Debugging
Here to start, first ask the self-test to understand the previous several questions:
-
Webpack_require_ is a function that you’re already familiar with, and probably recognizable no matter what it’s reduced to.
-
You already know how webpack’s subpacked code is executed in response.
-
You’ll break points with Chrome.
Here we start with the image above, first of all when you realize that this is a Webpack and finally a website that goes live. Try to see the variable that starts with Webpack in the console (the console will automatically prompt you if you type the word webpack first). Often these variables are the core, and the entire webpack package can be found here.
And as you can see, we’re almost there, first two arrays. There is no doubt that there are all the dependency functions, and the last push is the core response dependency function. As mentioned earlier, what this function does is it overwrites the response of the array’s own push method.
Follow along to the Source panel:
Boy, did you see all that stuff? Blue “◑ ◑) Blue “(. • get out of here. . I think it’s easy to see here, isn’t it?
The l function is clearly a compressed version of the key loader function webpack_require_. All modules must pass through it.
I believe that when you see this, you will also have doubts? It’s a hassle to have all these functions go through, and it’s a hassle to have conditional breakpoints, because I want to see different functions, and I have to go back and forth.
We define a global variable (denO) in the outer layer of the global function, and then assign the value to deno below the l function, so that we can use the L function directly through denO in the global.
Here are a few ways:
1. Use the Fiddler proxy to respond to the file and locally overwrite the L function.
2. Open the breakpoint and reference it directly from the console.
Then release the breakpoint.
Because my code blocks here are packed with numbers (other sites may not necessarily, but may also be strings as key values). I can easily debug and start a block of code from the console using deno (XXX). This is a similar structure to what we saw when the web developers were working on their projects, and in this way, we can learn from it. To debug the “troublesome code” that comes with more build tools.