Реализация простого веб-пакета от 0 до 1

внешний интерфейс Webpack

Введение

Основные функции этой простой версии веб-пакета следующие:

  • Загружать блоки кода асинхронно
  • Извлечение общедоступного блока кода (Commons) / сторонней библиотеки (Vendors)
  • компиляция загрузчика

2. Рабочий процесс веб-пакета

Работающий процесс Webpack является последовательным процессом, и следующие процессы выполняются последовательно от начала до конца:

  • Параметры инициализации:Чтение и объединение параметров из файлов конфигурации и операторов оболочки для получения окончательных параметров;
  • Начать компиляцию:Инициализируйте объект Compiler с параметрами, полученными на предыдущем шаге, загрузите все настроенные плагины и выполните метод запуска объекта, чтобы начать компиляцию;
  • Определяем вход:Найти все входные файлы по записи в конфигурации;
  • Скомпилируйте модуль:Начиная с файла записи, вызовите все настроенные загрузчики для компиляции модуля, затем найдите модули, от которых зависит модуль, а затем повторите этот шаг, пока все файлы, от которых зависит запись, не будут обработаны на этом шаге;
  • Завершите компиляцию модуля:После компиляции всех модулей с помощью Loader получается финальное содержимое каждого модуля после компиляции и зависимости между ними;
  • Выходной ресурс:В соответствии с зависимостями между записью и модулем, собрать их в чанки, содержащие несколько модулей, а затем преобразовать каждый чанк в отдельный файл и добавить его в список вывода.Этот шаг — последний шанс изменить содержимое вывода;
  • Вывод сделан:После определения выходного содержимого определите выходной путь и имя файла в соответствии с конфигурацией и запишите содержимое файла в файловую систему.
  • В приведенном выше процессе Webpack будет транслировать определенное событие в определенный момент времени, подключаемый модуль будет выполнять определенную логику после прослушивания интересующего события, а подключаемый модуль может вызывать API, предоставленный Webpack, для изменения текущего результат вебпака.


3. Tapable из Webpack

  • Webpack по сути представляет собой механизм потока событий. Его рабочий процесс заключается в последовательном подключении различных подключаемых модулей, и ядром всего этого является Tapable. Основной компилятор в Webpack и компиляция, отвечающие за создание пакетов, Tapable.
  • Внутри Webpack есть различные хуки, и плагины регистрируют свои собственные методы на соответствующих хуках, так что при компиляции Webpack эти хуки будут срабатывать, поэтому будет срабатывать метод плагина.

1. Настраиваемая классификация

  • Tapable предоставляет множество типов хуков, которые делятся на две категории: синхронные (Sync) и асинхронные (Async).


Типы

Как сказать

Использовать баллы
Basic

Хук не содержит следующие три типа ключевых слов

Не важно, имеет ли функция слушателя возвращаемое значение
Bail

Залог включен в крюк

Безопасно: пока в функции прослушивателя есть возвращаемое значение (не undefined ), последующая функция прослушивателя будет пропущена.
Waterfall

Водопад в крюке

Водопад: возвращаемое значение предыдущего шага передается следующему шагу для использования.
Loop

Хук содержит петлю

Тип цикла: если функция прослушивателя возвращает значение true, функция прослушивателя будет выполняться повторно, а если она возвращает неопределенное значение, цикл будет прерван.

2. Примечания ко всем хукам

  • Когда все экземпляры крюка, получает необязательный параметр, параметр - это имена параметров массива строки
  • Имя параметра можно заполнять произвольно, но длина массива параметров должна соответствовать фактическому количеству принимаемых параметров.
  • Если функция обратного вызова не принимает параметры, вы можете передать пустой массив
  • Длина массива, переданного при создании экземпляра, полезна, а значение бесполезно.
  • Каждый экземпляр Hook - это менеджер событий, аналогичный публикации и подписке.Регистрация событий осуществляется по тапу, а первый параметр можно заполнять произвольно, даже если вы пишите комментарии на китайском языке, ведь при вызове вызова не нужно передавать имя события , и все события будут выполнены.
  • При выполнении вызова количество параметров связано с длиной массива на момент создания экземпляра.

