Исследование использования Webpack

Webpack

причина

Я хочу изучить принцип работы Webpack, и обнаружил, что упоминается tapable, что меня смущает, тогда будем внимательно изучать, что это за библиотека?

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

image

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

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

начать исследование

Сначала создайте простейший проект

Как обычно, сначала создадим простейший проект:

image

Установите необходимые библиотеки:

npm install --save-dev wepback
npm install --save-dev webpack-cli
npm install --save-dev webpack-dev-server

npm install --save tapable

Мы пишем наш тестовый код под src, а затем запускаем его, чтобы увидеть результаты наших экспериментов. Конфигурация webpack.config.js выглядит следующим образом:

module.exports = {
  entry: {
    index: __dirname + "/src/index.js",
  },
  output: {
    path: __dirname + "/dist",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  devServer: {
    contentBase: "./dist",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  },
}

Настройте сценарий запуска в package.json и используйте сервер запуска npm для просмотра результатов выполнения:

"scripts": {
    "start": "webpack",
    "server": "webpack-dev-server --open"
},

Синхронизирующий хук

Первый крючок SyncHook

Адрес Tapable на github:GitHub.com/Веб-пакет/Taping…

Вот адрес ветки tapable-1, которую сейчас использует Webpack.

Согласно readme.md, tapable предоставляет множество классов Hook, которые могут помочь нам создавать крючки для плагинов.

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");

Так много хуков, давайте посмотрим, давайте посмотрим на Synchook, напишите под Index.js:

import { SyncHook } from 'tapable';

const hook = new SyncHook(); // 创建钩子对象
hook.tap('logPlugin', () => console.log('被勾了')); // tap方法注册钩子回调
hook.call(); // call方法调用钩子,打印出‘被勾了’三个字

Используйте сервер запуска npm для успешного запуска в браузере. Он также успешно печатает «галочку». Он по-прежнему очень прост в использовании.

Это классический механизм регистрации и запуска событий. На практике код объявления и срабатывания события обычно находится в одном классе, а код регистрации события — в другом классе (наш плагин). код показывает, как показано ниже:

// Car.js
import { SyncHook } from 'tapable';

export default class Car {
  constructor() {
    this.startHook = new SyncHook();
  }

  start() {
    this.startHook.call();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.startHook.tap('startPlugin', () => console.log('我系一下安全带'));
car.start();

Использование хуков в основном означает это.Car отвечает только за объявление и вызов хуков.Настоящая логика выполнения больше не в Car, а в index.js, где он зарегистрирован, вне Car. Это обеспечивает хорошую развязку.

Для автомобиля такой способ регистрации плагинов обогащает собственные функции.

Передать параметры плагину

Я хочу это:

// index.js
import Car from './Car';

const car = new Car();
car.accelerateHook.tap('acceleratePlugin', (speed) => console.log(`加速到${speed}`));
car.accelerate(100); // 调用时,将100传给插件回调的speed

Класс автомобиля можно записать так:

import { SyncHook } from 'tapable';

export default class Car {
  constructor() {
    this.startHook = new SyncHook();
    this.accelerateHook = new SyncHook(["newSpeed"]); // 在声明的时候,说明我这个Hook需要一个参数即可。
  }

  start() {
    this.startHook.call();
  }

  accelerate(speed) {
    this.accelerateHook.call(speed);
  }
}

На этом Hook завершается с параметрами.Параметр SyncHook предназначен для передачи массива, а это значит, что мы также можем передавать несколько параметров, например new SyncHook(["arg1","arg2","arg3"]). Таким образом, три параметра также могут быть переданы при вызове, и три параметра вызова также могут быть получены в функции обратного вызова.

Наш класс Car — это класс Tapable, объявление события и колл-центр.

Второй крючок SyncBailHook

У нас есть общее представление о механизме регистрации/вызова хука. SyncHook работает отлично, но tapable также предоставляет много хуков. Какие проблемы решают эти хуки?

Причина в том, что для события мы можем зарегистрироваться несколько раз следующим образом:

const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
car.hooks.brake.tap('brakePlugin2', () => console.log(`刹车2`));
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));

car.brake(); // 会打印‘刹车1’‘刹车2’‘刹车3’

Здесь мы добавляем хук hooks.brake и метод тормоза в класс Car. Хук тормоза регистрируется 3 раза, когда мы вызываем метод тормоза, все 3 плагина получают событие.

Мы немного переработали класс Car.Говорят, что такой способ написания больше соответствует передовой практике тапового использования.На самом деле, все хуки помещаются в поле хуков. Код автомобиля следующий:

import { SyncHook, SyncBailHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncHook(),
      accelerate: new SyncHook(["newSpeed"]),
      brake: new SyncBailHook(), // 这里我们要使用SyncBailHook钩子啦
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}

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

import Car from './Car';

const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
// 只需在不想继续往下走的插件return非undefined即可。
car.hooks.brake.tap('brakePlugin2', () => { console.log(`刹车2`); return 1; }); 
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));

