An overview of the
Because busy for a long time did not update the article, happened to do technology sharing in the company, privately wrote an article. Hope to bring help to partners in need, grow up together ~💪
Here’s what you can learn:
Webpack
In the configurationLoader
The use of theWebpack
In the configurationPlugin
The use of theLoader
å’ŒPlugin
The difference between- How to write a custom
Loader
- How to write a custom
Plugin
As we all know, Webpack can only handle JavaScript and JSON files, other types of files can only be converted into JavaScript code by the corresponding loader for Webpack!
To explore the Loader
Loader is a code transcoder that converts various resources. Next, we will thoroughly understand the essence of Loader from its characteristics, classification, usage and execution sequence, so as to pave the way for us to realize custom Loader and understand the underlying principle.
Loader
Features: single principle, eachLoader
Just do the right thingLoader
The classification of:pre
,normal
(Default),inline
,post
Four kinds ofLoader
Usage: singleloader
More than,loader
Object formLoader
Order: right to left, bottom to top
Conclusion: Loader is a function that takes the original resource as an argument and outputs the converted content.
Basic usage of Loader
Next, we’ll create a project to explore how loaders are used in Webpack, what to watch out for, and how to customize a Loader.
mkdir webpack-loader-plugin
cd webpack-loader-plugin
npm init -y
npm install webpack webpack-cli -D
Copy the code
After installing the dependency and creating the corresponding directory, create the webpack configuration file, webpack.config.js, to try the use of the loader, and import a non-JS file and package it. You may need an appropriate loader to handle this file type. Therefore, non-JS files loaded in Webpack need to be converted by the corresponding loader before packaging. Let’s take the.css style file as an example:
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development'.entry: './src/index.js'.output: {
filename: 'bundle.js'.path: path.resolve(__dirname, 'dist'),},module: {
rules: [{test: /\.css$/,
use: ['style-loader'.'css-loader'],},],},}Copy the code
The above are the characteristics and basic usage of Loader. The following describes the classification and sequence of Loader by customizing loader.
Custom Loader
Create a loader folder to store our custom loader1.js, loader2.js, loader3.js, loader4.js and modify webpack.config.js configuration:
const path = require('path')
module.exports = {
...
module: {
rules: [{test: /\.js$/,
use: [path.resolve(__dirname, 'loader/loader1.js'), path.resolve(__dirname, 'loader/loader2.js'), path.resolve(__dirname, 'loader/loader3.js'), path.resolve(__dirname, 'loader/loader4.js']}]}}Copy the code
Import mode of loader:
-
- through
npm
Packages installedloader
, just use the name
{ test: /\.css$/, use: 'css-loader' } Copy the code
- through
-
- The custom
loader
, the absolute path is used
{ test: /\.css$/, use: path.resolve(__dirname, 'loader/loader1.js')}Copy the code
- The custom
-
- Configuring alias Mode
resolveLoader: { alias: { 'loader1': path.resolve(__dirname, 'loader/loader1.js')}},module: { rules: [{test: /\.css$/, use: 'loader1'}}]Copy the code
-
- Configuring the Lookup Mode
resolveLoader: { modules: ['node_modules', path.resolve(__dirname, 'loader')]},module: { rules: [{test: /\.css$/, use: 'loader1'}}]Copy the code
Loader execution sequence:
As shown in the preceding example, the normal Loader is executed from right to left and from bottom to top.
By changing the execution order of the four loader types: pre, normal, inline, and POST. This allows us to change the execution order of the Loader at will.
{
test: /.js$/,
use: 'loader1'.enforce: 'pre'
},
{
test: /.js$/,
use: 'loader2'
},
{
test: /.js$/,
use: 'loader3'.enforce: 'post'
}
Copy the code
If the enforce attribute is not added, the loading sequence is loader3 -> Loader2 -> loader1 from bottom to top. However, after configuring the invincibility attribute, the loading sequence is changed to loader1 -> loader2 -> loader3
Inline loader loading rules
!
: ignorenormal loader
-!
: ignorenormal loader
,pre loader
!!!!!
: ignorenormal loader
,pre loader
,post loader
Summary: Use prefix to disable different types of Loaders
Picth method
Loader execution is divided into two stages: Pitch stage and Noraml stage. When defining a Loader function, you can export a pitch method that is executed before the Loader function is executed.
The loader executes pitch, obtains resources, and then executes normal Loader. If the pitch has a return value, it will not go to the later loader and return the return value to the previous loader. That’s why a pitch is a circuit breaker!
// loader/loader1.js
function loader1(source) {
console.log('loader1~~~~~~', source)
return source
}
module.exports = loader1
module.exports.pitch = function (res) {
console.log('pitch1')}/ / loader/loader2. Js in the same way
/ / loader/loader3. Js in the same way
/ / loader/loader4. Js in the same way
Copy the code
To obtain parameters
To get a parameter passed in by the user, you need to download a dependency:
NPM install loader-utils -d // Note the [email protected] versionCopy the code
Loader-utils dependencies have a getOptions method that gets the configuration of loader options
// webpack.config.js
{
test: /.js$/,
use: [
{
loader: 'loader1'.options: {
name: 'tmc'}}}]// loader1.js
const loaderUtil = require('loader-utils')
function loader1 (source) {
const options = loaderUtil.getOptions(this) | | {}console.log(options)
return source
}
module.exports = loader1
{name: 'TMC'}
Copy the code
Validate parameter
To verify that the parameter passed by the user is valid, you need to download a dependency:
npm install schema-utils -D
Copy the code
The schema-utils dependency has a validate method that verifies that the options configuration in the loader is valid
// loader/loader1.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')
const schemaJson = require('./schema.json')
function loader1 (source) {
const options = getOptions(this) || {}
validate(schemaJson, options, { name: 'loader1' })
return source
}
module.exports = loader1
// schema.json
{
"type": "object"."properties": {
"name": {
"type": "string"."description": "Name"}},"additionalProperties": true
}
Copy the code
The key name in “properties” is the name of the field in “options” that we need to check. AdditionalProperties indicates whether additionalProperties are allowed for options.
Note: The additionalProperties property defaults to false. When loader allows multiple options properties, change the value to true.
Synchronous & asynchronous
Loaders are classified into synchronous and asynchronous loaders
When converting content synchronously, results can be returned using either return or this.callback()
The detailed method for callback is as follows:
callback({
error: Error | Null, // Returns an Error to Webpack when the original content cannot be converted
content: String | Buffer, // The converted contentsourceMap? : SourceMap,// The converted content generates the Source Map of the original content (optional)abstrctSyntaxTree? : AST// The number of AST syntax generated from the original content (optional)
})
Copy the code
function loader3 (source, map, meta) {
console.log('loader3~~~~~~', source)
return source
}
/ / or
function loader3 (source, map, meta) {
console.log('loader3~~~~~~', source)
this.callback(null, source, map, meta)
return;
}
module.exports = loader3
// Map and meta are optional parameters
Copy the code
Note: When callback() is called, undefined is always returned
When transforming content asynchronously, use the this.async() form to get the callback function for the asynchronous operation and return the result in the callback function
function loader4 (source) {
console.log('loader4~~~~~~', source)
const callback = this.async()
setTimeout(() = > {
callback(null, source) // The first argument is an error object that can be set to null
}, 1000)}module.exports = loader4
Copy the code
skills
webpack
All are cached by defaultloader
If you don’t want to use cachingthis.cacheable(false)
- Working with binary data
module.exports = function(source) {
source instanceof Buffer === true;
return source;
};
// Tells Webpack whether the Loader needs binary data via the exports.raw property
module.exports.raw = true;
Copy the code
Note: The most critical code is the last line of module.exports. Raw = true; Loader can only get the string without the line
In actual combat
Next, let’s write a Loader to see how it works:
Requirements: Simulate the functions of babel-loader
// loader/babelLoader.js
const {
getOptions
} = require('loader-utils')
const {
validate
} = require('schema-utils')
const babel = require('@babel/core')
const schema = require('./babelSchema.json')
function babelLoader(source) {
// Get the parameters passed in by the user
const options = getOptions(this)
// Validate parameters
validate(schema, options, {
name: 'babelLoader'
})
const callback = this.async()
// Convert the code
babel.transform(source, {
presets: options.presets,
sourceMap: true
}, (err, result) = > {
callback(err, result.code, result.map)
})
}
module.exports = babelLoader
// loader/babelSchema.json
{
"type": "object"."properties": {
"presets": {
"type": "array"}},"additionalProperties": true
}
/ / use
module.exports = {
module: {
rules: [{test: /.js$/,
use: {
loader: 'babelLoader'.options: {
presets: [
"@babel/preset-env"]}}}]},// Parse loader rules
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loader')]}}Copy the code
To explore the Plugin
Plugin is an extender that is more flexible than Loader because it has access to the Webpack compiler. A number of events are broadcast during the life cycle of a Webpack run, and the Plugin can listen for these events and change the output when appropriate through the API provided by Webpack. This allows Plugin to intercept Webpack execution through hook functions that do things other than Webpack packaging. Like: package optimization, resource management, injection of environment variables, and so on.
Basic usage of Plugin
There are three steps to using a plugin:
-
npm
Install the corresponding plug-in
-
- Introduce installed plug-ins
-
- in
plugins
The use of
- in
// 1. NPM installs the corresponding plug-in
npm install clean-webpack-plugin -D
// 2. Import the installed plug-in
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 3. Use in plugins
export default {
plugins: [
new CleanWebpackPlugin()
]
}
Copy the code
A custom Plugin
Before writing the plug-in, we first need to familiarize ourselves with the overall running flow of WebPack. The running of Webpack is essentially a mechanism of event flow. During the running of Webpack, a series of processes including initialization parameters, compiling files, confirming entry, compiling modules, compiling completion, generating resources and finally outputting resources are carried out. Along the way, WebPack broadcasts events from the corresponding nodes, and we can listen for those events and do something with the API provided by WebPack at the right time.
The Webpack plug-in consists of:
- a
JavaScript
Naming functions - In the plug-in function
propotype
Define aapply
methods - Make a binding to
webpack
Its own event hook - To deal with
webpack
Specific data for the internal instance - Called when the function completes
webpack
The callback provided
The core of the Plugin is that when the Apply method executes, it operates on hooks that are packed by WebPack at different points in time. Its working process is as follows:
webpack
After startup, executenew myPlugin(options)
Initialize the plug-in and get the instance- Initialize the
compiler
Object, callmyPlugin.apply(compiler)
Passing in a plug-incompiler
object - Plug-in instance acquisition
compiler
It listenswebpack
Broadcast the event throughcompiler
operationwebpack
In other words, a plug-in is a class, using the plug-in is a new instance of the plug-in, and passing the configuration parameters required by the plug-in to the constructor of the class. Gets the user-passed argument in the constructor.
The first step in writing a plug-in is as follows:
class myPlugin {
constructor(options) {
this.options = options // The parameter passed by the user}}module.exports = myPlugin
Copy the code
According to the Webpack source code, the plug-in instance will have an apply method with compiler as its parameter.
/ / webpack. Js source code
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else{ plugin.apply(compiler); }}}Copy the code
Extensions: Webpack plug-ins have an Apply method, just like Vue plug-ins have an Install method
The second step of writing a plug-in is as follows:
class myPlugin {
constructor(options) {
this.options = options // The parameter passed by the user
}
apply(compiler){}}module.exports = myPlugin
Copy the code
The two most commonly used object Compiler and Compilation in Plugin development, both inherited from Tapable, are the bridge between Plugin and Webpack. React-redux is a bridge between React and Redux.
Core object
- Responsible for the overall compilation process
Compiler
object - Responsible for compiling the Module
Compilation
object
Compiler
The Compiler object represents the complete configuration of the Webpack environment (think of it as an instance of Webpack). This object is created once when webPack is started, and all the actionable Settings are configured, including Options, Loader, and plugin. When a plug-in is applied in a WebPack environment, the plug-in receives a reference to this Compiler object. You can use it to access the main webPack environment.
Compilation
The Compilation object represents a resource version build. When running the WebPack development environment middleware, each time a file change is detected, a new Compilation is created, resulting in a new set of Compilation resources. A Compilation object represents the current module resources, the compile-generated resources, the changing files, and the state information of the dependencies being tracked. The Compilation object also provides a number of critical timing callbacks that plug-ins can choose to use when doing custom processing.
Note: The difference between the two is that one represents the entire build process and the other represents a module in the build process
compiler
Represents the wholewebpack
Lifecycle from startup to shutdown, whilecompilation
It just represents a one-time compilation process.compiler
å’Œcompilation
Both expose a number of hooks that we can customize to the scenario we need.
Tapable
Tapable provides a publish-subscribe API for a series of events that can be registered with Tapable to trigger registered event callbacks at different times for execution. The Plugin mechanism in Webpack is based on this mechanism to call different plug-ins at different compilation stages and thus affect compilation results.
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
Copy the code
There is only one way to register synchronous hooks
- Can only be registered through TAP, by
call
To perform the
Asynchronous hooks provide three ways to register:
tap
: Registers hooks synchronously with thecall
To perform thetapAsync
: Registers hooks asynchronously withcallAsync
To perform thetapPromise
: Registers hooks asynchronously and returns onePromise
Note: Asynchronous hooks can be divided into:
- Asynchronous serial hook (
AsyncSeries
) : Asynchronous hook functions that can be executed concatenated (called sequentially) - Asynchronous parallel hooks (
AsyncParallel
) : an asynchronous hook function that can be executed in parallel (called concurrently)
What are the differences between different types of hooks?
- Basic type: it does not care about the return value of each called event and only executes the registered callback function
- Waterfall type: passes the return value of the previous callback function to the next callback function as an argument
- Insurance type: if not in the callback function
undefined
When a value is returned, subsequent callbacks are not executed - Loop type: If there is no in the callback function
undefined
All callbacks are restarted until all callbacks have returnedundefined
Once we know what events WebPack broadcasts, we can listen for events in Apply and write the corresponding logic, as follows:
class myPlugin {
constructor(options) {
this.options = options // The parameter passed by the user
}
apply(compiler) {
// Listen for an event
compiler.hooks.'Compiler event name'.tap('myPlugin'.(compilation) = > {
// Write the corresponding logic}}})module.exports = myPlugin
Copy the code
Note: While listening for compilation events in the Compiler object, you can also continue listening for compilation events in the compiler object in the callback function, as follows:
class myPlugin {
constructor(options) {
this.options = options // The parameter passed by the user
}
apply(compiler) {
// Listen for an event
compiler.hooks.'Compiler event name'.tap('myPlugin'.(compilation) = > {
// Write the corresponding logic
compilation.hooks.'Compilation Event Name'.tap('myPlugin'.() = > {
// Write the corresponding logic}}})})module.exports = myPlugin
Copy the code
The events listened for above are synchronized hooks registered with tap. When listening for asynchronous hooks, we need to register with tapAsync and tapPromise. We also need to pass in a cb callback function. After the plugin is finished, we must call this cb callback function as follows:
class myPlugin {
constructor(options) {
this.options = options // The parameter passed by the user
}
apply(compiler) {
// Listen for an event
compiler.hooks.emit.tapAsync('myPlugin'.(compilation, cb) = > {
setTimeout(() = > {
// Write the corresponding logic
cb()
}, 1000)}}}module.exports = myPlugin
Copy the code
Get & validate parameters
We know that we can get arguments passed to the plug-in from the hook function of the plug-in. When writing a plug-in, we need to verify that the parameters passed in are valid. Similar to Loader, to verify the validity of parameters, download:
npm install schema-utils -D
Copy the code
Debugging tips:
"scripts": {
"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
},
Copy the code
The WebPack plug-in is essentially a publish-subscribe model, listening for events on compiler. It then packages the events that trigger listening during compilation to add some logic to affect the packaging results.
In actual combat
Next, let’s put the Plugin into practice by writing it by hand
Requirement: Remove comments from js files before packaging
class MyPlugin {
constructor(options) {
console.log('Plug-in options:', options)
this.userOptions = options || {}
}
// Must have the apply method
apply(compiler) {
compiler.hooks.emit.tap('Plug-in name'.(compilation) = > {
// compilation Specifies the compilation context
console.log('Webpack build process begins! ', compilation)
for (const name in compilation.assets) {
// if(name.endsWith('.js'))
if (name.endsWith(this.userOptions.target)) {
// Get the content before processing
const content = compilation.assets[name]
// Remove comments from the original content via regular expressions
const noComments = content.replace(/\/\*[\s\S*?] \*\//g.' ')
// Replace the processed result
compilation.assets[name] = {
source: () = > noComments,
size: () = > noComments.length,
}
}
}
})
}
}
module.exports = MyPlugin
/ / use
const MyPlugin = require('./plugin/my-plugin')
module.exports = {
// Plug-in configuration
plugins: [
new MyPlugin({
// Plug-in options
target: '.js',})],}Copy the code
conclusion
From the above we have a general understanding of two important concepts in Webpack: Loader and Plugin. Front-end engineering is becoming more and more important in the field of front-end engineering, whether in daily work or interview to improve their technical skills. Mastering the use of Webpack and understanding how it works is of great benefit!