4. Компилятор и компиляция

  • И компилятор, и компиляция наследуются от Tapable, поэтому на события можно подписываться и генерировать.
  • Компилятор:Когда Webpack выполняет сборку, он сначала считывает файл конфигурации Webpack для создания экземпляра объекта Compiler, а затем вызывает свой метод run для запуска полной компиляции.Объект Compiler представляет собой полную конфигурацию среды Webpack. Этот объект вызывается при запуске WebpackОдноразовыйСоздайте и настройте все рабочие параметры, включая параметры, загрузчик и плагин. При применении плагина в среде Webpack плагин получит ссылку на этот объект Compiler. Вы можете использовать его для доступа к основной среде Webpack.
  • Сборник:Объект представляет сборку версии ресурса. При запуске промежуточного программного обеспечения среды разработки Webpack каждый раз, когда обнаруживается изменение файла, создается новая компиляция, в результате чего создается новый набор скомпилированных ресурсов. Объект Compilation представляет текущие ресурсы модуля, скомпилированные ресурсы, измененные файлы и информацию о состоянии отслеживаемых зависимостей. Объект Compilation также предоставляет множество обратных вызовов для критических моментов времени, которые можно выбрать и использовать, когда подключаемый модуль выполняет пользовательскую обработку.

Пять, навыки чтения исходного кода Webpack

1. Найдите ключевые файлы

1.1 bin/webpack.js

node_modules\webpack\bin\webpack.js

  • Откройте файл package.json в проекте, найдите веб-пакет, Ctrl + щелчок мыши ==> Вы можете быстро найти местоположение веб-пакета.
// 找到这里的代码
// webpack 有两种命令行工具: webpack-cli 和 webpack-command 
// 因为 webpack-cli 功能更强大,一般都是用 webpack-cli,所以会执行下面的语句
else if (installedClis.length === 1) {
    const path = require("path");
    const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
    const pkg = require(pkgPath);
    require(path.resolve(
        path.dirname(pkgPath),
        pkg.bin[installedClis[0].binName]
    ));
} 

1.2 lib/webpack.js

node_modules\webpack\lib\webpack.js

  • Входной файл веб-пакета, вы можете прочитать исходный код здесь

1.3 webpack\declarations

node_modules\webpack\declarations

  • В этом каталоге размещаются файлы объявлений элементов конфигурации/плагинов веб-пакета, написанные на машинописном языке.

1.4 Compiler.js / Compilation.js

node_modules\webpack\lib

  • Введите следующий код в файл Compiler.js / Compilation.js веб-пакета: Вы можете получить все хуки веб-пакета.

2. Код отладки

2.1 Чтение идей

  • Первый сложить логику неродственных ветвей, посмотрите только на основной код процессов
  • Найдите критический путь, угадайте намерение на основе имен переменных и методов, а затем проверьте идею, прочитав исходный код.
  • Критический путь отладчика для понимания всего процесса выполнения

2.2 Первый способ отладки

  • Скопируйте код, найденный выше в bin/webpack.js, в отдельный файл debugger.js
// debugger.js 和项目中的 packge.json 同级
// 右键运行 debugger.js ,相当于使用 npx webpack 运行 webpack
// npx webpack 其实就是用 node 执行 bin 下面的 cli.js 
// npx webpack = node ./node_modules/webpack-cli/bin/cli.js
// 找到 webpack-cli/bin/cli.js ,设置断点,就可以开始调试了(这是第一种方法)
const path = require("path");
const pkgPath = require.resolve(`webpack-cli/package.json`);
const pkg = require(pkgPath);
require(path.resolve(
        path.dirname(pkgPath),
        './bin/cli.js'
        //pkg.bin['webpack-cli']
));