car.brake(); // 只会打印‘刹车1’‘刹车2’

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

Предполагается, что tabable предоставляет различные хуки для управления взаимосвязью этих внешних плагинов.

Третий крючок SyncWaterfallHook

После того, как я понял второй хук, следующий хук легко понять.Вот определение SyncWaterfallHook: каждый его шаг зависит от результата выполнения предыдущего шага, то есть значение возврата предыдущего шага равно параметр следующего шага.

Преобразуем хук ускорителя в SyncWaterfallHook:

import { SyncHook, SyncBailHook, SyncWaterfallHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncHook(),
      accelerate: new SyncWaterfallHook(["newSpeed"]), // 重点在这里
      brake: new SyncBailHook(),
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.accelerate.tap('acceleratePlugin1', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin2', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin3', (speed) => { console.log(`加速到${speed}`); });

car.accelerate(50); // 打印‘加速到50’‘加速到150’‘加速到250’

Четвертый хук SyncLoopHook

Syncloophook - это синхронный циклический крючок, плагин которого возвращает неотказанное значение. Функция обратного вызова этого плагина будет выполнена до тех пор, пока она не возвращается undefined.

Мы изменили хук запуска на SyncLoopHook.

import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncLoopHook(), // 重点看这里
      accelerate: new SyncWaterfallHook(["newSpeed"]),
      brake: new SyncBailHook(),
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}
// index.js
import Car from './Car';

let index = 0;
const car = new Car();
car.hooks.start.tap('startPlugin1', () => {
  console.log(`启动`);
  if (index < 5) {
    index++;
    return 1;
  }
}); // 这回我们得到一辆破车,启动6次才会启动成功。

car.hooks.start.tap('startPlugin2', () => {
  console.log(`启动成功`);
});

car.start(); // 打印‘启动’6次,打印‘启动成功’一次。

асинхронный хук

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

Пятый хук AsyncParallelHook

AsyncParallelHook обрабатывает плагины, которые выполняются асинхронно параллельно.

Мы добавляем calculateRoutes в класс Car и используем AsyncParallelHook. Напишите другой метод calculateRoutes, который инициирует выполнение ловушки при вызове метода callAsync. Здесь вы можете передать обратный вызов, который будет вызван, когда все плагины закончат выполнение.

// Car.js
import {
  ...
  AsyncParallelHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      ...
      calculateRoutes: new AsyncParallelHook(),
    };
  }

  ...
  
  calculateRoutes(callback) {
    this.hooks.calculateRoutes.callAsync(callback);
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin1', (callback) => {
  setTimeout(() => {
    console.log('计算路线1');
    callback();
  }, 1000);
});

car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin2', (callback) => {
  setTimeout(() => {
    console.log('计算路线2');
    callback();
  }, 2000);
});

car.calculateRoutes(() => { console.log('最终的回调'); }); // 会在1s的时候打印‘计算路线1’。2s的时候打印‘计算路线2’。紧接着打印‘最终的回调’

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

Метод Promise для AsyncParallelHook

В дополнение к использованию tapAsync/callAsync используйте AsyncParallelHook. Вы также можете использовать подход tapPromise/обещание.

код показывает, как показано ниже:

