I’ve been working on the webpack recently, so here’s a quick note.
The module
As a newbie, I probably knew webPack was a Bundler that solved the problem of how to bundle multiple modules together. The module definition here is very extensive, can be a JS file, can be a picture, can be a style file… Of course, because we are front-end developers, the most understanding of the concept of modules, should be JS files.
In the normal development process, we usually treat a JS file as a Module, for Module specification connected to ES Module, probably can give such an example:
/ / the import
import message from './message.js';
import {word} from './word.js'
import a from './a.js'
/ / export export
export const a = 1;
export default.export function.Copy the code
I think the important thing to understand here is:
- We can get through
import
orexport
To introduce or export something. - We will be
import
What comes in is assigned to a variable inside the current module. - we
export
The thing that goes out can be used as an import for other module files, and we can call the thing that goes outexports
.
I can probably draw a picture:
The import statement is used to get exports from other module files. We also know that the browser will not recognize the import syntax without the module attribute attached to the
Why does Webpack pack our code by the entry file, packaged JS file hanging on the HTML file can run it?
Based on the above behavior, we can basically think of:
- for
import
We can abstract out a functionrequire(path)
To get the JS file exported under pathexports
Object. - for
export
The behavior of, we simply understand that it is calledexports
Object assignment, as for thisexports
Where did the object come from? Let’s leave it for a moment.
That’s where THE concept came from. I found a handwritten Bundler code I’d copied from somewhere.
Hand write a Bundler
The code is here. Please do ignore my grassy doghouse. Please refer to the code for the following information.
First let’s look at the overall process.
First of all, we should convert the syntax of all files, at least not import and export. Instead, we should convert the syntax of the require function to fetch the exports of another module, so that we can generate transformed code for each file.
Then, the question arises, how do we start from the entry file and find all the relevant files for this packaging?
Module analysis
Let’s start with the analysis of the entry file.
We use fs module to read the contents of the entry file according to the path, and use @babel/ Parser to convert it into abstract syntax tree AST.
All we need to know is that the AST is a tree structure, and each import statement corresponds to a node of type ImportDeclaration in the tree, which contains some meta information about the corresponding import statement, such as the path of the dependent module following from.
We traversed the AST with @babel/traverse. For each ImportDeclaration node, we did some path mapping with respect to the entry file and placed the mapping in a Dependencies object.
Finally, we used @babel/core in combination with @babel/preset-env to convert the AST to the desired syntax we said earlier, that is, the format without import and export syntax.
We can look at the current entry file index.js:
import message from './message.js';
console.log(message);
export const a = 1;
Copy the code
We can get something like this:
const result = {
filename: './src/index.js'.dependencies: { './message.js': 'src/message.js' },
code:
'"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \n exports.a = void 0; \n\nvar _message = _interopRequireDefault(require("./message.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_message["default"]); \nvar a = 1; \nexports.a = a; '};Copy the code
-
The import syntax has become a require function, and the export syntax has become an assignment to an exports variable.
-
The end result is a Denpendencies object, in which the key is a path to the current file (in this case, the entry file) and the value is a path to our Bundler.
Dependency graph generation
We get the dependencies map of the entry file, so we can analyze the dependencies again. In fact, it is breadth-first traversal, and we can easily obtain the analysis results of all the required modules.
const graph = {
'./src/index.js': {
dependencies: { './message.js': 'src/message.js' },
code:'... '
},
'src/message.js': {
dependencies: { './word.js': 'src/word.js' },
code:'... '
},
'src/word.js': {
dependencies: {},
code:'... '}};Copy the code
The generated code
We need to start generating the final runnable code.
In order not to pollute the global scope, we wrap our code with the execute now function, passing in the dependency graph as an argument:
(function(graph){
// todo
})(graph)
Copy the code
We need to start running the code for the entry file, so we have to find the corresponding code for the entry file in graph and run it:
(function(graph){
function require(module){
eval(graph[module].code)
}
require('./src/index.js')
})(graph)
Copy the code
However, in code, we also need to call require to get the exports of other modules, so the require function must have exports. It also supports internal calls to the require function, but beware!! The require function is not declared as the require function, because we can see from the compiled code that in code the require function passes the path relative to the current Module. At this point, the dependencies map we stored for each module comes in handy again.
(function(graph){
function require(module){
// define code internal use of require function -> localRequire
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
eval(graph[module].code)
return exports;
}
require('./src/index.js')
})(graph)
Copy the code
To override the require variable in the current scope chain, we immediately execute functions around eval, passing localRequire, exports, and code as arguments. This also ensures that the names of the code-related functions in Eval correspond.
(function(graph){
function require(module){
// define code internal use of require function -> localRequire
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require.exports, code){
eval(code);
})(localRequire, exports, graph[module].code)
return exports;
}
require('./src/index.js')
})(graph);
Copy the code
Bundler is written, and the resulting code runs directly in the browser.
Still, I was curious to see what the webPack would look like when packaged.
Webpack results analysis
We’ll use the same file, do a package with WebPack, and do some analysis of the results (since the code is quite long, I recommend doing it yourself).
Start with the outermost layer:
(function (modules) {
// ...({})'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
eval(
// ...
);
},
'./src/message.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
eval(
'__webpack_require__.r(__webpack_exports__); \n/* harmony import */ var _word_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./word.js */ "./src/word.js"); \n\n\nconst message = `say ${_word_js__WEBPACK_IMPORTED_MODULE_0__["word"]}`; \n\n/* harmony default export */ __webpack_exports__["default"] = (message); \n\n\n//# sourceURL=webpack:///./src/message.js? '
);
},
'./src/word.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
eval(
// ...); }});Copy the code
According to our close inspection, we found that Webpack also converted import and export. The above mentioned require function and exports object inside code became __webpack_require__ and __webpack_exports__.
More smartly, the __webpack_require__ parameter in each module’s code is now converted to the bundler path instead of the previous path relative to the module.
In addition, the value for each key in the graph becomes a function, much like the immediate function we wrote outside the eval code.
Step two, go inside and analyze the core logic:
// The module cache
var installedModules = {}
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
}
// ...
return __webpack_require__(__webpack_require__.s = './src/index.js')
/ /...
Copy the code
The result is to call the declared __webpack_require__ function with the entry in webpack.config.js as the moduleId.
The __webpack_require__ function internally preferentially returns the exports object found in the cache. Module. exports = module.exports = module.exports = module.exports = module.exports = module.exports = module.exports = module.
There is a detail here, although we bound module.exports to call, we actually used this in the outermost layer of our module, which was converted to undefined when we coded graph.
I feel I have a rough understanding of Webpack.