Writing a Plugin

플러그인은 서드파티 개발자들에게 webpack 엔진의 모든 잠재력을 보여줍니다. 단계별로 빌드 callback을 사용하여 개발자들은 webpack의 빌드 프로세스에 자신만의 행동을 도입할 수 있습니다. 플러그인을 빌드하는 것은 로더를 빌딩 하는 것보다 조금 더 진보되었습니다. 왜냐하면, 플러그인은 몇몇 webpack의 low-level에 내장된 훅을 이해할 필요가 있기 때문입니다. 이제 소스 코드를 읽어볼 준비를 해봅시다!

Creating a Plugin

Webpack용 plugin은 다음과 같이 구성됩니다.

  • JavaScript function 또는 JavaScript class로 이름을 붙입니다.
  • 프로토타입에서 apply 메소드를 정의합니다.
  • 특정한 이벤트 훅으로 지정합니다.
  • webpack 내부에 있는 인스턴스의 특정한 데이터를 처리합니다.
  • 기능적으로 완벽해지면 webpack에서 제공하는 callback을 호출합니다.
// A JavaScript class.
class MyExampleWebpackPlugin {
  // `apply`를 compiler를 인자로 받는 프로토타입 메소드로 정의합니다.
  apply(compiler) {
    // attach할 이벤트 훅을 지정합니다.
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log(
          'Here’s the `compilation` object which represents a single build of assets:',
          compilation
        );

        // Webpack에서 제공되는 플러그인 API를 사용하여 빌드를 처리합니다.
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

Basic plugin architecture

플러그인은 apply라는 프로토타입 메소드로 인스턴스화 된 객체입니다. 이러한 apply 메소드는 플러그인을 설치하는 동안 webpack 컴파일러에 의해 한번 호출됩니다. apply 메소드는 컴파일러 callback에 대해 접근 권한을 부여하는 기본 webpack 컴파일러에 의해 참조됩니다. 플러그인의 구조는 다음과 같습니다.

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (stats /* 훅의 탭이 끝나면 stats는 인자로 통과됩니다.*/) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

그런 다음에 플러그인을 사용하려면 webpack의 설정인 plugins 배열에 인스턴스를 포함해야합니다.

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... 여기서 구성을 설정할 것 ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

플러그인 옵션을 통해 전달되는 옵션의 유효성을 확인하려면 schema-utils를 사용하세요. 다음은 사용 예제입니다.

import { validate } from 'schema-utils';

// options 객체에 대한 schema
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};

export default class HelloWorldPlugin {
  constructor(options = {}) {
    validate(schema, options, {
      name: 'Hello World Plugin',
      baseDataPath: 'options',
    });
  }

  apply(compiler) {}
}

Compiler and Compilation

플러그인 개발 중 가장 중요한 두 개의 리소스는 compilercompilation 객체입니다. 두 가지의 역할을 이해하는 것은 webpack 엔진을 확장하는 첫 번째 단계입니다.

class HelloCompilationPlugin {
  apply(compiler) {
    // callback 함수에 인자로 compilation을 제공하는 훅으로 compliation 훅을 탭 합니다.
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // 이제 컴파일을 통해 이용할 수 있는 다양한 훅들을 이용할 수 있습니다.
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('Assets are being optimized.');
      });
    });
  }
}

module.exports = HelloCompilationPlugin;

compiler, compilation 그리고 다른 중요한 객체에서 사용 가능한 훅들은 플러그인 API 문서를 참고해 주세요.

Async event hooks

일부 플러그인 훅은 비동기입니다. 이를 탭 하려면, 동기로 동작하는 tap 메소드를 사용하거나 비동기 메소드인 tapAsync 또는 tapPromise 메소드를 사용할 수 있습니다.

tapAsync

tapAsync 메소드를 사용하여 플러그인을 탭해야 할 경우 함수에서 마지막 인자로 제공되는 callback 함수를 호출해야 할 필요가 있습니다.

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // Do something async...
        setTimeout(function () {
          console.log('Done with async work...');
          callback();
        }, 1000);
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

tapPromise

플러그인에 탭 하기 위해 tapPromise를 사용할 때, 동기식 작업이 완료될 때의 해석 promise를 반드시 반환해야 할 필요가 있습니다.

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // 끝났을 때 해석을 반환해야 합니다..
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('Done with async work...');
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = HelloAsyncPlugin;

