웹팩 - 플러그인 메커니즘 잡기

22325 단어
시리즈 문장
Webpack 시리즈 - 첫 번째 기본 잡기 웹팩 시리즈 - 플러그인 메커니즘 잡기
전언
웹 패키지 자체가 어렵지 않다. 그가 완성한 각종 복잡하고 시크한 기능은 모두 그의 플러그인 메커니즘에 의존한다.아마도 우리는 일상적인 개발 수요에서 플러그인을 직접 쓸 필요가 없을 것이다. 그러나 그 중의 메커니즘을 이해하는 것도 일종의 학습 방향이다. 플러그인에 문제가 발생할 때 우리는 스스로 위치를 정할 수 있다.
Tapable
Webpack의 플러그인 메커니즘은 핵심 라이브러리인 Tapable에 의존합니다.웹 패키지의 플러그인 메커니즘에 깊이 들어가기 전에 이 핵심 라이브러리에 대해 어느 정도 알아야 한다.
Tapable가 뭐예요?
tapable는 nodejs와 유사한 Event Emitter의 라이브러리로 갈고리 함수의 발표와 구독을 제어합니다.물론tapable가 제공하는 hook 메커니즘은 비교적 전면적이고 동기화와 비동기화 두 가지 유형(비동기화에서 비동기화 병행과 비동기화 직렬로 구분됨)으로 나뉘며 이벤트 실행의 종료 조건에 따라Bail/Waterfall/Loop 유형이 파생된다.
Tapable 사용(이 소절 내용 인용 문장)
기본 사용:
const {
  SyncHook
} = require('tapable')

//        Hook,    
const hook = new SyncHook(['arg1', 'arg2'])

//   
hook.tap('a', function (arg1, arg2) {
	console.log('a')
})

hook.tap('b', function (arg1, arg2) {
	console.log('b')
})

hook.call(1, 2)

갈고리 종류:
BasicHook: 함수에 관심이 없는 모든 값을 실행합니다. SyncHook, AsyncParallelHook, AsyncSeries Hook이 있습니다.
BailHook: 순서대로 Hook을 실행하고 첫 번째 결과를 만나면result!==undefined는 되돌아와서 더 이상 실행하지 않습니다.SyncBailHook, AsyncSeriseBailHook, AsyncParallelBailHook이 있습니다.
어떤 장면에서 BailHook이 사용될까요?예를 들어 우리가 모듈 M을 가지고 있다고 가정하면 A 또는 B 또는 C 세 가지 조건을 충족시키면 단독으로 포장한다.여기서 A, B, C는 선후 순서가 존재하지 않습니다. 그러면 AsyncParallel Bail Hook으로 해결할 수 있습니다.
x.hooks.     Hook.tap('A', () => {
   if (A       ) {
     return true
   }
 })
 x.hooks.     Hook.tap('B', () => {
   if (B       ) {
     return true
   }
 })
 x.hooks.     Hook.tap('C', () => {
   if (C       ) {
     return true
   }
 })


만약 A에서true로 되돌아온다면 B와 C를 더 이상 판단할 필요가 없다.그러나 A, B, C의 검사가 선후 순서를 엄격히 따라야 할 때 순서가 있는 SyncBailHook(A, B, C가 동기 함수일 때 사용) 또는 AsyncSeriseBailHook(A, B, C가 비동기 함수일 때 사용)을 사용해야 한다.
Waterfall Hook: Reduce와 유사합니다. 만약 이전 Hook 함수의 결과가result!==undefined,result는 다음 훅 함수의 첫 번째 인자입니다.순서대로 집행된다면 Sync와 AsyncSeries 클래스에서만 이 Hook을 제공한다. SyncWaterfallHook, AsyncSeries WaterfallHook을 하나의 데이터로 삼아 A, B, C 세 단계의 처리를 거쳐 최종 결과를 얻어야 한다. 그리고 A에서 조건이 충족되면 a로 처리하고 그렇지 않으면 처리하지 않는다. B와 C가 같으면 다음과 같이 사용할 수 있다.
x.hooks.tap('A', (data) => {
   if (   A        ) {
     //      data
     return data
   } else {
     return
   }
 })
x.hooks.tap('B', (data) => {
   if (  B       ) {
     //      data
     return data
   } else {
     return
   }
 })
 x.hooks.tap('C', (data) => {
   if (   C        ) {
     //      data
     return data
   } else {
     return
   }
 })

