Универсальный дизайн сборщика на основе esbuild

JavaScript

задний план

Поскольку инструмент компиляции Lynx (самостоятельно разработанная кросс-энд инфраструктура компании) сильно отличается от традиционной цепочки инструментов веб-компиляции (например, он не поддерживает динамический стиль и динамический скрипт, фактически прощаясь с безпакетностью и разделением кода, модуль система основана на json вместо js, и нет среды просмотра сервера), и есть требования для компиляции в реальном времени на веб-стороне (построение системы), динамической компиляции на веб-стороне (WebIDE), в реальном времени компиляция на стороне сервера (компиляция и доставка на стороне сервера) и переключение между несколькими версиями, поэтому нам нужно разработать ту, которая поддерживает Упаковщик, который работает в браузере и может быть гибко настроен в соответствии с бизнесом, также поддерживается локально, а именно универсальный сборщик.В процессе разработки универсального сборщика мы тоже столкнулись с некоторыми проблемами.Наконец мы разработали новый универсальный сборщик на основе esbuild,который решил наши проблемы.до большинства проблем.

что такое упаковщик

Задача сборщика состоит в том, чтобы упаковать ряд кода, организованного в модули, в один или несколько файлов.Наши распространенные сборщики включают webpack, rollup, esbuild и т. д. Большинство форм модульной организации здесь относятся к модульной системе на основе js, но не исключают модульные системы, организованные другими способами (например, wasm, использование компонентов json апплета, импорт css и html и т. д.), и сгенерированные файлы также могут быть не одним файлом, например (несколько файлов js, сгенерированных путем разделения кода, или разные файлы js, css, html и т. д.). Основные принципы работы большинства сборщиков аналогичны, но они делают акцент на определенных функциях, таких как

  • webpack : подчеркивает поддержку веб-разработки, особенно встроенную поддержку HMR, система подключаемых модулей относительно мощная, а совместимость с различными модульными системами является лучшей (amd, cjs, umd, esm и т. д., совместимость многовато, это На самом деле есть плюсы и минусы, ведущие к webpack-ориентированному программированию), и у него богатая экология. Недостаток в том, что продукт недостаточно чистый, продукт не поддерживает генерацию esm трудно начать разработку плагинов, и он не подходит для разработки библиотек.
  • rollup: Упор на поддержку разработки библиотек. Основанный на модульной системе ESM, он имеет хорошую поддержку для встряхивания дерева. Продукт очень чистый и поддерживает несколько выходных форматов. Он подходит для разработки библиотек. hacks, не поддерживает HMR и нуждается в различных подключаемых модулях для разработки приложений.
  • esbuild: Акцент на производительность, встроенная поддержка css, изображений, реагирования, машинописи и т. д., скорость компиляции особенно высока (в 100 раз быстрее, чем webpack и rollup), недостатком является то, что текущая система плагинов относительно простой, а экология не такая зрелая, как webpack и rollup.

как работает упаковщик

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

  • llvm: скомпилируйте каждый язык в LLVM IR через внешний интерфейс компилятора, затем выполните различные оптимизации на основе LLVM IR, а затем создайте различные коды наборов инструкций ЦП на основе оптимизированного LLVM IR в соответствии с различными архитектурами процессоров.
  • упаковщик: сначала скомпилируйте каждый модуль в граф модуля, затем выполните встряхивание дерева && разделение кода &&minify и другие оптимизации на основе графа модуля и, наконец, сгенерируйте js-код в разных форматах из оптимизированного графа модуля в соответствии с указанным форматом.

Сравнение LLVM и упаковщика

6.pngGJWJPЭто также позволяет реализовать многие стратегии оптимизации компиляции традиционного LLVM в сборщике, и esbuild является примером расширения этой практики до крайности. Так как функция и структура свертки относительно упрощены, давайте возьмем ее в качестве примера, чтобы увидеть, как работает упаковщик. Процесс объединения пакетов делится на два этапа: объединение и создание, которые соответствуют двум процессам внешнего интерфейса и внутреннего интерфейса сборщика соответственно.

  • src/main.js

import lib from './lib';

console.log('lib:', lib);
  • src/lib.js
const answer = 42;
export default answer;

Сначала создайте график модуля с помощью

const rollup = require('rollup');
const util = require('util');
async function main() {
  const bundle = await rollup.rollup({
    input: ['./src/index.js'],
  });
  console.log(util.inspect(bundle.cache.modules, { colors: true, depth: null }));
}
main();