// Car.js
import {
  SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook,
  AsyncParallelHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      ...
      calculateRoutes: new AsyncParallelHook(),
    };
  }

  ...

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');
      resolve();
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve();
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });

Разница только в использовании, а эффект такой же, как у tapAsync/callAsync.

Шестой хук AsyncParallelBailHook

Эта функция хука похожа на AsyncParallelHook, но после того, как хук, зарегистрированный первым плагином, будет выполнен, он сработает (плавится), а затем будет вызван окончательный обратный вызов, независимо от того, закончили ли выполнение другие плагины.

// Car.js
import {
  AsyncParallelBailHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      drift: new AsyncParallelBailHook(),
    };
  }

  drift(callback) {
    this.hooks.drift.callAsync(callback);
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.drift.tapAsync('driftPlugin1', (callback) => {
  setTimeout(() => {
    console.log('计算路线1');
    callback(1);
  }, 1000);
});

car.hooks.drift.tapAsync('driftPlugin2', (callback) => {
  setTimeout(() => {
    console.log('计算路线2');
    callback(2);
  }, 2000);

});

car.drift((result) => { console.log('最终的回调', result); });
// 打印结果是,等1s打印'计算路线1' ,马上打印‘最终的回调 1’,再到第2s,打印'计算路线2'

Я хотел бы поблагодарить пользователя Nuggets @小quilt за вопросы в комментариях, которые заставили меня понять, что мое предыдущее понимание было неверным. Я думал, что это основано на времени завершения выполнения плагина, а затем я пошел на github, чтобы отправить вопрос в веб-пакет, и получил ответ от автора веб-пакета.Они имели в виду, что исходя из времени регистрации плагина в качестве основы для предохранителя, первый зарегистрированный плагин После того, как выполнение будет завершено, он будет взорван. Вот мой вопрос на github:GitHub.com/Веб-пакет/Taping…

Тот же AsyncParallelBailHook также имеет метод вызова промиса, который похож на AsyncParallelHook, поэтому экспериментировать не будем.

Седьмой хук AsyncSeriesHook

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

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

// Car.js
import {
  AsyncSeriesHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesHook(),
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');

      resolve();
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve();
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1,再过2s(而不是到了第2s,而是到了第3s),打印计算路线2,再立马打印最终的回调。

Мы используем формат обещания прямо здесь и выполняем то же самое.

Восьмой хук AsyncSeriesBailHook

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

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

// Car.js
import {
  AsyncSeriesBailHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesBailHook(),
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');

      resolve(1);
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve(2);
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1,立马打印最终的回调,不会再执行计算路线2了。

Девятый хук AsyncSeriesWaterfallHook

Последовательное выполнение, и возвращаемое значение предыдущего плагина будет использоваться в качестве параметра последнего плагина.

код показывает, как показано ниже:

// Car.js
import {
  AsyncSeriesWaterfallHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesWaterfallHook(['home']), // 要标注一下,要传参数啦
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', (result) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1', result);

      resolve(1);
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', (result) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2', result);
      resolve(2);
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s过后,打印计算路线1 undefined,再过2s打印计算路线2 1,然后立马打印最终的回调。

Результат печати показан на рисунке:

image

Плагин пакета

Мы отдельно упакуем логику плагина регистрации следующим образом:

export default class CalculateRoutesPlugin {
  // 调用apply方法就可以完成注册
  apply(car) {
    car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin', (result) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('计算路线1', result);

          resolve('北京');
        }, 1000);
      });
    });
  }
}

В вызове index.js:

// index.js
import Car from './Car';
import CalculateRoutesPlugin from './CalculateRoutesPlugin';
const car = new Car();
const calculateRoutesPlugin = new CalculateRoutesPlugin();

calculateRoutesPlugin.apply(car); // 此节重点逻辑

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 运行正常,会打印'计算路线1'

Видя это, код похож на то, как используется Webpack, а машина похожа на компилятор/компиляцию в Webpack. index.js уподобляется работающему классу Webpack, использующему наш Car (аналог Compiler/Compilation) и использующему внедренный CalculateRoutesPlugin (аналог различных плагинов Webpack). Завершить упаковку.

Tapable