LoopHook: 모든 함수 결과result===undefined가 나올 때까지 끊임없이 Hook을 실행합니다.마찬가지로 직렬성에 의존하기 때문에 SyncLoopHook과 AsyncSeriseLoopHook만 있습니다.
Tapable의 원본 분석
Tapable의 기본 논리는 먼저 클래스 실례의tap 방법을 통해 훅에 대응하는 처리 함수를 등록하는 것이다. 여기서sync동기갈고리의 주요 절차를 직접 분석하고 다른 비동기갈고리와 차단기 등은 군말하지 않는다.
const hook = new SyncHook(['arg1', 'arg2'])

이 코드는 원본 분석의 입구로서
class SyncHook extends Hook {
    //     ,           
	tapAsync() {
		throw new Error("tapAsync is not supported on a SyncHook");
	}
    //     ,       promise  
	tapPromise() {
		throw new Error("tapPromise is not supported on a SyncHook");
	}
    //     
	compile(options) {
		factory.setup(this, options);
		return factory.create(options);
	}
}

클래스 SyncHook에서 볼 수 있듯이 그는 하나의 클래스 Hook에 계승되었다. 그의 핵심 실현compile 등은 다음에 이야기할 것이다. 우리는 먼저 클래스 Hook을 살펴보자.
//       
constructor(args) {
	if (!Array.isArray(args)) args = [];
	this._args = args;
	this.taps = [];
	this.interceptors = [];
	this.call = this._call;
	this.promise = this._promise;
	this.callAsync = this._callAsync;
	this._x = undefined;
}

초기화가 완료되면 일반적으로 다음과 같은 이벤트가 등록됩니다.
//   
hook.tap('a', function (arg1, arg2) {
	console.log('a')
})

hook.tap('b', function (arg1, arg2) {
	console.log('b')
})

이 두 문장은 모두 기본 클래스의 tap 방법을 호출할 것이 분명하다.
tap(options, fn) {
    //     
	if (typeof options === "string") options = { name: options };
	if (typeof options !== "object" || options === null)
		throw new Error(
			"Invalid arguments to tap(options: Object, fn: function)"
		);
	options = Object.assign({ type: "sync", fn: fn }, options);
	if (typeof options.name !== "string" || options.name === "")
		throw new Error("Missing name for tap");
	//       register  ,        
	options = this._runRegisterInterceptors(options);
	//       
	this._insert(options);
}

위의 원본 코드를 분석하면insert 방법은 등록 단계의 관건적인 함수로 이 방법 내부에 직접 들어간다
_insert(item) {
    //            
	this._resetCompilation();
	//           taps  
	let before;
	if (typeof item.before === "string") before = new Set([item.before]);
	else if (Array.isArray(item.before)) {
		before = new Set(item.before);
	}
	let stage = 0;
	if (typeof item.stage === "number") stage = item.stage;
	let i = this.taps.length;
	while (i > 0) {
		i--;
		const x = this.taps[i];
		this.taps[i + 1] = x;
		const xStage = x.stage || 0;
		if (before) {
			if (before.has(x.name)) {
				before.delete(x.name);
				continue;
			}
			if (before.size > 0) {
				continue;
			}
		}
		if (xStage > stage) {
			continue;
		}
		i++;
		break;
	}
	this.taps[i] = item;
}
}

_sert는 주로 tap을 정렬하고 taps 수조에 넣는다. 정렬의 알고리즘은 특별히 복잡하지 않다. 여기서는 군말하지 않는다. 여기에 이르면 등록 단계는 이미 끝났다. 트리거 단계를 계속 본다.
hook.call(1, 2)  //     

기본 훅에는 초기화 과정이 있습니다.
this.call = this._call; 

Object.defineProperties(Hook.prototype, {
	_call: {
		value: createCompileDelegate("call", "sync"),
		configurable: true,
		writable: true
	},
	_promise: {
		value: createCompileDelegate("promise", "promise"),
		configurable: true,
		writable: true
	},
	_callAsync: {
		value: createCompileDelegate("callAsync", "async"),
		configurable: true,
		writable: true
	}
});

우리가 볼 수 있는call은createCompileDelegate에서 생성된 것으로 아래를 보십시오
function createCompileDelegate(name, type) {
	return function lazyCompileHook(...args) {
		this[name] = this._createCall(type);
		return this[name](...args);
	};
}

