Webpack completes module conversion through Loader, making “everything module” possible. The Plugin mechanism makes it more flexible, allowing hooks to be called during the Webpack life cycle to accomplish various tasks, including modifying output resources, output directories, and so on.

Today we will learn how to write a Webpack plug-in [1].

The build process

Before writing the plug-in, you also need to understand the Webpack build process so that you can plug in the right plug-in logic at the right time. The basic build process for Webpack is as follows:

  1. Verifying configuration files
  2. Generating Compiler objects
  3. Initialize the default plug-in
  4. run/watch: Execute the watch method if it is running in watch mode, otherwise execute the run method
  5. compilation: creates the Compilation object to call back the Compilation related hooks
  6. emit: The file content is ready. Prepare the generated file. This is the last chance to modify the final file
  7. afterEmit: The file has been written to the disk
  8. doneFinish compiling

Plug-in sample

A typical Webpack plug-in looks like this:

// Plug-in code

class MyWebpackPlugin {

constructor(options) {

}



apply(compiler) {

// Insert the hook function in the emit phase

compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => {});

}

}



module.exports = MyWebpackPlugin;

Copy the code

Next you need to introduce this plug-in in webpack.config.js.

module.exports = {

plugins: [

// Pass in the plug-in instance

new MyWebpackPlugin({

param:'paramValue'

}),

]

};

Copy the code

After initializing the Compiler object, the Apply method of the plug-in instance will be called and the Compiler object will be passed in. The plug-in instance will register the hooks of interest in the Apply method. During the execution, Webpack will call back the corresponding hooks according to the construction stage.

Compiler && Compilation objects

The Compiler and Compilation objects provided by Webpack are the two most commonly used and most important objects in compiling Webpack plug-ins. The Compiler and Compilation objects are accessed by the Plugin to complete its work.

  • The Compiler object contains the current configuration for running Webpack, including entry, output, loaders, etc. This object is instantiated when Webpack is started and is globally unique. Plugin can use this object to obtain Webpack configuration information for processing.
  • Compilation objects understand compiled objects and contain information about modules, dependencies, files, and so on. When running Webpack in development mode, a new Compilation object is generated each time a file is modified, and the Plugin can access the modules, dependencies, file contents, and other information during the Compilation process.

Common hook

Webpack calls the corresponding hooks back and forth according to the execution flow. Let’s take a look at the common hooks and what tap operations they support.

hook instructions parameter type
afterPlugins Start a new compilation compiler synchronous
compile Before creating the compilation object compilationParams synchronous
compilation The compilation object has been created compilation synchronous
emit Resource generation is complete before output compilation asynchronous
afterEmit Resource output to directory is complete compilation asynchronous
done compile stats synchronous

Tapable

Tapable is a core tool for Webpack, and many objects in Webpack extend from the Tapable class. The Tapable class exposes the TAP, tapAsync, and tapPromise methods, and you can choose a function injection logic based on how the hook is synchronous/asynchronous.

  • Tap Sync hook
  • TapAsync Asynchronous hook, which tells Webpack that the asynchron has finished with a callback
  • TapPromise Asynchronous hook, which returns a Promise telling Webpack that the asynchro is done

tap

Tap is a synchronous hook. A synchronous hook cannot be used with asynchronous calls because the asynchronous logic may not complete when the function returns, causing problems.

Here is an example of inserting synchronous hooks in the compile phase.

compiler.hooks.compile.tap('MyWebpackPlugin', params => {

console.log('I'm a sync hook')

});

Copy the code

tapAsync

TapAsync is an asynchronous hook that we can use as a callback to tell Webpack that the asynchronous logic has finished executing.

Here is an example in the EMIT phase that prints a list of files after 1 second.

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {

setTimeout((a)= >{

console.log('File List'.Object.keys(compilation.assets).join(', '));

callback();

}, 1000);

});

Copy the code

tapPromise

TapPromise is also an asynchronous hook, and differs from tapAsync in that tapPromise informs Webpack that the asynchronous logic has finished by returning a Promise.

Here is an example of uploading the generated results to the CDN.

compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {

return new Promise((resolve, reject) = > {

const filelist = Object.keys(compilation.assets);

uploadToCDN(filelist, (err) => {

if(err) {

reject(err);

return;

}

resolve();

});

});

});

Copy the code

The general form for inserting hooks in the apply method is as follows:

Compileer.hooks. Stage. Tap function ('Plug-in name', (phase callback parameter) => {



});

Copy the code

Common operations

Read output resources, modules, and dependencies

During the EMIT phase, we can read the final resources, chunks, modules, and corresponding dependencies that need to be output, and change the output resources if necessary.

apply(compiler) {

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {

// compilation.chunks holds a list of code blocks

compilation.chunks.forEach(chunk= > {

// Chunk contains multiple modules. Chunk. modulesIterable allows you to iterate through the list of modules

for(const module of chunk.modulesIterable) {

// Module contains multiple dependencies, traversed by module.dependencies

module.dependencies.forEach(dependency= > {

console.log(dependency);

});

}

});

callback();

});

}

Copy the code

Modifying output Resources

By manipulating the compilation.assets object, we can add, delete, and change the final output of the resource.

apply(compiler) {

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation) => {

// Modify or add resources

compilation.assets['main.js'] = {

source() {

return 'modified content';

},

size() {

return this.source().length;

}

};

// Delete the resource

delete compilation.assets['main.js'];

});

}