Вывод выглядит следующим образом

[
{
  code: 'const answer = 42;\nexport default answer;\n',
  ast: xxx,
  depenencies: [],
  id: 'Users/admin/github/neo/examples/rollup-demo/src/lib.js'
  ...
},
{
  ast: xxx,
  code: 'import lib from './lib';\n\nconsole.log('lib:', lib);\n',
  dependencies: [ '/Users/admin/github/neo/examples/rollup-demo/src/lib.js' ]
  id: '/Users/admin/github/neo/examples/rollup-demo/src/index.js',
  ...
}]

Разобранная структура ast каждого модуля, уже включенного в наш сгенерированный продукт, а также зависимости между модулями. После построения графа модулей накопительный пакет может продолжать создавать продукты на основе графа модулей в соответствии с пользовательской конфигурацией.

 const result = await bundle.generate({
    format: 'cjs',
  });
  console.log('result:', result);

Сгенерированный контент выглядит следующим образом

exports: [],
      facadeModuleId: '/Users/admin/github/neo/examples/rollup-demo/src/index.js',
      isDynamicEntry: false,
      isEntry: true,
      type: 'chunk',
      code: "'use strict';\n\nconst answer = 42;\n\nconsole.log('lib:', answer);\n",
      dynamicImports: [],
      fileName: 'index.js',

Таким образом, базовый процесс сборки JavaScript не сложен, но если его действительно нужно применять в производственной среде и поддерживать сложные и разнообразные бизнес-потребности, его мощная система подключаемых модулей неотделима.

Система плагинов

Большинство упаковщиков предоставляют систему подключаемых модулей, позволяющую пользователям настраивать логику упаковщика. Например, плагины свертки делятся на входные плагины и выходные плагины, Входной плагин соответствует процессу генерации Module Graph по входным данным, а выходной плагин соответствует процессу генерирования продукции согласно Модульному графику. Здесь мы в основном обсуждаем подключаемый модуль ввода, который является ядром системы подключаемых модулей сборщика.В качестве примера мы возьмем систему подключаемых модулей esbuild, чтобы увидеть, что мы можем сделать с системой подключаемых модулей. Основным процессом ввода является создание графа зависимостей.Основная функция графа зависимостей заключается в определении содержимого исходного кода каждого модуля. Плагин ввода позволяет настроить способ загрузки исходного кода модулем. Большинство систем плагинов ввода предоставляют два основных хука.

  • onResolve (resolveId в сводке, factory.hooks.resolver в веб-пакете): определение фактического адреса модуля на основе идентификатора модуля.
  • onLoad (называется loadId в сводке, загрузчик в webpack): загружает содержимое модуля в соответствии с адресом модуля)

Load Здесь esbuild и rollup отличаются от обработки webpack. esbuild предоставляет только хуки загрузки. Вы можете выполнять работу по трансформации в хуках загрузки. Rollup дополнительно предоставляет хуки трансформации, которые четко отличаются от функции загрузки ( Но это не мешает вам выполнять преобразование при загрузке), в то время как веб-пакет делегирует работу преобразования загрузчику для завершения. Хотя функции этих двух крючков кажутся небольшими, в сочетании они могут выполнять очень богатые функции. (Документация плагина, в отличие от документации веб-пакета, просто мусор) По сравнению с системой подключаемых модулей rollup и webpack, наиболее выдающейся особенностью системы подключаемых модулей esbuild является поддержка виртуальных модулей. Давайте кратко рассмотрим несколько примеров, чтобы продемонстрировать роль плагинов.

loader

Одним из наиболее распространенных требований к людям, использующим webpack, является использование различных загрузчиков для обработки ресурсов, отличных от js, таких как импорт изображений и css.Давайте посмотрим, как использовать плагин esbuild для реализации простого меньшего загрузчика.

export const less = (): Plugin => {
  return {
    name: 'less',
    setup(build) {
      build.onLoad({ filter: /.less$/ }, async (args) => {
        const content = await fs.promises.readFile(args.path);
        const result = await render(content.toString());
        return {
          contents: result.css,
          loader: 'css',
        };
      });
    },
  };
};

Нам нужно только отфильтровать типы файлов, которые мы хотим обработать, через фильтр в onLoad, затем прочитать содержимое файла и настроить преобразование, а затем вернуть результат встроенному css-загрузчику esbuild для обработки. это очень просто Большинство функций загрузчика можно реализовать через плагин onLoad.

sourcemap && cache && error handle