createCompiledelegate는lazyCompileHook이라는 함수를 되돌려줍니다. 말 그대로 컴파일을 게을리 하고 콜을 호출할 때까지 콜 함수를 컴파일합니다.
createCompileDelegate도 호출됨createCall 및createCall에서 Compier 함수를 호출했습니다.
_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}  
compile(options) {
	throw new Error("Abstract: should be overriden");
}

compiler는 하위 클래스에서 다시 써서syncHook의compile 함수, 즉 우리가 처음에 말한 핵심 방법으로 되돌아가야 한다는 것을 볼 수 있다.
class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
    ...
	compile(options) {
		factory.setup(this, options);
		return factory.create(options);
	}
}

관건은 SyncHookCodeFactory와 공장류HookCodeFactory입니다. setup 함수를 먼저 보고
setup(instance, options) {
  //    instance  syncHook   ,      tap            _x   .
  instance._x = options.taps.map(t => t.fn);
}

그 다음에 가장 관건적인create 함수입니다. 마지막으로 되돌아오는 fn을 볼 수 있습니다. 사실은 new Function 동적 생성 함수입니다.
create(options) {
  //      ,  options    this.options,  new Hook(["options"])        this._args
  this.init(options);
  let fn;
  //       ,      ,   ,   , promise
  switch (this.options.type) {
    //     
    case "sync":
      //           
      fn = new Function(
        //        ,no before no after         xxx,xxx  
        //     this.args         ,
        //        options
        this.args(),
        '"use strict";
'
+ this.header() + this.content({ onError: err => `throw ${err};
`, onResult: result => `return ${result};
`, onDone: () => "", rethrowIfPossible: true }) ); break; case "async": fn = new Function( this.args({ after: "_callback" }), '"use strict";
'
+ this.header() + // content content , // , this.callTapsSeries() this.content({ onError: err => `_callback(${err});
`, onResult: result => `_callback(null, ${result});
`, onDone: () => "_callback();
"
}) ); break; case "promise": let code = ""; code += '"use strict";
'
; code += "return new Promise((_resolve, _reject) => {
"
; code += "var _sync = true;
"
; code += this.header(); code += this.content({ onError: err => { let code = ""; code += "if(_sync)
"
; code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));
`; code += "else
"
; code += `_reject(${err});
`; return code; }, onResult: result => `_resolve(${result});
`, onDone: () => "_resolve();
"
}); code += "_sync = false;
"
; code += "});
"
; fn = new Function(this.args(), code); break; } // init undefined // this.options = undefined; // this._args = undefined; this.deinit(); return fn; }

마지막으로 생성된 코드는 대체로 다음과 같다.
"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
//                     
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[1].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[2].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[3].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
}

ok, 이상은 Tapabled의 메커니즘이지만 본 편의 주요 대상은 Tapable를 바탕으로 이루어진compile와compilation 대상이다.그러나 이들은 모두tapable에 기반을 두고 있기 때문에 소개된 편폭은 상대적으로 짧다.
compile
compile
compiler 대상은 완전한 웹 패키지 환경 설정을 대표합니다.이 대상은 웹 패키지를 시작할 때 한꺼번에 만들어지고 옵션,loader,plugin을 포함한 모든 조작 가능한 설정을 설정합니다.웹 패키지 환경에서 플러그인을 사용할 때, 플러그인은 이compiler 대상의 인용을 받을 것입니다.compiler를 사용하여 웹 패키지의 주 환경에 접근할 수 있습니다.
즉, compile는 웹 패키지의 전체적인 환경이다.
compile의 내부 구현
class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      /** @type {SyncBailHook} */
      shouldEmit: new SyncBailHook(["compilation"]),
      /** @type {AsyncSeriesHook} */
      done: new AsyncSeriesHook(["stats"]),
      /** @type {AsyncSeriesHook<>} */
      additionalPass: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook} */
      ......
      ......
      some code
    };
    ......
    ......
    some code
}

보시다시피Compier는Tapable를 계승하고 실례에 훅 대상을 연결하여Compier의 실례compier를 이렇게 사용할 수 있습니다
compiler.hooks.compile.tapAsync(
  'afterCompile',
  (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     plugin API       
    compilation.addModule(/* ... */);

    callback();
  }
);

