preface
This article focuses on laoder and plugins in Webpack. It does not cover how to use and configure Webpack, because these foundations are already clear in official documents. The focus is on how to implement custom Laoder and plugins. So before we get started, let’s take a quick look at what a build tool is.
Build tools
In web applications, in addition to HTML files, often need to use a lot of other static resources to decorate, such as images, CSS styles, JS files used in HTML, but the browser does not recognize all file resources, and correctly load.
Therefore, developers need to process different file resources in order to correctly load and use the corresponding file resources. Such as:
- Images in addition to some common formats can be loaded and displayed normally, some special formats can not be directly used;
- CSS styles may be used by less/SCSS/CSS in JS etc.
- Js files may use relatively new syntax, such as ES6, ES7 or newer features, need corresponding compilation tools to do the conversion, etc.
Build tools are born out of the need to do different processing for different file resources and the maintenance problems of tools that deal with file resources.
The build tool contains solutions to most of the problems mentioned above, meaning that we originally needed different widgets to handle different file contents, but now we just need to focus on how the build tool itself is used.
webpack
What is Webpack?
Webpack, one of many build tools, is also a static module packaging tool for modern JavaScript applications.
When WebPack processes an application, it internally builds a dependency graph from one or more entry points, and then combines each module needed in the project into one or more bundles, which are static resources that are used to present your content.
The concepts of chunk and bundles can be understood as follows:
- According to the imported file resources, a dependency map is formed, which contains the chunk of code to be processed
- The corresponding processing of the code chunk is also called packaging. After the output, the required bundles are obtained
The five core
mode
- Optional values: Development, production, None
- Setting the mode parameter enables default optimizations built into WebPack for the appropriate environment
- The mode parameter defaults to production
entry
The entry point indicates which file webPack should use as the entry module to build its internal dependency diagram, and can have multiple entry files.
output
Output is responsible for telling WebPack where to export the bundles it creates and how to name those files.
- Default output directory:
./dist
- The default main output file name is./dist/main.js
- Other generated files are placed by default
./dist
下
loader
Webpack can only understand JavaScript and JSON files, and unboxed Webapck can’t recognize other file types. Loader can then convert these file types to resources that weback can recognize and convert them to valid modules that can be used in the application and added to the dependency graph.
plugin
Loader is used to transform certain types of modules, while plugin can be used to perform a wider range of tasks, including Loader. For example: package optimization, resource management, injection of environment variables, etc.
- You can import the corresponding plugin via require and instantiate the new PluginName(opt) call in an array of options to configure plugins.
- You can customize the WebPack plug-in to fulfill the requirements of a specific scenario
loader
What is a loader in Webpack?
A loader is essentially a function that takes three arguments:
content
: Indicates the content of the corresponding modulemap
: Sourcemap of the corresponding modulemeta
: Metadata of the corresponding module
Loader execution sequence
In general, the loader’s writing structure determines the order of execution:
- Left-right structure — > Execution order is from right to left
- Top up structure — > Execute from bottom up
For clarity and intuition, here is a list of common styles related configurations in webPack configurations:
module: {
rules: [{test: /\.css$/.// Left/right structure
use: ['style-loader'.'css-loader']./ / or
// upper and lower structure
use: [
'style-loader'.'css-loader']]}}Copy the code
Whether the left and right structure or the upper and lower structure, can be uniformly understood as the order from back to front to execute.
Custom loader
- Create loader1.js and loader2.js as custom loaders. Note that in addition to the exposed function methods, a pitch method is added to this function object.
The pitch method is executed in reverse order to loader, that is, the pitch method is executed from front to back.
// loader1.js
module.exports = function(content, map, meta) {
console.log('loader1 ... ');
return content;
}
module.exports.pitch = function (){
console.log('loader1 pitch... ');
}
// loader2.js
module.exports = function(content, map, meta) {
console.log('loader2 ... ');
return content;
}
module.exports.pitch = function (){
console.log('loader2 pitch... ');
}
Copy the code
- And configure it in webpack.config.js as follows:
// webpack.config.js
const { resolve } = require('path');
module.exports = {
mode: 'production'.module: {
rules: [{test: /\.js$/,
use: [
resolve(__dirname, 'loaders/loader1.js'),
resolve(__dirname, 'loaders/loader2.js'),]},]}}Copy the code
- To simplify every introduction of customizationloader, write the full path, such as:
resolve(__dirname, 'loaders/xxx.js)
, so it can be configuredresolveLoaderUniform option designationloaderThe path to be searched is as follows:
// webpack.config.js
const { resolve } = require('path');
module.exports = {
mode: 'production'.module: {
rules: [{test: /\.js$/,
use: [
'loader1'.'loader2',]},]},resolveLoader: {
modules: [
resolve(__dirname, 'loaders'),
'node_modules'].}}Copy the code
- When the webpack directive is entered in the editor terminal for packaging, the console output is as follows:
Loader synchronization and asynchrony
Synchronous loader
Callback (); callback(); callback(); callback();
This.callback (error, content, map, meta), where error indicates the error content, if there is no error, it can be executed as null. In this way, there is no need to explicitly return.
// loader1.js
module.exports = function(content, map, meta) {
console.log('loader1 ... ');
this.callback(null, content, map, meta);
}
module.exports.pitch = function (){
console.log('loader1 pitch... ');
}
// loader2.js
module.exports = function(content, map, meta) {
console.log('loader1 ... ');
this.callback(null, content, map, meta);
}
module.exports.pitch = function (){
console.log('loader1 pitch... ');
}
Copy the code
Asynchronous loader
CallBack = this.async(); Method, and then the asynchronous execution is complete by calling the callBack() method.
Loader2.js can be changed into an asynchronous loader. The modification content and running result are as follows:
// loader2.js
module.exports = function(content, map, meta) {
console.log('loader2 ... ');
const callback = this.async();
setTimeout(() = >{
callback(null,content, map, meta);
},1000);
}
module.exports.pitch = function (){
console.log('loader2 pitch... ');
}
Copy the code
PS: After the command is executed to loader2, the system waits about 1s and then executes loader1. At the same time, there is significantly more successfully compiled time than before.
Verify the validity of options in the Loader
Why is it necessary to verify validity?
The Options configuration is provided to make the custom loader more flexible and configurable, but if such flexibility is not constrained, the Options configuration may become meaningless. Imagine that external use passes a bunch of configurations that loader doesn’t need, making the configuration look more complex and rendering the loader’s internal judgment logic useless. For all the above reasons, it is important to verify the validity of Options. Only after the validation is passed, execute other processors in loader.
Obtain the Options configuration in the Loader
To validate options, obtain options first. There are two ways to obtain options:
- through
const options = this.getOptions()
The method of obtaining - By calling the
loader-utils
In the librarygetOptions(this)
Methods to obtain
Verification validity
This can be verified using the validate() method in the schema-utils library.
Intuitive understanding, through an example. First of all in webpack config. Modify configuration in js, namely to loader1 incoming configuration options, and then to loader1. Js rewrite the content, as follows:
// webpack.config.js
module: {
rules: [{test: /\.js$/,
use: [
{
loader: 'loader1'.options: {
name: 'this is a name! '}},'loader2',]},]}// loader1.js
const { validate } = require('schema-utils');
// schema indicates the schema that defines the verification rules
const loader1_schema = {
type: "object".properties: {
name: {
type: 'string',}},// additionalProperties indicates whether attributes can be appended
additionalProperties: true
};
module.exports = function (content, map, meta) {
console.log('loader1 ... ');
/ / get the options
const options = this.getOptions();
console.log('loader1 options = ',options);
// Verify that options are valid
validate(loader1_schema, options,{
name: 'loader1'.baseDataPath: 'options'});this.callback(null, content, map, meta);
}
module.exports.pitch = function () {
console.log('loader1 pitch... ');
}
Copy the code
Legal configuration in webpack.config.js:
{
loader: 'loader1'.options: {
name: 'this is a name! '}}Copy the code
Illegal configuration in webpack.config.js:
{
loader: 'loader1'.options: {
name: false}}Copy the code
Implement custom loader — vueLoader
Functional description
For.vue file
,
webapck.config.js
const { resolve } = require('path');
module.exports = {
mode: 'production'.module: {
rules: [{test: /\.vue$/,
use: {
loader: 'vueLoader'.options: {
template: {
path: resolve(__dirname, 'src/index.html'),
fileName: 'app',},name: 'app'.title: 'Home Page'.reset: true}}},]},resolveLoader: {
modules: [
resolve(__dirname, 'loaders'),
'node_modules'].}}Copy the code
vueLoader.js
const { validate } = require('schema-utils');
const fs = require('fs');
const { resolve } = require('path');
const vueLoader_schema = {
type: "object".properties: {
template: {
type: 'object'.properties: {
path: { type: 'string' },
fileName: { type: 'string'}},additionalProperties: false
},
name: {
type: 'string',},title: {
type: 'string',},reset: {
type: 'boolean',}},additionalProperties: false
};
module.exports = function (content, map, meta) {
const options = this.getOptions();
const regExp = {
template: /<template>([\s\S]+)<\/template>/,
script: /<script>([\s\S]+)<\/script>/,
style: /<style.+>([\s\S]+)<\/style>/}; validate(vueLoader_schema, options, {name: 'vueLoader'.baseDataPath: 'options'});let template = ' ';
let script = ' ';
let style = ' ';
if (content.match(regExp.template)) {
template = RegExp. $1; }if (content.match(regExp.script)) {
let match = RegExp. $1;let name = match.match(/name:(.+),? /) [1].replace(/("|')+/g.' ');
script = match.replace(/export default/.`const ${name} = `);
}
if (content.match(regExp.style)) {
style = RegExp. $1; }let { path, fileName } = options.template;
fileName = fileName || path.substring(path.lastIndexOf('\ \') + 1, path.lastIndexOf('.html'));
fs.readFile(path, 'utf8'.function (error, data) {
if (error) {
console.log(error);
return false;
}
const innerRegExp = {
headEnd: /<\/head>/,
bodyEnd: /<\/body>/}; content = data .replace(innerRegExp.headEnd,(match, p1, index, origin) = > {
let resetCss = "";
if (options.reset) {
resetCss = fs.readFileSync(resolve(__dirname, 'css/reset.css'), 'utf-8')}let rs = `<style>${resetCss} ${style}</style></head>`;
return rs;
})
.replace(innerRegExp.bodyEnd, (match, p1, index, origin) = > {
let rs = `${template}<script>${script}</script></body>`;
return rs;
});
if (options.title) {
content = content.replace(/<title>([\s\S]+)<\/title>/.() = > {
return `<title>${options.title}</title>`
});
}
fs.writeFile(`dist/${fileName}.html`, content, 'utf8'.function (error) {
if (error) {
console.log(error);
return false;
}
console.log('Write successfully!!! ');
});
});
return "";
}
Copy the code
plugins
What is a plugin in Webpack?
The plugin in Webpack consists of the following:
- A JavaScript named function or JavaScript class
- In the plug-in functionprototypeDefine a
apply()
methods - Specifies an event hook bound to the named function itself
- Handles specific data for webPack internal instances
- The callback provided by WebPack is invoked when the functionality is complete
Here is the basic structure of a plugin:
The tap() method in Apply is used to bind synchronous operations, but some plugins need to be asynchronous, and two asynchronous methods, tapAsync() or tapPromise(), can be used to bind. When using tapAsync, the callback argument has an additional callback to indicate whether asynchronous processing has ended. When the tapPromise approach was used, a Promise object was returned within it, indicating the outcome of the asynchronous processing by changing the Promise state.
class TestWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap('TestWebpackPlugin'.(compilation) = > {
console.log('tap callBack ... ');
// Return true to output output, false otherwise
return true;
});
compiler.hooks.emit.tapAsync('TestWebpackPlugin'.(compilation, callback) = > {
setTimeout(() = > {
console.log('tapAsync callBack ... ');
callback();
}, 2000);
});
compiler.hooks.emit.tapPromise('TestWebpackPlugin'.(compilation) = > {
return new Promise((resolve, reject) = > {
setTimeout(() = > {
console.log('tapPromise callBack ... ');
resolve();
}, 1000); }); }); }}module.exports = TestWebpackPlugin;
// Output sequence:
// 1. tap callBack ...
// 2. tapAsync callBack ... (Wait for the previous tap to complete and output 2s later)
// 3. tapPromise callBack ... (Wait for the previous tapAsync execution to finish, output 1s later)
Copy the code
Order of execution in plugin
From the above example, you can see that the order of execution is:
- Refer to the lifecycle hook functions for the timing of execution of the different hooks, which determine the order of execution
- Callbacks registered in the same hooks in the same plugin are executed in serial order, even if asynchronous operations are involved
Verify the validity of options in plugin
The validate() method in schema-utils is used to validate this, as is the case with the loader. Unlike loader, plugin options do not need to be retrieved by this.getoptions () because plugin is a class or constructor. It can therefore be obtained directly from constructor.
Implement a custom Plugin — CopyWebpackPlugin
Functional description
Copies all files in the specified directory to the destination directory. Some files can be ignored.
webpack.config.js
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin');
module.exports = {
mode:'none'.plugins: [
new CopyWebpackPlugin({
from: './public'.to: 'dist'.ignores: ['notCopy.txt']]}});Copy the code
CopyWebpackPlugin.js
const { validate } = require('schema-utils');
const { join, resolve, isAbsolute, basename } = require('path');
const { promisify } = require('util');
const fs = require('fs');
const webapck = require('webpack');
const { RawSource } = webapck.sources;
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const schema = {
type: 'object'.properties: {
from: {
type: 'string',},to: {
type: 'string',},ignores: {
type: 'array',}},additionalProperties: false,}class CopyWebpackPlugin {
constructor(options = {}) {
this.options = options;
// Verify that options are valid
validate(schema, options);
}
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyWebpackPlugin'.async (compilation, callback) => {
let { from, to = '. ', ignores = [] } = this.options;
// The directory to run the instruction
let dir = process.cwd() || compilation.options.context;
// Check whether the path passed is an absolute path
from = isAbsolute(from)?from : resolve(dir, from);
to = isAbsolute(to) ? to : resolve(dir, to);
// 1. Obtain all file or folder names in the form directory
let dirFiles = await readdir(from.'utf-8');
// 2. Filter file or folder names by ignores
dirFiles = dirFiles.filter(name= >! ignores.includes(name));// 3. Read all files in the form directory
const files = await Promise.all(dirFiles.map(async (name) => {
const fullPath = join(from, name);
const data = await readFile(fullPath);
const filename = basename(fullPath);
return {
data,// File content data
filename,/ / file name
};
}));
// 4. Generate resources in Webpack format
const assets = files.map(file= > {
const source = new RawSource(file.data);
return {
source,
filename: file.filename,
};
});
// 5. Add it to compilation to output
assets.forEach((asset) = > {
compilation.emitAsset(asset.filename, asset.source);
});
// 6. Use callback to indicate that the current processing is completecallback(); }); }}module.exports = CopyWebpackPlugin;
Copy the code