Приведенный выше пример относительно упрощен. Как более зрелый подключаемый модуль, он также должен учитывать сопоставление исходной карты и пользовательского кэша после преобразования, чтобы уменьшить повторяющиеся накладные расходы на загрузку и обработку ошибок. Давайте используем пример svelte, чтобы увидеть, как разобраться с исходной картой, кешем и ошибками.

let sveltePlugin = {
  name: 'svelte',
  setup(build) {
    let svelte = require('svelte/compiler')
    let path = require('path')
    let fs = require('fs')
    let cache = new LRUCache(); // 使用一个LRUcache来避免watch过程中内存一直上涨
    build.onLoad({ filter: /.svelte$/ }, async (args) => {
      let value = cache.get(args.path); // 使用path作为key
      let input = await fs.promises.readFile(args.path, 'utf8');
      if(value && value.input === input){
         return value // 缓存命中,跳过后续transform逻辑,节省性能
      }
      // This converts a message in Svelte's format to esbuild's format
      let convertMessage = ({ message, start, end }) => {
        let location
        if (start && end) {
          let lineText = source.split(/\r\n|\r|\n/g)[start.line - 1]
          let lineEnd = start.line === end.line ? end.column : lineText.length
          location = {
            file: filename,
            line: start.line,
            column: start.column,
            length: lineEnd - start.column,
            lineText,
          }
        }
        return { text: message, location }
      }

      // Load the file from the file system
      let source = await fs.promises.readFile(args.path, 'utf8')
      let filename = path.relative(process.cwd(), args.path)

      // Convert Svelte syntax to JavaScript
      try {
        let { js, warnings } = svelte.compile(source, { filename })
        let contents = js.code + `//# sourceMappingURL=` + js.map.toUrl() // 返回sourcemap,esbuild会自动将整个链路的sourcemap进行merge
        return { contents, warnings: warnings.map(convertMessage) } // 将warning和errors上报给esbuild,经esbuild再上报给业务方
      } catch (e) {
        return { errors: [convertMessage(e)] }
      }
    })
  }
}

require('esbuild').build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
  plugins: [sveltePlugin],
}).catch(() => process.exit(1))

На данный момент мы реализовали относительно полную функцию svelte-loader.

virtual module

Основным улучшением плагина esbuild по сравнению с плагином rollup является поддержка виртуальных модулей.Как правило, упаковщик должен обрабатывать две формы модулей, один из которых представляет собой путь, соответствующий реальному пути к файлу на диске, а другой путь не соответствуют реальному пути к файлу.Путь к файлу должен генерировать соответствующий контент в соответствии с формой пути, то есть виртуальным модулем. Виртуальные модули имеют очень богатые сценарии применения.

glob import

Для общего сценария разрабатываем аналогичныйrollupjs.org/repl/При подобном repl обычно необходимо загружать некоторые примеры кода в memfs, а затем строить на основе memfs в браузере, но если в примере задействовано много файлов, очень хлопотно импортировать эти файлы по одному, мы может поддерживать импорт в форме шара. Примеры/

examples
    index.html
    index.tsx
    index.css
import examples from 'glob:./examples/**/*';
import {vol}  from 'memfs';
vol.fromJson(examples,'/'); //将本地的examples目录挂载到memfs

Подобные функции могут бытьviteИли babel-plugin-macro реализовать, посмотрим как это реализует esbuild. Реализация вышеуказанной функции на самом деле очень проста, нам нужно только

  • Проанализируйте пользовательский путь в onResolve, затем передайте метаданные в onLoad через pluginData и path и настройте пространство имен (роль пространства имен заключается в том, чтобы предотвратить загрузку возвращаемого пути обычной логикой загрузки файла и выполнение этого для последующих загрузок. filter filter )
  • Получите метаданные, возвращаемые onResolve, через фильтрацию пространства имен в onLoad, настройте логику загрузки и генерации данных в соответствии с метаданными, а затем передайте сгенерированный контент встроенному загрузчику esbuild для обработки