Example

일단 webpack compiler와 각각의 개별적인 compilations를 이해 할 수 있게 되면, 엔진 자체로 할 수 있는 가능성이 무궁무진해집니다. 기존 파일을 다시 포맷하거나 파생 파일을 만들거나 완전히 새로운 애셋을 만들 수 있습니다.

새로운 빌드 파일인 assets.md를 생성하는 예제를 작성해봅시다. 내용은 빌드에 있는 모든 애셋 파일을 나열합니다. 이 플러그인은 다음과 같습니다.

class FileListPlugin {
  static defaultOptions = {
    outputFile: 'assets.md',
  };

  // 어떤 옵션이 플러그인의 생성자에 전달될 때,
  // (이는 플러그인의 public API입니다.)
  constructor(options = {}) {
    // 기본 옵션에 대해 사용자 지정 옵션을 적용하고
    // 병합된 옵션을 플러그인 메소드에 추가로 사용할 수 있게 합니다.
    // 여기서 모든 옵션을 확인해야 합니다.
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }

  apply(compiler) {
    const pluginName = FileListPlugin.name;

    // webpack 모듈 인스턴스는 컴파일러 객체로부터 접근할 수 있으며,
    // 이는 사용되는 모듈의 정확한 버전을 보장합니다.
    // (webpack 또는 직접적으로 어떤 심볼을 require/import 하지 말아야 합니다.)
    const { webpack } = compiler;

    // Compilation 객체는 몇 가지 유용한 상수에 대한 참조를 제공합니다.
    const { Compilation } = webpack;

    // Rawsource는 compilation에서 애셋 소스를 나타내는데
    // 사용해야 하는 "sources" 클래스 중 하나입니다.
    const { RawSource } = webpack.sources;

    // 이전 단계에서 compilation 프로세스를 추가로 탭하기 위해서는
    // "thisCompilation" 훅을 탭해야합니다.
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // 특정 단계에서 애셋 처리 파이프라인으로 탭합니다.
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,

          // 이후 애셋 처리 단계 중 하나를 사용하여
          // 모든 애셋이 이미 다른 플러그인에 의해 compliation에 추가되었는지 확인합니다.
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // "assets"는 compilation의 모든 애셋을 포함하는 객체이고,
          // 객체의 key는 애셋의 경로 이름입니다.
          // 그리고 values 파일 소스입니다.

          // 모든 애셋을 거쳐 반복하고
          // 마크다운 파일로 내용을 생성합니다.
          const content =
            '# In this build:\n\n' +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join('\n');

          // 출력 디렉터리 webpack에 의해 자동으로 생성되도록
          // compilation에 새로운 애셋을 추가합니다.
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}

module.exports = { FileListPlugin };

webpack.config.js

const { FileListPlugin } = require('./file-list-plugin.js');

// 플러그인에 webpack 설정을 사용합니다.
module.exports = {
  // …

  plugins: [
    // 기본 옵션을 사용하여 플러그인을 추가합니다.
    new FileListPlugin(),

    // 또는

    // 지원되는 옵션을 전달하도록 선택할 수 있습니다.
    new FileListPlugin({
      outputFile: 'my-assets.md',
    }),
  ],
};

이렇게 하면 다음과 같은 이름을 가진 마크다운 파일이 생성됩니다.

# In this build:

- main.css
- main.js
- index.html

Different Plugin Shapes

플러그인은 탭 하는 이벤트 훅에 따라 타입으로 분류될 수 있습니다. 모든 이벤트 훅은 동기 또는 비동기 waterfall 또는 병렬 훅으로 사전에 정의되어 있고 훅은 내부적으로 call/callAsync 메소드를 사용하여 호출됩니다. 지원되거나 탭이 된 훅의 목록은 일반적으로 this.hooks 프로퍼티에 명시되어 있습니다.

예시는 다음과 같습니다.

this.hooks = {
  shouldEmit: new SyncBailHook(['compilation']),
};

이것은 SyncBailHook 타입의 훅인 shouldEmit만이 지원되는 훅이고 shouldEmit 훅에 탭 되는 플러그인에 전달되는 유일한 파라미터는 compilation입니다.

지원되는 다양한 훅의 타입은 다음과 같습니다.

Synchronous Hooks