2.3 Второй способ отладки

  • Создайте новый файл cli.js, установите точки останова в webstorm, затем щелкните правой кнопкой мыши, чтобы запустить cli.js, чтобы начать отладку кода.
let webpack = require("webpack");
let webpackOptions = require("./webpack.config");
const compiler = webpack(webpackOptions);

compiler.run((err, stat) => {
  console.log(err);
  console.log(stat)
});

6. Анализ кода после сборки Webpack

1. webpack 4

1.1 webpack.config.js

const path = require('path');
module.exports = {
    // 用开发模式打包代码 !!!!!!
    mode:'development',
    devtool:'none',
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'bundle.js'
    },
};

1.2 Исходный код

// src/index.js
import {logMsg}  from './sync-module';
console.log(logMsg);
let button = document.createElement('button');
button.innerHTML = '请点我';
button.addEventListener('click',()=>{
    import(/*webpackChunkName: 'async-module'*/'./async-module.js').then(result=>{
        console.log(result.default);
    });
});
document.body.appendChild(button);


// src/async-module.js
module.exports = "我是异步模块";


// src/sync-module.js
export function logMsg() {
    console.log('我是同步模块');
}

1.3 bundle.js

/*
  src/
  index.js
  sync-module.js
  async-module.js
*/
// webpack 打包后,会把引用模块的相对路径变成相对于 webpack.config.js 的相对路径
// 在 index.js 中 引入 "./sync-module.js"  => 最终会变成 "./src/sync-module.js"