const globReg = /^glob:/;
export const pluginGlob = (): Plugin => {
  return {
    name: 'glob',
    setup(build) {
      build.onResolve({ filter: globReg }, (args) => {
        return {
          path: path.resolve(args.resolveDir, args.path.replace(globReg, '')),
          namespace: 'glob',
          pluginData: {
            resolveDir: args.resolveDir,
          },
        };
      });
      build.onLoad({ filter: /.*/, namespace: 'glob' }, async (args) => {
        const matchPath: string[] = await new Promise((resolve, reject) => {
          glob(
            args.path,
            {
              cwd: args.pluginData.resolveDir,
            },
            (err, data) => {
              if (err) {
                reject(err);
              } else {
                resolve(data);
              }
            }
          );
        });
        const result: Record<string, string> = {};
        await Promise.all(
          matchPath.map(async (x) => {
            const contents = await fs.promises.readFile(x);
            result[path.basename(x)] = contents.toString();
          })
        );
        return {
          contents: JSON.stringify(result),
          loader: 'json',
        };
      });
    },
  };
};

Фильтрация esbuild на основе фильтра и пространства имен осуществляется из соображений производительности. Регулярность фильтра здесь соответствует регулярности golang, а пространство имен — это строка. Поэтому esbuild может фильтровать на основе фильтра и пространства имен, чтобы избежать ненужных вызовов js. Свернуть накладные расходы на вызов golang js, но вы все равно можете установить фильтр в /.*/, чтобы полностью попасть в js, и фильтровать в js, фактическое попадание в накладные расходы на самом деле приемлемо.

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

memory virtual module

Модуль env здесь полностью рассчитывается на основе переменных окружения.

let envPlugin = {
  name: 'env',
  setup(build) {
    // Intercept import paths called "env" so esbuild doesn't attempt
    // to map them to a file system location. Tag them with the "env-ns"
    // namespace to reserve them for this plugin.
    build.onResolve({ filter: /^env$/ }, args => ({
      path: args.path,
      namespace: 'env-ns',
    }))

    // Load paths tagged with the "env-ns" namespace and behave as if
    // they point to a JSON file containing the environment variables.
    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
      contents: JSON.stringify(process.env),
      loader: 'json',
    }))
  },
}

// 
import { NODE_ENV } from 'env' // env为虚拟模块,

function virtual module

Используйте имена модулей в качестве функций, выполняйте вычисления во время компиляции и даже поддерживайте рекурсивные вызовы функций.

 build.onResolve({ filter: /^fib((\d+))/ }, args => {
            return { path: args.path, namespace: 'fib' }
   })
  build.onLoad({ filter: /^fib((\d+))/, namespace: 'fib' }, args => {
        let match = /^fib((\d+))/.exec(args.path), n = +match[1]
        let contents = n < 2 ? `export default ${n}` : `
              import n1 from 'fib(${n - 1}) ${args.path}'
              import n2 from 'fib(${n - 2}) ${args.path}'
              export default n1 + n2`
         return { contents }
  })
  // 使用方式
  import fib5 from 'fib(5)' // 直接编译器获取fib5的结果,是不是有c++模板的味道

stream import

Вы можете выполнить npm run dev без загрузки node_modules

import { Plugin } from 'esbuild';
import { fetchPkg } from './http';
export const UnpkgNamepsace = 'unpkg';
export const UnpkgHost = 'https://unpkg.com/';
export const pluginUnpkg = (): Plugin => {
  const cache: Record<string, { url: string; content: string }> = {};
  return {
    name: 'unpkg',
    setup(build) {
      build.onLoad({ namespace: UnpkgNamepsace, filter: /.*/ }, async (args) => {
        const pathUrl = new URL(args.path, args.pluginData.parentUrl).toString();
        let value = cache[pathUrl];
        if (!value) {
          value = await fetchPkg(pathUrl);
        }
        cache[pathUrl] = value;
        return {
          contents: value.content,
          pluginData: {
            parentUrl: value.url,
          },
        };
      });
      build.onResolve({ namespace: UnpkgNamepsace, filter: /.*/ }, async (args) => {
        return {
          namespace: UnpkgNamepsace,
          path: args.path,
          pluginData: args.pluginData,
        };
      });
    },
  };
};

// 使用方式
import react from 'react'; //会自动在编译器转换为 import react from 'https://unpkg.com/react'

