Научить вас простому веб-пакету

Node.js внешний интерфейс JavaScript Webpack

задний план

С постоянным улучшением сложности внешнего интерфейса появилось много инструментов упаковки, таких как первыйgrunt,gulp. к более позднемуwebpackиParcel. Но есть много инструментов лесов, таких какvue-cliПомог нам интегрировать использование некоторых инструментов сборки. Иногда мы можем не знать его внутреннего принципа реализации. Фактически, понимание того, как работают эти инструменты, может помочь нам лучше понять и использовать эти инструменты, а также облегчить наше применение в разработке проекта.

некоторые очки знаний

Прежде чем мы начнем строить колеса, нам нужно проделать некоторую резервную работу над некоторыми точками знаний.

модульное знание

Первое — это актуальные знания модуля, главное —es6 modulesиcommonJSМодульная спецификация. Более подробную информацию можно найти здесьАнализ принципов CommonJS, AMD/CMD, модулей ES6 и веб-пакета. Теперь нам просто нужно понять:

  1. es6 modulesЭто способ определения зависимостей модуля во время компиляции.
  2. CommonJSВ спецификации модуля Node оборачивает содержимое заголовка и хвоста файла при компиляции JS-файла. , добавьте в шапку(function (export, require, modules, __filename, __dirname){\nдобавлено в конце\n};. Таким образом, мы можем использовать эти параметры внутри одного JS-файла.

Основы АСТ

Что такое абстрактное синтаксическое дерево?

В информатике абстрактное синтаксическое дерево (сокращенно абстрактное синтаксическое дерево или AST) или синтаксическое дерево (синтаксическое дерево) представляет собой древовидное представление абстрактной синтаксической структуры исходного кода, в частности исходного кода языка программирования. . Каждый узел дерева представляет собой структуру в исходном коде. Грамматика называется «абстрактной», потому что грамматика здесь не представляет все детали, которые встречаются в реальной грамматике.

image

Каждый может пройтиEsprimaэтот сайт, чтобы преобразовать код вast. Во-первых, абстрактное синтаксическое дерево, преобразованное в фрагмент кода, представляет собой объект, который будет иметь структуру верхнего уровня.typeАтрибутыProgram, второе свойствоbodyпредставляет собой массив. Каждый элемент, хранящийся в массиве body, представляет собой объект, содержащий всю информацию описания оператора:

type:描述该语句的类型 --变量声明语句
kind:变量声明的关键字 -- var
declaration: 声明的内容数组,里面的每一项也是一个对象
	type: 描述该语句的类型 
	id: 描述变量名称的对象
		type:定义
		name: 是变量的名字
    init: 初始化变量值得对象
		type: 类型
		value: 值 "is tree" 不带引号
		row: "\"is tree"\" 带引号

К точке

простая упаковка webpack

Обладая вышеуказанными базовыми знаниями, давайте взглянем на простойwebpackВ процессе упаковки сначала мы определяем 3 файла:

// index.js
import a from './test'

console.log(a)

// test.js
import b from './message'

const a = 'hello' + b

export default a

// message.js
const b = 'world'

export default b

Способ очень простой, определитеindex.jsЦитироватьtest.js;test.jsвнутренняя ссылкаmessage.js. Взгляните на упакованный код:

(function (modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      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;
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {enumerable: true, get: getter});
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
    }
    Object.defineProperty(exports, '__esModule', {value: true});
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    /******/
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', {enumerable: true, value: value});
    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;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
    var getter = module && module.__esModule ?
      function getDefault() {
        return module['default'];
      } :
      function getModuleExports() {
        return module;
      };
    __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 = "";
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {

    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");

  }),
  "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  })
});

Выглядишь грязным? Ничего страшного, повторим. На первый взгляд мы видим эту форму:

(function(modules) {
  // ...
})({
 // ...
})

Это легко понять, это самовыполняющаяся функция, которая передается вmodulesОбъект, каков формат объекта модулей? Приведенный выше код уже дает нам ответ:

{
  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  })
}

такой路径 --> 函数Такая пара ключ-значение ключ-значение. А внутри функции находится код после передачи определенного нами файла в ES5:

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");

На этом этапе структура в основном анализируется, а затем мы смотрим на ее выполнение Код, выполняемый в начале самовыполняющейся функции:

__webpack_require__(__webpack_require__.s = "./src/index.js");

называется__webpack_require_функционировать и передавать вmoduleIdпараметр"./src/index.js". Давайте посмотрим на основную реализацию внутри функции:

// 定义 module 格式   
var module = installedModules[moduleId] = {
      i: moduleId, // moduleId
      l: false, // 是否已经缓存
      exports: {} // 导出对象,提供挂载

};

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

Здесь нас называютmodulesфункция в и передается в__webpack_require__Функция как вызов внутри функции.module.exportsПараметры экспортируются как внутри функции. так какindex.jsцитируется вtest.js, так что это пройдет снова__webpack_require__выполнитьtest.jsЗагрузка:

var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");