// webpack 启动代码的自执行函数
(function(modules) { // webpackBootstrap

  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {
    // data => [
      // [chunkName],
      // { chunkID : chunk 内容}
    // ]
    var chunkIds = data[0];
    var moreModules = data[1];

    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        // installedChunks[chunkId] => [resolve, reject, Promise]
        // 将 resolve 存到 resolves 数组中,先不着急执行 resolve()
        resolves.push(installedChunks[chunkId][0]);
      }
      // 设置为 0 ,表示已经加载成功
      installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
      if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        // 异步 chunk 加载完成后,将异步 chunk 的代码合并到 modules 中
        // 这样之后加载该 chunk 时,可以直接从 modules 中获取到
        // key 是模块 ID ,value 是模块内容
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if(parentJsonpFunction){
      parentJsonpFunction(data);
    }

    while(resolves.length) {
      // 执行 resolve
      resolves.shift()();
    }
  }


  // The module cache
  // 普通模块的缓存:只要加载初始化过一次的模块都放到这,之后再使用这个模块时
  // 直接从这里获取,不需要再初始化一遍
  var installedModules = {};


  // object to store loaded and loading chunks
  // !!!!!!!!!!!!!!!!!!!!!!
  // 存储已加载的或者加载中的 chunk (这里的 chunk 包含: 入口 chunk 和异步加载的 chunk)
  // !!!!!!!!!!!!!!!!!!!!!!
  // installedChunks 对象中,每个 key 对应的 value 值的意思
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // undefined 表示 chunk 还未加载,null 表示 chunk 会预加载
  // Promise = chunk loading, 0 = chunk loaded
  // Promise 表示 chunk 正在加载中,0 表示 chunk 加载完成
  var installedChunks = {
    // 如果是单入口,key 的默认值是 main
    "main": 0
  };


  // script path function
  // 设置异步 chunk 的请求 url
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle.js"
  }

  // The require function
  // webpack 自己实现的一个 require 方法,可以直接在浏览器中运行
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    // 加载模块前,先从缓存列表中查找,是否已经加载过
    if(installedModules[moduleId]) {
      // 如果有,说明模块已经缓存过,直接返回该模块的导出对象 exports
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // 创建一个新的模块对象,并且放到缓存列表中
    var module = installedModules[moduleId] = {
      // 模块 ID
      i: moduleId,
      // 是否已经加载 loaded:false
      l: false,
      // 模块导出对象,默认是一个空对象
      exports: {}
    };

    // Execute the module function
    // 加载模块
    // modules 是自执行函数接收的参数——一个包含模块信息的对象
    // key 是模块路径, value 是一个函数,里面包含了模块的内容
    // {
    //  "./src/a.js":
    //  (function(module, __webpack_exports__, __webpack_require__) {
    //    模块内容:xxx
    //    模块内容:xxx
    //    模块内容:xxx
    //   },
    // }
    // 从 modules 对象中找到对应的 key,执行函数(value)并将内部的 this 指向上面新建 module 的 exports 对象
    // 目的是将函数内部的内容都放置到 module.exports 中
    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;
  }

  // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  // 加载异步 chunk
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];

    // JSONP chunk loading for javascript
    // 用 jsonp 来请求加载异步 chunk
    var installedChunkData = installedChunks[chunkId];

    // 0 means "already installed".
    // 如果要加载的 chunk 没有初始化过
    if(installedChunkData !== 0) {

      // a Promise means "currently loading".
      // 排除了 0,缓存 chunk 列表里的值就剩下 undefined/null/Promise
      // 当模块正在加载中时
      if(installedChunkData) {
        promises.push(installedChunkData[2]);
      }
      // 当模块还未加载过
      else {
        // setup Promise in chunk cache
        var promise = new Promise(function(resolve, reject) {
          // 新建一个 promise 时,会立即执行它的函数体
          // 将当前 chunk 的状态设置为 Promise,表示正在加载中
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        // 给当前的 installedChunkData 添加一个值
        // 然后将 installedChunkData 添加到 promises 数组中
        promises.push(installedChunkData[2] = promise);

        // start chunk loading
        // 用 jsonp 来请求加载异步 chunk
        var script = document.createElement('script');
        var onScriptComplete;

        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        // 设置请求 url
        script.src = jsonpScriptSrc(chunkId);

        // create error before stack unwound to get useful stacktrace later
        var error = new Error();
        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);
      }
    }
    // 执行完所有的 promise后再返回结果
    return Promise.all(promises);
  };

  // expose the modules object (__webpack_modules__)
  // 将模块列表放到 __webpack_require__ 的 m 属性上
  __webpack_require__.m = modules;

  // expose the module cache
  // 将缓存列表放到 __webpack_require__ 的 c 属性上
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  // 在 exports 对象上定义 name 属性的 getter 方法
  __webpack_require__.d = function(exports, name, getter) {
    // 判断 exports 对象上是否有 name 属性
    if(!__webpack_require__.o(exports, name)) {
      // 在 exports 对象上添加 name 属性,可枚举为 true
      // get 的值为 getter,当访问该属性时,该方法会被执行
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  // 在 exports 对象上定义一个 __esModule 属性,用来判断当前模块是否为 es6 模块
  __webpack_require__.r = function(exports) {
    // 如果当前浏览器支持 Symbol
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // 设置前
      // console.log(exports.toString());// [object Object]
      // 给 exports 对象类型设置为 Module
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      // 设置后
      // console.log(exports.toString());// [object Module]
    }
    // 否则给 exports 对象添加一个表示 esm 的属性
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // create a fake namespace object  创建一个命名空间对象
  / 为什么要创建一个命名空间对象?
  // 因为 import('xxx.js') 加载的 js,可能是 esm ,也可能是 cjs
  // 所以需要兼容处理
  // mode & 1: value is a module id, require it 如果值是模块ID,加载它
  // mode & 2: merge all properties of value into the ns 把所有的属性合并到命名空间上 ns —— nameSpace
  // mode & 4: return value when already ns object  当已经是命名空间对象的话直接返回值
  // mode & 8|1: behave like require 就像 require 一样
  // mode 为什么要用二进制来判断?  高效。节约内存
  // linux 里面的权限判断也是用的二进制, 7 => 111 可读可写可执行
  __webpack_require__.t = function(value, mode) {
    // value 最开始是模块 ID
    // 直接加载模块
    if(mode & 1) value = __webpack_require__(value);
    // 不用加载模块,直接返回模块内容
    if(mode & 8) return value;
    // 如果 value 已经是一个对象并且 __esModule 属性为 true 的话就直接返回 value
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    // 否则就创建一个空对象,加载这个对象,
    var ns = Object.create(null);
    // 在对象上设置 __esModule 属性为true
    __webpack_require__.r(ns);
    // 给 ns 对象定义一个 default 属性
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    // 如果 mode 为2,并且 value 不是字符串,把值的所有属性都定义到 ns 对象上
    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;//{__esModule:true,default:'模块内容'}
  };

  // getDefaultExport function for compatibility with non-harmony modules
  // 一个能获取模块内容的函数
  __webpack_require__.n = function(module) {
    // 如果是 __esModule,说明是 es6 模块,需要返回模块的 default 属性
    // 如果不是,说明是 cjs 模块,直接返回模块本身
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    //给 getter 添加一个 a 的属性,就是 getter 方法本身
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  // 公开访问路径
  __webpack_require__.p = "";

  // on error function for async loading
  // 加载异步 chunk 时的错误输出
  __webpack_require__.oe = function(err) { console.error(err); throw err; };


  // 第一次执行的时候,window["webpackJsonp"] 会是一个空数组
  // jsonpArray 和 window["webpackJsonp"] 共同指向同一块内存地址
  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];

  // 绑定 this,将老的数组的 push 方法始终指向 jsonpArray
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  // 如果不绑定 this 的话,那么在 webpackJsonpCallback 中执行 parentJsonpFunction(data) 的时候
  // 就相当于执行了一个 “裸的”数组原生 的 push,data 不知道该添加给谁
  // var oldJsonpFunction = jsonpArray.push;

  //重写 jsonArray 的 push 方法,赋值为 webpackJsonpCallback
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  // 为什么要保留老的数组的 push 方法?
  // 避免 "重复发请求" 加载 chunk ,如果已经加载好了的,就拿来直接用
  var parentJsonpFunction = oldJsonpFunction;

  // 总结:
  // 1、window["webpackJsonp"] 的 push 方法被重写,不再是数组原生的方法,而是用来执行 jsonp 回调函数的
  // 2、这时候如果想要给 window["webpackJsonp"] 这个数组添加数据时,就无法用 push 来添加了
  // 3、所以这里多定义一个 jsonpArray 数组,它和 window["webpackJsonp"] 共同指向同一块内存地址
  // 4、通过给 jsonpArray 添加(push)数据,那么相应的 window["webpackJsonp"] 就能获取到这些数据

  // Load entry module and return exports
  // 加载入口模块并且返回导出对象
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
/*
  src/
  index.js
  sync-module.js
  async-module.js
*/
// webpack 打包后,会把引用模块的相对路径变成相对于 webpack.config.js 的相对路径
// 在 index.js 中 引入 "./sync-module.js"  => 最终会变成 "./src/sync-module.js"

({
  // key 是模块 ID ,value 是模块内容
  "./src/index.js":
  /*!**********************!*\
    // 入口 chunk
      !*** ./src/index.js ***!
      \**********************/
  /*! no exports provided */
    (function(module, __webpack_exports__, __webpack_require__) {

      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var _sync_module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync-module */ "./src/sync-module.js");

      console.log(_sync_module__WEBPACK_IMPORTED_MODULE_0__["logMsg"]);

      let button = document.createElement('button');
      button.innerHTML = '请点我';
      button.addEventListener('click',()=>{
        __webpack_require__.e(/*! import() | async-module */ "async-module")
          .then(__webpack_require__.t.bind(null, /*! ./async-module.js */ "./src/async-module.js", 7))
          .then(result=>{// result = {__esModule:true,default:'模块内容'}
          console.log(result.default);
        });
      });
      document.body.appendChild(button);
    }),

  "./src/sync-module.js":
  /*!****************************!*\
      !*** ./src/sync-module.js ***!
       // 入口 chunk 依赖的同步模块
      \****************************/
  /*! exports provided: logMsg */
    (function(module, __webpack_exports__, __webpack_require__) {

      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "logMsg", function() { return logMsg; });

      function logMsg() {
        console.log('我是同步模块');
      }
    })
});