Как видно из приведенных выше примеров, конструкция виртуального модуля esbuild очень гибкая и мощная Когда мы используем виртуальный модуль, вся структура нашей модульной системы фактически становится следующей: Не удалось скопировать загружаемый контент Для разных сценариев мы можем выбрать разные пространства имен для объединения.

  • Локальная разработка: полная загрузка локальных файлов, то есть всех пространств имен файлов.
  • Локальная разработка без установки node_modules: аналогично deno и snowpackstreaming importВ этом сценарии вы можете использовать пространство имен файлов для бизнес-файлов и пространство имен unpkg для файлов node_modules, что больше подходит для сценариев, где установка всех node_modules происходит слишком медленно при разработке проекта для сверхбольшого монорепозитория.
  • Сценарий веб-компиляции в реальном времени (проблемы с производительностью и сетью): то есть сторонняя библиотека исправлена, а бизнес-код может измениться, поэтому локальный файл и node_modules переходят в memfs.
  • Динамическая компиляция на веб-стороне: то есть сценарий веб-сайта интрасети.В это время сторонняя библиотека и бизнес-код не исправлены, затем локальный файл отправляется в memfs, а node_modules отправляется в unpkg для динамического извлечения

Мы обнаружили, что универсальный упаковщик, задействованный в виртуальном модуле, очень гибкий и может гибко реагировать на различные бизнес-сценарии, а накладные расходы между каждым сценарием не влияют друг на друга.

universal bundler

Большинство сборщиков запускаются в браузере по умолчанию, поэтому самая большая трудность в создании универсального упаковщика состоит в том, чтобы заставить его работать в браузере. В отличие от нашего локального сборщика, сборщик в браузере имеет много ограничений.Давайте рассмотрим проблемы, которые необходимо решить, если сборщик портирован на браузер.

rollup

Прежде всего, нам нужно выбрать подходящий упаковщик, который поможет нам завершить работу над сборкой, Rollup — очень хороший упаковщик, а у rollup есть много очень хороших свойств.

  • Поддержка встряхивания деревьев очень хороша, а также поддерживает встряхивание деревьев js.
  • Богатые хуки плагинов с очень гибкими возможностями настройки
  • Поддержка работы в браузере
  • Поддержка нескольких форматов вывода (esm, cjs, umd, systemjs)

Формально из-за вышеперечисленных замечательных функций многие из новейших инструментов пакетирования |viteиwmr, должен сказать, что писать плагины для роллапа гораздо удобнее, чем писать плагины для вебпака. Наш ранний универсальный упаковщик фактически был разработан на основе свертки, но мы столкнулись со многими проблемами в процессе использования свертки.

Проблемы совместимости с CommonJS

Для студентов, которые используют rollup для объединения в реальном бизнесе, плагин, которого нельзя избежать, — это rollup-plugin-commonjs, потому что rollup изначально поддерживает только пакеты модулей ESM, поэтому, если вам нужно связать commonjs в реальном бизнесе, первый шаг Необходимо преобразовать CJS в ESM.К сожалению, проблема взаимодействия Commonjs и модуля ES является очень сложной проблемой (ищите проблемы взаимодействия с помощью таких инструментов, как babel, rollup, typescript и т. д.)так не дает покоя A.GitHub.IO/inter op-special…, существует естественный разрыв между двумя семантиками Преобразование ESM в Commonjs, как правило, не является большой проблемой (будьте осторожны, чтобы избежать проблемы с экспортом по умолчанию), но преобразование CJS в ESM имеет больше проблем. Несмотря на то, что rollup-plugin-commonjs вложил много усилий в cjs2esm, по-прежнему остается много пограничных случаев.На самом деле, rollup также переписывает основной модуль.GitHub.com/roll-up/plug…Вот некоторые типичные проблемы

проблема с круговыми ссылками

Поскольку модуль экспорта commonjs не является живой привязкой, как только появится циклическая ссылка commonjs, будет проблемой преобразовать его в esm.

Проблема подъема динамических требований

Синхронизированный динамический запрос вряд ли можно преобразовать в esm.Если он преобразуется в импорт верхнего уровня, согласно семантике импорта, упаковщик должен поднять содержимое синхронизированного требования, но это противоречит синхронизированному требованию, поэтому динамическое требование также трудная сделка с

Гибрид CJS и ESM

Поскольку не существует стандартной спецификации для семантики смешивания ESM и CJS в модуле, хотя webpack поддерживает смешивание CJS и ESM (уменьшение до времени выполнения веб-пакета) в модуле, накопительный пакет отказывается от поддержки такого поведения (последняя версия. Его можно включить условно). , эффекта не пробовала)

проблемы с производительностью

Именно из-за сложности cjs2esm алгоритм конвертации очень сложен.В результате, как только в бизнесе будет много cjs-модулей, производительность компиляции rollup резко упадет.Это может не быть большой проблемой при компиляции некоторых библиотек, но используется для развития крупного бизнеса, скорость компиляции неприемлема.

Преобразование cjs в esm в браузере