compilation
compilation 소개
compilation 대상은 자원 버전 구축을 대표합니다.웹 패키지 개발 환경 중간부품을 실행할 때 파일 변화가 감지될 때마다 새로운compilation을 만들어서 새로운 컴파일 자원을 생성합니다.한compilation 대상은 현재의 모듈 자원, 컴파일 생성 자원, 변화된 파일, 추적에 의존하는 상태 정보를 나타낸다.compilation 대상도 플러그인을 사용자 정의 처리할 때 선택할 수 있도록 중요한 시기에 리셋을 제공합니다.
compilation의 실현
class Compilation extends Tapable {
	/**
	 * Creates an instance of Compilation.
	 * @param {Compiler} compiler the compiler which created the compilation
	 */
	constructor(compiler) {
		super();
		this.hooks = {
			/** @type {SyncHook} */
			buildModule: new SyncHook(["module"]),
			/** @type {SyncHook} */
			rebuildModule: new SyncHook(["module"]),
			/** @type {SyncHook} */
			failedModule: new SyncHook(["module", "error"]),
			/** @type {SyncHook} */
			succeedModule: new SyncHook(["module"]),

			/** @type {SyncHook} */
			addEntry: new SyncHook(["entry", "name"]),
			/** @type {SyncHook} */
		}
	}
}

위에서 언급한compiler 실현을 구체적으로 참고하세요.
플러그인 작성
tapable\compiler\compilation을 알게 된 후에 플러그인의 실현을 보면 더 이상 안개가 끼지 않습니다. 아래 코드는 공식 문서에서 유래한 것입니다.
class MyExampleWebpackPlugin {
  //    `apply`   
  apply(compiler) {
    //             
    compiler.hooks.compile.tapAsync(
      'afterCompile',
      (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     plugin API       
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

이를 통해 알 수 있듯이 apply에 하나의Compiler 실례를 전송한 다음에 이 실례를 바탕으로 이벤트를 등록하고compilation는 같은 이치를 가진다. 마지막으로 웹 패키지는 각 절차에서call 방법을 실행한다.
compiler와compilation 비교적 중요한 이벤트 갈고리
compier
이벤트 훅
촉발 시기
매개 변수
타입
entry-option
초기화 옵션
-
SyncBailHook
run
컴파일 시작
compiler
AsyncSeriesHook
compile
컴파일링을 시작하기 전에
compilation
SyncHook
compilation
compilation 대상이 생성되어 이 대상을 조작할 수 있습니다
compilation
SyncHook
make
entry부터 귀속 분석 의존, 각 모듈에build 준비
compilation
AsyncParallelHook
after-compile
컴파일build 프로세스 종료
compilation
AsyncSeriesHook
emit
메모리에 assets 내용을 디스크 폴더에 쓰기 전에
compilation
AsyncSeriesHook
after-emit
메모리에 assets 내용을 디스크 폴더에 쓴 후
compilation
AsyncSeriesHook
done
모든 컴파일 프로세스 완료
stats
AsyncSeriesHook
failed
컴파일 실패 시
error
SyncHook
compilation
이벤트 훅
촉발 시기
매개 변수
타입
normal-module-loader
일반 모듈loader, 모듈 맵 (graph) 의 모든 모듈의 함수를 불러옵니다.
loaderContext module
SyncHook
seal
컴파일 (compilation) 이 새 모듈을 수신하지 않을 때 터치합니다.
-
SyncHook
optimize
최적화 단계가 시작될 때 터치합니다.
-
SyncHook
optimize-modules
모듈의 최적화
modules
SyncBailHook
optimize-chunks
최적화
chunks
SyncBailHook
additional-assets
컴파일 (compilation) 을 위한 추가 자원 (asset) 을 만듭니다.
-
AsyncSeriesHook
optimize-chunk-assets
모든 chunk 자원 (asset) 을 최적화합니다.
chunks
AsyncSeriesHook
optimize-assets
compilation에 저장을 최적화합니다.assets의 모든 자원 (asset)
assets
AsyncSeriesHook
총결산
플러그인 메커니즘은 복잡하지 않고 웹팩도 복잡하지 않습니다. 복잡한 것은 플러그인 자체...또 절차를 먼저 써야 하는데 절차를 뒤에 보충할 수밖에 없다.
인용하다
시리즈만 사용하는 것에 만족하지 않습니다: tapable 웹팩 시리즈의 2 Tapable는 플러그인을 작성합니다 Compiler Compilationcompiler와comnpilation 갈고리로 진정한 Webpack 플러그인을 똑똑히 보십시오

좋은 웹페이지 즐겨찾기