Realize the function
- support
esModule
- support
import()
Asynchronously loading files - support
loader
The preparatory work
We need to parse with Babel, NPM init -y
npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D
Copy the code
Final file directory structure
| - dist / / packaging target folder | | - 0. Bundle. Js | | - 1 bundle. Js | | -- result. Js | | - SRC / / project test code | -- entry. Js | | - Messgae. Js | | - name. Js | | - a. s | | | - b.j s -- index. HTML / / load file package file | - app. Js / / boot file | -- init. Js / / Packaging is needed for the project initialization code | - Babel - plugin. Js / / Babel plugins | -- loader. Js / / loader | -- package. JsonCopy the code
File content entry.js
import message from "./message.js";
console.log(message);
import("./a.js").then((a)= > {
console.log("a done");
});
Copy the code
message.js
import { name } from "./name.js";
export default `hello ${name}! `;
import("./a.js").then((a)= > {
console.log("copy a done");
});
Copy the code
name.js
export const name = "world";
import("./b.js").then((a)= > {
console.log("b done");
});
Copy the code
a.js
console.log("import a");
setTimeout((a)= > {
document.body.style = "background:red;";
}, 3000);
Copy the code
b.js
console.log("import b");
Copy the code
write
As I wrote earlier in the WebPack series on analyzing the output files, the webPack code looks roughly like 👇
(function(modules) {
function __webpack_require__(moduleId) {... }... return __webpack_require__(__webpack_require__.s ="./src/main.js"); ({})"./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {}
"./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {}
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {}})Copy the code
Using his ideas, we can also quickly write a simple Webpack, first (function(modules) {… }) the internal code is basically writable, that is, init.js, which we will need to write later.
Warm up
Let’s use bable to compile the code and take a quick look at the 👇 example
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
let id = 0;
const resolve = function(filename) {
let content = "";
content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8");
/ / the ast tree
const ast = parser.parse(content, {
sourceType: "module"});/ / rely on
const dependencies = [];
traverse(ast, {
ImportDeclaration({ node }) {
// import '' from ''dependencies.push(node.source.value); }});// ES6 becomes ES5
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"]});return {
id: id++,
dependencies,
filename,
code,
};
};
const result = resolve("./src/entry.js");
console.log(result);
Copy the code
Print the result
{ id: 0, dependencies: [ './message.js' ], filename: './src/entry.js', code: '"use strict"; \n\nvar _message = _interopRequireDefault(require( ....." }Copy the code
ImportDeclaration intercepts import, adds it to dependencies dependency, converts import to ES5, and outputs the object. Contains the current file ID, dependencies, filename, and compiled source code. This code is the essence of the whole article, but now we are only dealing with one file, we just found the dependencies of the current file, then need to recursively find the dependencies of the next file, and finally combine them, similar to the idea of the files we saw in the webPack output.
Recursively find all dependencies
Add the following code below 👇 and drop the last two lines const result = resolve(“./ SRC /entry.js”); console.log(result);
const start = function(filename) {
const entry = resolve(filename);
const queue = [entry];
for (const asset of queue) {
const dependencies = asset.dependencies;
const dirname = path.dirname(asset.filename);
asset.mapping = {};
dependencies.forEach((val) = > {
const result = resolve(path.join(dirname, val));
asset.mapping[val] = result.id;
queue.push(result);
});
}
return queue;
};
const fileDependenceList = start("./src/entry.js");
console.log(fileDependenceList);
Copy the code
Js import 👉 message.js, message.js import 👉 name.js, name.js There is no import file so the dependency is empty
[{id: 0.dependencies: [ './message.js'].filename: './src/entry.js'.code: '"use strict"; \n\nvar _message = _interopRequireDefault(require( ....." '
},
{
id: 1.dependencies: [ './name.js'].filename: 'src/message.js'.code: '"..." '
},
{
id: 2.dependencies: [].filename: 'src/name.js'.code: '"..." '},]Copy the code
As a result, we have, so far, not the structure we wanted, so add the following code
let moduleStr = "";
fileDependenceList.forEach((value) = > {
moduleStr += `${value.id}:[
function(require, module, exports) {
${value.code};
},
The ${JSON.stringify(value.mapping)}], `;
});
const result = ` (${fs.readFileSync("./init.js"."utf-8")}) ({${moduleStr}}) `;
fs.writeFileSync("./dist/result.js", result); // Note that there needs to be a dist folder
Copy the code
This introduces init.js, as follows
function init(modules) {
function require(id) {
var [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
var module = { exports: {}}; fn(localRequire,module.module.exports);
return module.exports;
}
// Execute the entry file,
return require(0);
}
Copy the code
After execution there is a result file under dist/, which we put in the browser to execute and load in index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>webpack</title>
</head>
<body>
<script src="./dist/result.js"></script>
</body>
</html>
Copy the code
Sure enough, the console says Hello World, followed by three errors. That’s right, because we didn’t deal with import().then(). This needs to be handled separately, so if you want to get rid of the error, go to the SRC folder and comment out import().
If you look at the code content of result, you will see that we first execute require(0), trigger from the entry, and then recursively call require to complete the process. If you look at the code output from moduleStr, the structure is a little different from the webpack input
{
0: [
function(require, module, exports) {
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message["default"]);
},
{ "./message.js": 1},].1: [function(require, module, exports) {... }, {"./name.js": 2}].2: [function(require, module, exports) {... }, {},}Copy the code
{“./message.js”: {“./message.js”: {“./message.js”: 1} to locate the id of the mapping, the id is 1, and the following is the logic of the mapping: find the id of the mapping by filename filename.
var [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
Copy the code
Support asynchronous import() loading
We need to create a file like 0.bundle.js 1.bundle.js and push it into the page head via document.createElement(“script”). Modify the Babel section
.+ let bundleId = 0;
+ const installedChunks = {};
const resolve = function(filename) {
let content = "";
content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8");
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = [];
traverse(ast, {
ImportDeclaration({ node }) {
// import '' from ''
dependencies.push(node.source.value);
},
+ CallExpression({ node }) {
+ // import()
+ if (node.callee.type === "Import") {
+ const realPath = path.join(
+ path.dirname(filename),
+ node.arguments[0].value
+);
+ if (installedChunks[realPath] ! == undefined) return;
+ let sourse = fs.readFileSync(realPath, "utf-8");
+ / / es5
+ const { code } = babel.transform(sourse, {
+ presets: ["@babel/preset-env"]
+});
+ sourse = `jsonp.load([${bundleId}, function(){${code}}])`;
+ fs.writeFileSync(`./dist/${bundleId}.bundle.js`, sourse);
+ installedChunks[realPath] = bundleId;
+ bundleId++;
+ process.installedChunks = {
+ nowPath: path.dirname(filename),
+... installedChunks,
+};
+}
+},}); / / ES6 into ES5 const. {code} = Babel transformFromAstSync (ast, null, {+ plugins: ["./babel-plugin.js"],presets: ["@babel/preset-env"], }); return { id: id++, dependencies, filename, code, }; }; .Copy the code
Babel-plugins: [“./babel-plugin.js”]. If you don’t understand, please refer to babel-handbook
babel-plugin.js
const nodePath = require("path");
module.exports = function({ types: t }) {
return {
visitor: {
CallExpression(path) {
if (path.node.callee.type === "Import") {
path.replaceWith(
t.callExpression(
t.memberExpression(
t.identifier("require"),
t.identifier("import")
),
[
t.numericLiteral(
process.installedChunks[
nodePath.join(
process.installedChunks["nowPath"],
path.node.arguments[0].value ) ] ), ] ) ); ,}}}}; };Copy the code
Import (‘./a.js’) to require.import(0).
Modify init.js, mainly add import method, borrowed from Webpack
function init(modules) {
function require(id) {
var [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
var module = { exports: {}}; localRequire.import =require.import; / / new
fn(localRequire, module.module.exports);
return module.exports;
}
var installedChunks = {}; // It is currently added
require.import = function(chunkId) { // It is currently added
var promises = [];
var installedChunkData = installedChunks[chunkId];
// If not loaded
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));
// start chunk loading
var script = document.createElement("script");
var onScriptComplete;
script.charset = "utf-8";
script.src = "dist/" + chunkId + ".bundle.js";
var error = new Error(a); onScriptComplete =function(event) {
// avoid mem leaks in IE.
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);
};
window.jsonp = {}; // It is currently added
jsonp.load = function(bundle) { // It is currently added
var chunkId = bundle[0];
var fn = bundle[1];
var resolve = installedChunks[chunkId][0];
installedChunks[chunkId] = 0;
// Execute asynchronous load file code
fn();
/ / resolve execution
resolve();
};
// Execute the entry file,
return require(0);
}
Copy the code
Our asynchronously loaded files execute the jsonp.load method, and we modify the code to get the following structure before generating the *.bunnd.js file, which controls the execution of the source code and.then().catch() operations
jsonp.load([
0.function() {
// The original file code},]);Copy the code
Dist (0. Bundle.js, 1. Bundle.js). If you don’t comment the code you wrote before import(), then go to the browser console and print the following files, and 3 seconds later the background of the page turns red
hello world!
import b
b done
import a
copy a done
a done
Copy the code
Wait, we used three import files, why only two files, because one import(‘./a.js’) was used twice, here I cache, so files imported asynchronously repeatedly will be cached
Support the loader
Loader. js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js: loader.js
module.exports = function(content) {
return content + "; console.log('loader')";
};
Copy the code
Add the code to print loader after each JS file
Next, modify the code inside the resolve method
+ const loader = require("./loader");
const resolve = function(filename) {
let content = "";
content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8");
+ content = loader(content);const ast = parser.parse(content, { sourceType: "module", }); . }Copy the code
Then run the code and the browser console will print three Loaders
The last
So far, we have completed the esModule support, file asynchronous loading support, loader support, we also incidentally wrote a Babel plug-in, the whole process is not difficult to understand the place, a Webpack is completed in this way, of course, can also improve the function. Support plug-ins? Add Tapable? And so on, time is limited, so far, if there is a mistake also hope to correct
This chapter code part referencewebpack
The output of thebundle
及 You Gotta Love Frontend
In the videoRonen Amiel – Build Your Own Webpack
The code has been uploaded to GitHub: github.com/wclimb/my-w…
This paper addresses www.wclimb.site/2020/04/22/…