С другой стороны, хотя rollup можно легко портировать в memfs, rollup-plugin-commonjs трудно портировать в Интернет, поэтому мы можем полагаться только на онлайн-cjs2esm, такой как skypack, для создания веб-связки на основе rollup в первые дни. для завершения вышеуказанного преобразования, но у большинства этих сервисов серверная часть реализована через rollup-plugin-commonjs, поэтому первоначальные проблемы объединения не были устранены, и есть дополнительные сетевые накладные расходы, и трудно иметь дело с необработкой. модулей cjs в node_modules. К счастью, esbuild использует решение, отличное от решения rollup.Он использует оболочку модуля, подобную узлу, для совместимости с cjs и вводит очень маленькую среду выполнения для поддержки cjs (на самом деле webpack использует решение для среды выполнения, совместимое с cjs, но его время выполнения недостаточно лаконичное). ...).Он лучше совместим с cjs, полностью отказавшись от поддержки cjs tree shake, и в то же время может напрямую заставить веб-бандер поддерживать cjs без внедрения плагинов.

поддержка виртуальных модулей

Поддержка виртуального модуля агрегации относительно хакерская.Перед путем зависимости пишется '\0', что навязчиво для пути и не очень удобно для некоторых сценариев ffi (строка C++ рассматривает '\0' как терминатор) , когда При работе с более сложными сценариями виртуальных модулей путь '\0' очень удобен для решения проблем.

filesystem

Локальный упаковщик — это локальная файловая система, к которой осуществляется доступ, но в браузере нет локальной файловой системы, поэтому доступ к файлам обычно достигается путем реализации сборщика, независимого от конкретной fs, и весь доступ к файлам может быть настроен через fs для доступ.rollupjs.org/repl/То есть таким образом. Поэтому нам нужно только заменить логику загрузки модуля из fs в memfs в браузере, а хук onLoad можно использовать для замены логики чтения файла.

node module resolution

Когда мы переключаем доступ к файлу на memfs, последующая проблема заключается в том, как получить фактический формат пути, соответствующий идентификатору требования и импорта.Алгоритм сопоставления идентификатора с реальным адресом файла в узле:module resolution, реализация алгоритма более сложная и необходимо учитывать следующие условия Подробный алгоритм см.Специальности.byte dance.net/articles/69…

  • файл|индекс|каталог три случая
  • js, json, аддон многофайловый суффикс
  • Разница между загрузчиком esm и cjs
  • обработка основного поля
  • обработка условного экспорта
  • exports subpath
  • NODE_PATH обработка
  • Поиск вверх рекурсивно
  • Обработка символических ссылок

В дополнение к сложности самого разрешения модуля узла нам также может потребоваться рассмотреть резервный вариант файла основного модуля, поддержку псевдонимов, поддержку ts и других суффиксов и другие функции, которые Webpack дополнительно поддерживает, но более популярен в сообществе, а также инструменты управления пакетами, такие как поскольку пряжа | pnpm | npm совместимы и другие проблемы. Внедрение этого набора алгоритмов с нуля обходится дорого, и алгоритм разрешения модуля узла был обновлен, веб-пакетenhanced-resolveМодуль в основном реализует вышеуказанные функции и поддерживает пользовательские fs, которые можно легко портировать в memfs.

Я думаю, что алгоритм узла здесь немного переработан и неэффективен (куча резервной логики имеет много накладных расходов на ввод-вывод), и это также является основной причиной распространенности hoist, источник всех зол, может быть, голый импортировать с помощью карты импорта или deno | Путь отображения golang лучше.

main field

Основное поле также является относительно сложной проблемой, в основном потому, что нет единого набора спецификаций, а библиотеки сообщества не полностью соответствуют спецификациям, что в основном связано с распространением пакетов, за исключением того, что основное поле официально поддерживается nodejs. , модуль, браузер, браузер и другие поля, различные сборщики и сторонние библиотеки сообщества не достигли соглашения, такие как

  • Как настроить записи cjs и esm, esnext и es5, node и browser, dev и prod
  • Код в модуле | main должен быть es5 или esnext (определяет, должен ли код в node_module проходить через трансформатор)
  • Должен ли код в модуле указывать на реализацию браузера или реализацию узла (определяет сборщик узлов).

и проблема приоритета main и module в случае сборщика браузера)

  • Как распределить код на разницу между узлом и браузером и т.д.

unpkg