В readme Tapable не представлен класс Tapable, но его можно использовать следующим образом.

const {
  Tapable
} = require("tapable");
 
export default class Car extends Tapable {
    ...
}

Если вы посмотрите на исходный код tapable, вы не увидите этот класс, но переключитесь на ветку tapable-1, и вы сможете его увидеть.

В исходном коде Webpack Compiler и Compilation такие же, как Car выше, унаследованные от Tapable.

Тогда что именно сделал Tapable?Я посмотрел его исходный код и обнаружил, что он ничего не делает.Это признак того, что мой класс является классом, который может регистрировать плагины.

Хотя улучшений нет, у Автомобиля на данный момент есть два ограничения. следующим образом:

const car = new Car();
car.apply(); // 报错  Tapable.apply is deprecated. Call apply on the plugin directly instead
car.plugin(); // 报错 Tapable.plugin is deprecated. Use new API on `.hooks` instead

Эти два метода не разрешены.Я понимаю, что это ограничение для Webpack, напоминающее авторам плагинов обновить свои плагины и использовать последние практики.

Типы крючков

Использование хуков мы изучили выше, а затем сделаем некоторые выводы. Сначала поговорим о типе крючка.

Хуки разделены по логике выполнения зарегистрированных плагинов.

  1. Базовый крючок. Зарегистрированные плагины выполняются последовательно. Например, SyncHook, AsyncParallelHook, AsyncSeriesHook.

  2. Водопадный крючок. Возвращаемое значение предыдущего плагина является входным параметром последнего плагина. Например, SyncWaterfallHook, AsyncSeriesWaterfallHook.

  3. Залоговый крюк. Хук Bail относится к значению, которое подключаемый модуль возвращает не неопределенное, и не продолжает выполнять последующие подключаемые модули. Я так понимаю, что этот залог и есть смысл быстрого ухода. Например: SyncBailhook, AsyncSeriesbailhook

  4. Крючок-петля. Плагин вызывается в цикле до тех пор, пока возвращаемое значение плагина не станет неопределенным. Например, SyncLoopHook.

Различать крючки по времени

  1. Синхронный хук. Хук в начале синхронизации
  2. Асинхронный последовательный хук. Перехват в начале AsyncSeries.
  3. Асинхронный параллельный хук. Хук в начале AsyncParallel.

Перехватчик (Перехват)

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

car.hooks.calculateRoutes.intercept({
  call: (...args) => {
    console.log(...args, 'intercept call');
  }, // 插件被call时响应。
  //
  register: (tap) => {
    console.log(tap, 'ntercept register');

    return tap;
  },// 插件用tap方法注册时响应。
  loop: (...args) => {
    console.log(...args, 'intercept loop')
  },// loop hook的插件被调用时响应。
  tap: (tap) => {
    console.log(tap, 'intercept tap')
  } // hook的插件被调用时响应。
})

Контекст

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

myCar.hooks.accelerate.intercept({
	context: true, // 这里配置启用上下文对象
	tap: (context, tapInfo) => {
		if (context) { // 这里就可以拿到上下文对象
			context.hasMuffler = true;
		}
	}
});

myCar.hooks.accelerate.tap({
	name: "NoisePlugin",
	context: true
}, (context, newSpeed) => {
    // 这里可以拿到拦截器里的上下文对象,然后我们在插件里利用它的值做相应操作。
	if (context && context.hasMuffler) {
		console.log("Silence...");
	} else {
		console.log("Vroom!");
	}
});

Исходный код экспериментального проекта

адрес:git ee.com/dab UC это/он…

Запустите проект запуска NPM Install и NPM Run Server, см. информацию о печати в инструменте браузера.

заключительные замечания

Здесь изучается простое использование tapable. Он обеспечивает мощную поддержку механизма подключаемых модулей, который не только позволяет нам регистрировать различные подключаемые модули для основного тела (автомобиля), но также контролирует взаимосвязь между подключаемыми модулями и их соответствующее время.

Такую библиотеку целесообразнее использовать в Webpack. Webpack представляет собой набор плагинов. Через tapable плагины эффективно организованы и вызываются в разумное время.