test.jsиспользуется сноваmessage.jsтак,test.jsВнутри он будет выполнятьmessage.jsнагрузка.message.jsПосле завершения выполнения из-за отсутствия зависимостей результат возвращается напрямую:

var b = 'world'
__webpack_exports__["default"] = (b)

После завершения выполнения вернуться к корневому файлу один уровень за другимindex.js. Наконец, обработка всей файловой зависимости завершена. В течение всего процесса мы продолжаем вводить число в виде дерева зависимостей, а когда результат будет возвращен, начинаем трассировку до корня.

Разработайте простой крошечный пакет

Посредством вышеупомянутых исследований давайте сначала рассмотрим, что может сделать базовый инструмент для упаковки и компиляции?

  1. Преобразование синтаксиса ES6 в ES5
  2. Обработка зависимостей загрузки модуля
  3. Создайте файл js, который можно загрузить и выполнить в браузере.

Первый вопрос, синтаксис преобразования, мы действительно можем передатьbabelсделать это. Основные шаги:

  • пройти черезbabylonСгенерировать АСТ
  • пройти черезbabel-coreВосстановите AST в исходный код
/**
 * 获取文件,解析成ast语法
 * @param filename // 入口文件
 * @returns {*}
 */
function getAst (filename) {
  const content = fs.readFileSync(filename, 'utf-8')

  return babylon.parse(content, {
    sourceType: 'module',
  });
}

/**
 * 编译
 * @param ast
 * @returns {*}
 */
function getTranslateCode(ast) {
  const {code} = transformFromAst(ast, null, {
    presets: ['env']
  });
  return code
}

Затем нам нужно разобраться с зависимостями модулей, затем нам нужно получить представление зависимостей. к счастьюbabel-traverseобеспечивает проходимуюASTпросматривать и выполнять функцию обработки, черезImportDeclarationСвойства зависимости можно получить:

function getDependence (ast) {
  let dependencies = []
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  })
  return dependencies
}

/**
 * 生成完整的文件依赖关系映射
 * @param fileName
 * @param entry
 * @returns {{fileName: *, dependence, code: *}}
 */
function parse(fileName, entry) {
  let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName
  let dirName = entry ? '' : path.dirname(config.entry)
  let absolutePath = path.join(dirName, filePath)
  const ast = getAst(absolutePath)
  return {
    fileName,
    dependence: getDependence(ast),
    code: getTranslateCode(ast),
  };
}

Пока что мы также просто получаем зависимости корневого файла и скомпилированного кода, как нашindex.jsзависел отtest.jsно мы не знаемtest.jsтакже необходимо зависеть отmessage.js, их исходный код не компилируется. Итак, на этом этапе нам также нужно выполнить обход глубины, чтобы получить завершенные зависимости глубины:

/**
 * 获取深度队列依赖关系
 * @param main
 * @returns {*[]}
 */
function getQueue(main) {
  let queue = [main]
  for (let asset of queue) {
    asset.dependence.forEach(function (dep) {
      let child = parse(dep)
      queue.push(child)
    })
  }
  return queue
}

Итак, на этом шаге мы завершили компиляцию и разбор всех файлов. Последний шаг заключается в том, что нам нужно следоватьwebpackИдея некой упаковки исходного кода. Первым шагом является созданиеmodulesОбъект:

function bundle(queue) {
  let modules = ''
  queue.forEach(function (mod) {
    modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
  })
  // ...
}

получитьmodulesПосле объекта следующий шаг — внешняя упаковка общего файла, регистрацияrequire,module.exports:

(function(modules) {
      function require(fileName) {
          // ...
      }
     require('${config.entry}');
 })({${modules}})

Внутри функции он просто выполняет код JS каждого зависимого файла в цикле, завершите код:

function bundle(queue) {
  let modules = ''
  queue.forEach(function (mod) {
    modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
  })

  const result = `
    (function(modules) {
      function require(fileName) {
        const fn = modules[fileName];

        const module = { exports : {} };

        fn(require, module, module.exports);

        return module.exports;
      }

      require('${config.entry}');
    })({${modules}})
  `;

  return result;
}

Это в основном введение здесь, давайте упакуем и попробуем:

(function (modules) {
  function require(fileName) {
    const fn = modules[fileName];

    const module = {exports: {}};

    fn(require, module, module.exports);

    return module.exports;
  }

  require('./src/index.js');
})({
  './src/index.js': function (require, module, exports) {
    "use strict";

    var _test = require("./test");

    var _test2 = _interopRequireDefault(_test);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {default: obj};
    }

    console.log(_test2.default);
  }, './test': function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });

    var _message = require("./message");

    var _message2 = _interopRequireDefault(_message);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {default: obj};
    }

    var a = 'hello' + _message2.default;
    exports.default = a;
  }, './message': function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    var b = 'world';

    exports.default = b;
  },
})

Протестируйте еще раз:

image

Ну в принципе простойtinypack.

Справочная статья

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

Абстрактное синтаксическое дерево JS, которое вы можете понять с первого взгляда

исходный код

Весь исходный код tinypack загруженgithub