Далее нам нужно разобраться с модулями node_modules.На данный момент есть два способа.Первый – смонтировать полные node_modules в memfs, а затем использовать advanced-resolve для загрузки соответствующих модулей в memfs.Другой способ – используйте для unpkg, преобразуйте идентификатор node_modules в запрос unpkg. Оба метода имеют свои применимые сценарии Первый подходит для фиксированного количества сторонних модулей (если оно не фиксировано, memfs не должна иметь возможность нести бесконечные модули node_modules), а скорость доступа memfs намного выше, чем доступ по сетевым запросам, поэтому он очень подходит для построения системы. Второй тип применим к переменному количеству сторонних модулей, и нет очевидных требований к скорости компиляции в реальном времени.Это больше подходит для веб-сценариев, таких как codeandbox, и предприятия могут самостоятельно выбирать нужные им npm-модули.

прокладки и полифиллы

Еще одна проблема, с которой сталкивается веб-бандер, заключается в том, что большинство модулей сообщества разрабатываются вокруг узла, который в значительной степени зависит от собственного API-интерфейса узла, но эти API-интерфейсы не поддерживаются в браузере, поэтому эти модули запускаются непосредственно в браузере. быть проблемы. Есть два случая

  • Во-первых, эти модули на самом деле зависят от некоторых API-интерфейсов узла, таких как утилиты, путь и т. д. Эти модули на самом деле не зависят от среды выполнения узла. В настоящее время мы можем фактически имитировать эти API-интерфейсы в браузере. Чтобы решить этот сценарий, он предоставляет большое количество полифилов node API в браузере, таких как path-browserify, stream-browserify и т. д.
  • Во-вторых, логика браузера и узла обрабатываются отдельно.Хотя код узла не нужно выполнять в браузере, не ожидается, что реализация узла увеличит размер пакета браузера и вызовет ошибки. , В это время нам нужен узел. Связанные модули могут быть обработаны извне.

Небольшой трюк, большинство внешних конфигураций упаковщика может быть громоздким или невозможным для изменения конфигурации упаковщика, нам нужно только обернуть требование в eval, большинство упаковщиков пропустят упаковку требуемого модуля. Например, eval('require')('os')

Polyfill и нюхание окружающей среды, битва между копьем и щитом

Polyfill и сниффинг окружения являются конкурирующими функциями. С одной стороны, polyfill максимально сглаживает разницу между узлом и браузером. С другой стороны, сниффинг окружения пытается максимально отличить окружения браузера и узла от различий. функции используются одновременно, нужны всевозможные хаки

webassembly

Наш бизнес опирается на модули C++, в локальной среде C++ можно скомпилировать в статическую библиотеку и вызвать через ffi, а в браузере для запуска нужно скомпилировать в webassembly, а большая часть wasm размер не маленький. , размер wasm esbuild составляет около 8 М, а объем файла wasm, скомпилированный нашей собственной статической библиотекой, также составляет около 3 М, что сильно влияет на общий размер пакета, поэтому из схемы разделения кода мы можем узнать, как разделить wasm, и первый доступ может использовать Полученный код делится на горячий код, а маловероятно используемый код делится на холодный код, что может уменьшить размер пакета, загружаемого в первый раз.

где мы можем использовать esbuild

esbuild имеет три вертикальные функции, которые можно использовать в комбинации или полностью независимо друг от друга.

  • minifier
  • transformer
  • bundler

Более эффективные инструменты регистрации и минимизации

Используя функцию преобразования esbuild, используйте esbuild-register для замены регистра ts-node фреймворка модульного тестирования, что значительно повышает скорость: см.GitHub.com/Ааа Праджня/ESB…, но ts-node теперь поддерживает пользовательский регистр, вы можете напрямую заменить register на esbuild-register, а минимизация производительности esbuild намного более чем краткая (более чем в 100 раз)

Более эффективные инструменты предварительной сборки

В некоторых сценариях комплектования, несмотря на то, что бизнес-код не входит в комплект, для предотвращения совместимости между водопадом и cjs сторонней библиотеки обычно необходимо предварительно собрать стороннюю библиотеку. инструмент prebundle В последней версии vite функция prebundle заменена с rollup на esbuild.

Лучший онлайн-сервис cjs2esm

Используйте esbuild для создания службы esm cdn: esm.sh выглядит следующим образом