Copy the code

The source method returns the contents of the resource, supporting strings and Node.js’s Buffer, and size returns the size of the file in bytes.

Plug-in sample

Next we start writing custom plug-ins, all using the following example project (requiring installation of Webpack and Webpack-CLI) :

|----src

|----main.js

|----plugins

|----my-webpack-plugin.js

|----package.json

|----webpack.config.js

Copy the code

The relevant documents are as follows:

// src/main.js

console.log('Hello World');

Copy the code
// package.json

{

"scripts": {

"build":"webpack"

}

}

Copy the code
const path = require('path');

const MyWebpackPlugin = require('my-webpack-plugin');



// webpack.config.js

module.exports = {

entry:'./src/main'.

output: {

path: path.resolve(__dirname, 'build'),

filename:'[name].js'.

},

plugins: [

new MyWebpackPlugin()

]

};

Copy the code

Generate manifest file

This is done by operating compilation.assets in the EMIT phase.

class MyWebpackPlugin {

apply(compiler) {

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {

const manifest = {};

for (const name of Object.keys(compilation.assets)) {

manifest[name] = compilation.assets[name].size();

// Write the file name and size of the generated file to the manifest object

}

compilation.assets['manifest.json'] = {

source() {

return JSON.stringify(manifest);

},

size() {

return this.source().length;

}

};

callback();

});

}

}



module.exports = MyWebpackPlugin;

Copy the code

Json will be added to the build directory as follows:

{"main.js":956}

Copy the code

The construction results were uploaded to Qiniu

In actual development, resource files are generally synchronized to THE CDN after construction, and the final front-end interface uses static resources on the CDN server.

Below we write a Webpack plug-in, file construction after the completion of the upload seven cattle CDN.

Our plug-in relies on Qiniu, so we need to install additional Qiniu modules

npm install qiniu --save-dev

Copy the code

The node. js SDK document address of Qiniu is as follows:

https://developer.qiniu.com/kodo/sdk/1289/nodejs

Copy the code

Start writing plug-in code:

const qiniu = require('qiniu');

const path = require('path');



class MyWebpackPlugin {

// 7 ox SDK MAC object

mac = null;



constructor(options) {

// Read the incoming options

this.options = options || {};

// Check the parameters in the options

this.checkQiniuConfig();

// Initializes the MAC object

this.mac = new qiniu.auth.digest.Mac(

this.options.qiniu.accessKey,

this.options.qiniu.secretKey

);

}

checkQiniuConfig() {

// QiniU is not sent

if (!this.options.qiniu) {

this.options.qiniu = {

accessKey: process.env.QINIU_ACCESS_KEY,

secretKey: process.env.QINIU_SECRET_KEY,

bucket: process.env.QINIU_BUCKET,

keyPrefix: process.env.QINIU_KEY_PREFIX || ' '

};

}

const qiniu = this.options.qiniu;

if(! qiniu.accessKey || ! qiniu.secretKey || ! qiniu.bucket) {

throw new Error('invalid qiniu config');

}

}



apply(compiler) {

compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {

return new Promise((resolve, reject) = > {

// Total uploads

const uploadCount = Object.keys(compilation.assets).length;

// Number of uploads

let currentUploadedCount = 0;

// Qiniu SDK parameters

const putPolicy = new qiniu.rs.PutPolicy({ scope: this.options.qiniu.bucket });

const uploadToken = putPolicy.uploadToken(this.mac);

const config = new qiniu.conf.Config();

config.zone = qiniu.zone.Zone_z1;

const formUploader = new qiniu.form_up.FormUploader()

const putExtra = new qiniu.form_up.PutExtra();

// The error object needs to be called back at the end because it is a batch upload

let globalError = null;



// Iterate over the compile resource file

for (const filename of Object.keys(compilation.assets)) {

// Start uploading

formUploader.putFile(

uploadToken,

this.options.qiniu.keyPrefix + filename,

path.resolve(compilation.outputOptions.path, filename),

putExtra,

(err) => {

console.log(`uploade ${filename} result: ${err ? `Error:${err.message}` : 'Success'}`)

currentUploadedCount++;

if (err) {

globalError = err;

}

if (currentUploadedCount === uploadCount) {

globalError ? reject(globalError) : resolve();

}

});

}

})

});

}

}



module.exports = MyWebpackPlugin;

Copy the code

The configuration needs to be passed to the plug-in in Webpack:

module.exports = {

entry: './src/index'.

target: 'node'.

output: {

path: path.resolve(__dirname, 'build'),

filename: '[name].js'.

publicPath: 'the CDN domain name'

},

plugins: [

new CleanWebpackPlugin(),

new QiniuWebpackPlugin({

qiniu: {

accessKey: 'seven cows AccessKey'.

secretKey: 'seven cows SecretKey'.

bucket: 'static'.

keyPrefix: 'webpack-inaction/demo1/'

}

})

]

};

Copy the code

After compiling, the resources are automatically uploaded to the Qiniu CDN, so the front-end only needs to deliver index.html.

summary

At this point, Webpack related common knowledge and advanced knowledge are introduced, you need to explore more in your work, Webpack with node.js ecosystem, will emerge more excellent new language and new tools!

0

The resources

[1]

Webpack Plugin: https://webpack.js.org/api/plugins/