2. webpack 5

  • По сравнению с webpack 4 код читается немного легче.
(function(modules) { // webpack 的启动代码自执行函数
  // The module cache 模块的缓存
  var installedModules = {};

  // The require function webpack自己实现了一个require方法
  function __webpack_require__(moduleId) {

    // Check if module is in cache 判断一下这个模块ID是否在缓存中
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;//如果有,说明此模块加载过,直接返回导出对象exports
    }
    // Create a new module (and put it into the cache)
    // 创建一个新的模块对象并且把它放到缓存中
    var module = installedModules[moduleId] = {
      i: moduleId,// 模块ID
      l: false,//是否已经加载loaded false
      exports: {} //导出对象,默认是一个空对象
    };

    // Execute the module function 执行此模块对应的方法,目的是给module.exports赋值
    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;
  }

  // the startup function
  function startup() {
    // Load entry module and return exports
    // 加载入口模块并且返回导出对象
    return __webpack_require__("./src/index.js");
  }

  // run startup 执行启动方法
  return startup();
})
({

 "./src/hello.js":
 (function(module) {
   module.exports = "hello";
 }),
 "./src/index.js":
 (function(__unusedmodule, __unusedexports, __webpack_require__) {
    let hello = __webpack_require__( "./src/hello.js");
    console.log(hello);
 })
});

