Realize the function

  1. supportesModule
  2. supportimport()Asynchronously loading files
  3. supportloader

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 referencewebpackThe output of thebundleYou Gotta Love FrontendIn 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/…

The public,