  • SyncHook

    • new SyncHook([params])로 정의됩니다.
    • tap 메소드를 사용하여 탭 됩니다.
    • call(...params) 메소드를 사용하여 호출됩니다.
  • Bail Hooks

    • SyncBailHook[params]로 정의됩니다.
    • tap 메소드를 사용하여 탭 됩니다.
    • call(...params) 메소드를 사용하여 호출됩니다.

    Bail Hook 타입의 훅들은, 각각의 callback 플러그인이 특정한 args를 사용하여 차례로 호출됩니다. 만약 플러그인에 의해 정의되지 않은 값을 제외하고 값이 반환된다면 훅에 의해 값이 반환되고 추가 플러그인 callback은 호출되지 않습니다. 많은 유용한 optimizeChunks, optimizeChunkModules와 같은 이벤트는 SyncBailHooks입니다.

  • Waterfall Hooks

    • SyncWaterfallHook[params]로 정의됩니다.
    • tap 메소드를 사용하여 탭 됩니다.
    • call(...params) 메소드를 사용하여 호출됩니다.

    여기서 각각 플러그인은 이전 플러그인의 반환 값으로부터 인자들을 차례로 호출합니다. 플러그인은 실행 순서를 반드시 고려해야합니다. 실행된 이전의 플러그인으로부터 인자들을 반드시 허용해야합니다. 첫 번째 플러그인의 값은 init입니다. 따라서 waterfall hooks에는 최소 1개의 parameter를 제공해야 합니다. 이 패턴은 ModuleTemplate, ChunkTemplate 등과 같은 webpack 템플릿과 관련된 탭 될 수 있는 인스턴스에서 사용됩니다.

Asynchronous Hooks

  • Async Series Hook

    • AsyncSeriesHook[params]로 정의됩니다.
    • tap/tapAsync/tapPromise 메소드를 사용하여 탭 됩니다.
    • callAsync(...params) 메소드를 사용하여 호출됩니다.

    플러그인 핸들러 함수는 (err?: Error) -> void 서명이 있는 callback 함수와 모든 인자들이 호출됩니다. 핸들러 함수는 등록된 순서대로 호출 됩니다. callback은 모든 핸들러가 호출되고 난 뒤 호출됩니다. Async Series Hook은 또한 emit, run과 같은 이벤트에 흔히 사용되는 패턴입니다.

  • Async waterfall 플러그인은 waterfall 방식으로 비동기식으로 적용될 것입니다.

    • AsyncWaterfallHook[params]로 정의됩니다.
    • tap/tapAsync/tapPromise 메소드를 사용하여 탭 됩니다.
    • callAsync(...params) 메소드를 사용하여 호출됩니다.

    플러그인 핸들러 함수는 (err: Error, nextValue: any) -> void. 서명이 있는 callback 함수와 현재 값으로 호출됩니다. nextValue가 호출되면 다음 핸들러의 현재 값이 됩니다. 첫 번째 핸들러의 현재 값은 init입니다. 모든 핸들러가 적용된 후 callback은 마지막 값으로 호출됩니다. 만약 어떤 핸들러가 err 값을 전달하면 callback은 오류가 호출되고 더이상 핸들러는 호출되지 않습니다. 이 플러그인 패턴은 before-resolveafter-resolve와 같은 이벤트에서 사용할 수 있습니다.

  • Async Series Bail

    • AsyncSeriesBailHook[params]로 정의됩니다.
    • tap/tapAsync/tapPromise 메소드를 사용하여 탭 됩니다.
    • callAsync(...params) 메소드를 사용하여 호출됩니다.
  • Async Parallel

    • AsyncParallelHook[params]로 정의됩니다.
    • tap/tapAsync/tapPromise 메소드를 사용하여 탭 됩니다.
    • callAsync(...params) 메소드를 사용하여 호출됩니다.

Configuration defaults

플러그인의 기본값이 적용된 후 webpack은 설정의 기본값을 적용합니다. 이를 통해 플러그인은 고유의 기본값을 제공하고 사전에 플러그인의 설정을 만드는 방법을 제공합니다.

10 Contributors

slavafomintbroadleynveenjainiamakulovbyzykfranjohn21EugeneHlushkosnitin315rahul3vjamesgeorge007

Translators