7. Абстрактное синтаксическое дерево

1. Что такое АСТ

  • JavaScript Parser преобразует код в абстрактное синтаксическое дерево (AST), которое определяет структуру кода. Манипулируя этим деревом, мы можем точно определить местонахождение операторов объявления, операторов присваивания, операторов операций и т. д. Анализ кода, оптимизация, изменения и другие операции.


2. AST Использование

  • Проверка синтаксиса кода, проверка стиля кода, форматирование кода, подсветка кода, подсказки об ошибках кода, автодополнение кода и т. д.
  • Например, JSLint, JSHint для проверки ошибок кода или стиля, поиска возможных ошибок.
  • Подсказки IDE об ошибках, форматирование, выделение, автозаполнение и т. д.
  • обфускация кода
  • UglifyJS2 и т. д.
  • Оптимизировать и изменить код, изменить структуру кода, чтобы добиться желаемой структуры
  • Сборщики кода Webpack, Rollup и т. д.
  • Преобразование между CommonJS, AMD, CMD, UMD, как спецификации кода
  • CoffeeScript, TypeScript, JSX и т. д. в собственный Javascript

3. Процесс выполнения АСТ

  • Разобрать исходный код
  • Лексический анализ:Лексический парсер (Tokenizer) на этом этапе будетстрока кодаПреобразуется в массив синтаксических ячеек --Tokens(жетон). Например, результат лексического анализа for (const item of items) {} выглядит следующим образом:

Синтаксические единицы в коде Javascript в основном включают следующее:

  • Ключевые слова:const,let,varЖдать
  • Идентификатор: это может быть переменная или ключевые слова, такие как if и else, или константы, такие как true и false.
  • оператор
  • количество
  • пространство
  • Примечания
  • Синтаксический анализ:На этом этапе синтаксический анализатор (Parser) будет преобразовывать Токены в абстрактные синтаксические деревья.
  • Обход синтаксического дерева в глубину, изменение синтаксического дерева
  • Преобразование синтаксического дерева обратно в исходный код