node bundler

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

  • Уменьшен размер node_modules пользователя и ускорена скорость установки.По сравнению с установкой кучи зависимостей приложения ноды в node_modules бизнеса устанавливается только код бандла, что значительно сокращает объем установки бизнеса и ускоряет скорость установки.Twitter.com/PNP MJ is/stat…
  • Улучшается скорость холодного запуска, потому что связанный код уменьшает размер кода js, который фактически необходимо анализировать посредством встряхивания дерева (затраты на анализ js составляют большую часть скорости холодного запуска больших приложений, особенно приложений). которые чувствительны к скорости холодного запуска), с другой стороны, избегает файлового ввода-вывода, оба из которых значительно снижают скорость холодного запуска приложения, что очень подходит для некоторых сценариев, чувствительных к холодному запуску, таких как бессерверные
  • Избегайте семантического повреждения semver основной ветки. Хотя semver является набором норм сообщества, на самом деле он имеет очень строгие требования к коду. Когда вводится множество сторонних библиотек, трудно гарантировать, что зависимости основной ветки не нарушат семантику semver. Поэтому пакетный код Он может полностью избежать ошибок в восходящих зависимостях, которые приводят к ошибкам приложений, что очень важно для приложений с чрезвычайно высокими требованиями к безопасности (таких как компиляторы).

Поэтому я настоятельно рекомендую всем объединять приложения узлов, а esbuild обеспечивает готовую поддержку пакетов узлов.

альтернатива трансформатору tsc

Даже если tsc поддерживает инкрементную компиляцию, его производительность крайне тревожна Мы можем использовать esbuild вместо tsc для компиляции кода ts. (esbuid не поддерживает проверку типов ts и не планирует ее поддерживать), но если dev этап дела не сильно зависит от type checker, можно использовать esbuild вместо tsc на этапе dev. есть серьезные требования к проверке типов, вы можете обратить внимание на swc, swc использует ржавчину. Перепишите часть проверки типов tsc,GitHub.com/Коммерческий автомобиль-проект…

монорепозиторий против монотулов

esbuild — это редкий инструмент, который поддерживает как разработку библиотек, так и разработку приложений (поддержка библиотек webpack плохая, поддержка разработки накопительных приложений плохая), что означает, что вы можете полностью унифицировать инструменты сборки вашего проекта с помощью esbuild. esbuild изначально поддерживает разработку React, и скорость сборки чрезвычайно высока. Без какой-либо оптимизации, такой как пакетность, полный пакет занимает всего 80 мс (включая React, monaco-editor, Emotion, Mobx и многие другие библиотеки).Еще одним преимуществом этого является то, что очень удобно решать проблему компиляции общедоступных пакетов в вашем монорепозитории. Вам нужно только настроить основное поле esbuild на ['source','module','main'], а затем указать источник на запись исходного кода в вашей общедоступной библиотеке, esbuild сначала попытается скомпилировать исходный код вашего public library, скорость компиляции esbuild настолько высока, что компиляция общих библиотек вообще не повлияет на общую скорость вашего пакета 😁. Могу только сказать, что TSC не очень подходит для запуска компиляции, слишком медленный и слишком сложный.

Некоторые проблемы с esbuild

проблема с отладкой

Основной код esbuild написан на golang. Пользователь напрямую использует скомпилированный двоичный код и кучу JS-клея. Двоичный код практически не поддается отладке с помощью точек останова (отладка lldb|gdb). Вытащите код для перекомпиляции и отладки , требования к отладке выше и сложность выше

Поддерживает только цель для es6

Преобразователь esbuild в настоящее время поддерживает только цель es6, что мало влияет на стадию разработки, но в настоящее время в большинстве внутренних сценариев по-прежнему необходимо учитывать es5, поэтому продукт esbuild нельзя использовать в качестве конечного продукта, обычно он должен сотрудничать с babel | tsc | swc для преобразования es6 в es5

Производительность golang wasm имеет большие потери, чем родной, и пакет wasm больше,

В настоящее время производительность wasm, скомпилированного golang, не очень хорошая (падение производительности в 3-5 раз по сравнению с нативным), а пакет wasm, скомпилированный go, имеет большой объем (8M+), что не подходит для некоторых сценариев, которые чувствительны к объему упаковки.

Плагин API более оптимизирован

По сравнению с огромной поддержкой плагинов API для веб-пакетов и роллапов, esbuild поддерживает только два подключаемых хука, onLoad и onResolve. Хотя на основе этого можно сделать много работы, этого все еще не хватает. Например, post- обработка чанков после разделения кода не поддерживается.

欢迎关注「 字节前端 ByteFE 」简历投递联系邮箱「 tech@bytedance.com 」