4. JavaScript Parser

  • JavaScript Parser, парсер, который преобразует исходный код js в абстрактное синтаксическое дерево.
  • Браузер преобразует исходный код js в абстрактное синтаксическое дерево с помощью парсера, а затем преобразует его в байт-код или напрямую сгенерирует машинный код.
  • Вообще говоря, каждый движок js будет иметь свой собственный формат абстрактного синтаксического дерева, такой как движок Chrome v8, движок Firefox SpiderMonkey и т. д. MDN предоставляет подробное описание формата SpiderMonkey AST, который является отраслевым стандартом.

5. Инструменты, необходимые в проекте

  • astexplorer
  • @babel/coreОн имеет встроенный вавилон/парсер, который также можно использовать для конвертации AST
  • @babel/parser is a JavaScript parser used in Babel.
  • @babel/traverse maintains the overall tree state, and is responsible for replacing, removing, and adding nodes.
  • @babel/types contains methods for building ASTs manually and for checking the types of AST nodes.
  • @babel/generator Turns an AST into code.

6. Пример использования AST

6.1 Преобразование стрелочных функций

const babylon = require('@babel/parser');
// @babel/core 里面内置了 babylon/parser,也可以用它来转换 AST
const babel = require('@babel/core');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;

const originalSource = "const a = (a, b) => a + b;";

// 将 当前模块 的内容转换成 AST
const ast = babylon.parse(originalSource);
// @babel/core 里面内置了 babylon/parser,也可以用它来转换 AST
// const ast = babel.parse(originalSource);

// 遍历语法树,寻找要修改的目标节点
traverse(ast, {
    // 如果当前节点是一个 箭头函数 时
    ArrowFunctionExpression: (nodePath) => {
        let node = nodePath.node;
        let body = node.body;
        if(!types.isBlockStatement(node.body)){
            body = types.blockStatement([types.returnStatement(node.body)])
        }
        let newNode = types.functionExpression(null,node.params,body);
        nodePath.replaceWith(newNode);
    }
});

// 把转换后的抽象语法树重新生成代码
let {code} = generate(ast);
console.log('新的 code =>', code);

6.2 Преобразование классов

const babylon = require('@babel/parser');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;
const originalSource = `class Person{
    constructor(name){
        this.name = name;
    }
    getName(){
        return this.name
    }
}`;
// 将 当前模块 的内容转换成 AST
const ast = babylon.parse(originalSource);
// 遍历语法树,寻找要修改的目标节点
traverse(ast, {
    // 如果当前节点是一个 class 时
    ClassDeclaration: (nodePath) => {
        let node = nodePath.node;
        let bodys = node.body.body;
        let id = node.id;
        bodys = bodys.map(body => {
            if (body.kind === 'constructor') {
                return types.functionExpression(id, body.params, body.body)
            } else {
                let left = types.memberExpression(id, types.identifier('prototype'));
                left = types.memberExpression(left, body.key);
                let right = types.functionExpression(null, body.params, body.body);
                return types.assignmentExpression('=', left, right);
            }
        });
        nodePath.replaceWithMultiple(bodys);
    }
});
// 把转换后的抽象语法树重新生成代码
let {code} = generate(ast);
console.log('新的 code =>', code);

8. Послесловие

  • Некоторые вопросы, затронутые в этой статье, относительно неглубокие, и те, кто заинтересован, могут обратиться к соответствующей информации для более глубокого понимания.
  • Так как статья была написана на Yuque раньше, то водяные знаки некоторых картинок стали моими.Прошу простить,если обидела,скажите пожалуйста первоначальный адрес,я добавлю позже,спасибо.

9. Справочные статьи

GitHub.com/Джейми строит…

developer.Mozilla.org/this-cn/docs/…

nuggets.capable/post/684490…

GitHub.com/Веб-пакет/Taping…

woohoo.brief.com/afraid/273oh1from990…

10. Исходный код

GitHub.com/Давай в первом квартале/О…