Webpack catalog
- The core configuration of webpack5
- Webpack5 modularity principle
- Webpackage 5 with Babel/ESlint/ browser compatibility
- Webpack5 performance optimization
- Webpack5 Loader and Plugin implementation
- Webpack5 core source code analysis
Basic implementation of Loader
We have mentioned a lot of loaders in the core configuration, such as style-loader, CSS-loader, vue-loader, babel-loader, etc. How to implement a custom Loader? Loader is essentially a JavaScript module exported as a function. The Loader Runner library calls this function and then passes in the results or resource files generated by the previous Loader.
Now we are developing a custom loader. Let’s create a new loaders directory and create a new yJ-loader.js
// loaders/yj-loader.js
module.exports = function(content, map, meta) {
console.log(content)
console.log(map)
console.log(meta)
return content
}
Copy the code
The function takes three arguments
content
: The contents of the resource filemap
: sourcemAP Related datameta
: Some metadata
The following aspects are discussed from the introduction path of loader, execution sequence, asynchronous Loader, obtaining parameters and realizing a Loader.
The introduction of the path
Now we configure the custom loader in Webpack
{
test: /\.js$/,
use: [
'./loaders/yj-loader']},Copy the code
As you can see, the custom loader path we introduced is relative and based on the context property, but if we still want to load our own Loader file directly, we can configure the resolveLoader property
{
resolveLoader: {
modules: [
'node_modules'.'./loaders']}}Copy the code
The default is node_modules. If node_modules does not exist, go to our loaders directory. We can add our loaders directory to the module property
{
test: /\.js$/,
use: [
'yj-loader']},Copy the code
Execution order
Create three custom Loaders to prove this result. Create yJ-loader01. js, yJ-loader02.js, yJ-loader03.js. And print in each loader
// yj-loader01.js
module.exports = function(content, map, meta) {
console.log('loader01 execution')
return content
}
// yj-loader02.js
module.exports = function(content, map, meta) {
console.log('loader02 execution')
return content
}
// yj-loader03.js
module.exports = function(content, map, meta) {
console.log('loader03 execution')
return content
}
Copy the code
// webpack
{
test: /\.js$/,
use: [
'yj-loader01'.'yj-loader02'.'yj-loader03']},Copy the code
Now let’s pack up the NPM Run build
Loader03 is executed first, loader02 is executed second, and loader01 is executed last. In fact, we can also configure a pitch loader in loader, let’s modify the loader
// yj-loader01.js
module.exports = function(content, map, meta) {
console.log('loader01 execution')
return content
}
module.exports.pitch = function() {
console.log('the pitch - loader01 perform')}// yj-loader02.js
module.exports = function(content, map, meta) {
console.log('loader02 execution')
return content
}
module.exports.pitch = function() {
console.log('the pitch - loader02 perform')}// yj-loader03.js
module.exports = function(content, map, meta) {
console.log('loader03 execution')
return content
}
module.exports.pitch = function() {
console.log('the pitch - loader03 perform')}Copy the code
Then repackage
Loader-runner: lib/LoaderRunner. Js: lib/LoaderRunner. Js: LoaderRunner: lib/LoaderRunner.
The iteratePitchingLoaders function is executed first, that is, pitch-loader is executed first.
And in iteratePitchingLoaders loaderContext loaderIndex++, and recursive implementation iteratePitchingLoaders, iterateNormalLoaders after execution of the implementation, That is, a normal Loader.
Looking down can see loaderContext. LoaderIndex — –, and perform iterateNormalLoaders. Therefore, loaderIndex is used to execute loaderIndex
Conclusion:
- RunLoader executes PitchLoader first and loaderIndex++ during PitchLoader execution
- NormalLoader is then executed by runLoader, and loaderIndex– is executed when NormalLoader is executed
Can we customize the execution order? Yes, we need to split it into multiple Rule objects and use Enforce to change their order
Enforce has four methods:
- By default, all loaders are
normal
- The loader set in the line is
inline
- You can set this by Using Enforce
pre
andpost
- PitchingStage: Pitch method on loader, according to
After (POST), Inline (Inline), Normal (normal), Front (pre)
Sequential call of - NormalStage: General method on loader, as per
Front (Pre), Normal (normal), Inline (Inline), Post (POST)
Is called sequentially. Conversion of module source code occurs in this phase.
Now we will set pre for loader02
{
test: /\.js$/,
use: [
'yj-loader01'],}, {test: /\.js$/,
use: [
'yj-loader02',].enforce: 'pre'
},
{
test: /\.js$/,
use: [
'yj-loader03']},Copy the code
Now you can see that loader02 is executed first and pitch loader02 is executed last.
Asynchronous loader
All loaders created by default are synchronous loaders. This Loader must return the result through return or this.callback and hand it to the next Loader for processing. Usually we use this.callback in case of errors
This. Callback can be used as follows:
- The first argument must be Error or NULL
- The second argument is a string or Buffer
// yj-loader.js
module.exports = function(content, map, meta) {
console.log('execution loader')
return this.callback(null, content)
}
Copy the code
Now it is ok to use this. Callback method to return the result of Loader processing. Sometimes we use Loader to perform some asynchronous operation, and we hope to return the result of Loader processing after the asynchronous operation. Loader-runner has given us the implementation of this. Async function, which we use as follows
// yj-loader03.js
module.exports = function(content, map, meta) {
console.log('execution loader03')
const callback = this.async()
setTimeout(() = > {
callback(null, content)
}, 3000)}Copy the code
It can still be printed in sequence, and in the packaging process, it can be seen that loader03 is printed about 3S later than loader02 and loader01.
To obtain parameters
We used csS-loader or babel-loader to configure the parameters, so how can we also configure the parameters and get them? Loader-utils, a resolver library provided by WebPack, has a getOptions method to help us get the configuration, and the library automatically installs webPack when we install it. Modify our loader and add parameters to loader
// webpack
{
test: /\.js$/,
use: [
{
loader: 'yj-loader03'.options: {
name: 'lyj'.age: 18}}},Copy the code
// yj-loader-03.js
const { getOptions } = require('loader-utils')
module.exports = function(content, map, meta) {
console.log('loader03 execution')
// Get parameters
const options = getOptions(this)
console.log(options)
// Get the asynchronous Loader
const callback = this.async()
setTimeout(() = > {
callback(null, content)
}, 3000)}Copy the code
As you can see, we got the parameters by calling getOptions(this), so how do we verify the parameters passed in? We can use an official webPack validation library schema-utils, which has the validate method to validate parameters, and this library installs WebPack for us when we install it.
Now we need a validation rule file and create a loader-schema.json
// loader-schema.json
{
"type": "object".// Pass in the type
"properties": { / / property
"Name": {
"type": "string"."description": "Please enter your name"
},
"age": {
"type": "number"."description": "Please enter your age"}},"additionalProperties": true // indicates that additional attributes can be added in addition to the above attributes
}
Copy the code
// yj-loader-03.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils') // Used to verify loader parameters
const loaderSchema = require('./loader-schema.json')
module.exports = function(content, map, meta) {
console.log('loader03 execution')
// Get parameters
const options = getOptions(this)
console.log(options)
// Check parameters
validate(loaderSchema, options)
// Get the asynchronous Loader
const callback = this.async()
setTimeout(() = > {
callback(null, content)
}, 3000)}Copy the code
Now we pass in the age string and repackage it
Schema-utils helped us validate the parameters and prompt the description, and blocked the build, indicating that the validation was successful.
Implementing a Loader
Now let’s implement a simple Markdown loader that installs marked, highlight.js. Go straight to code
// mkdown-loader.js
const marked = require('marked')
const hljs = require('highlight.js')
module.exports = function(content) {
// Set code highlighting
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value
}
})
/ / to HTML
const htmlContent = marked(content)
// Switch to modular export
const innerContent = '` + htmlContent +'`
const moduleCode = `var code = ${innerContent}; export default code; `
console.log(moduleCode)
return moduleCode
}
Copy the code
// WebPack Loader configuration
{
test: /\.md$/,
use: 'mkdown-loader'
}
Copy the code
// test.md
# loader realize
## Import path
## Execution order
Asynchronous loader # #
``` module.exports = function(content, map, Meta) {console.log(' loader03') const callback = this.async() setTimeout(() => {callback(null, content)}, 3000)} ' '## parameter fetchCopy the code
// main.js
import mdContent from './test.md'
import 'highlight.js/styles/default.css'
document.body.innerHTML = mdContent
Copy the code
After repackaging, we can see our mkDown compilation on the page
The underlying implementation of Plugins
Webpack has two very important classes, Compiler and Compilation, which listen to the entire Webpack process by injecting plugins that need hooks managed by an officially maintained Tapable library. So we need to figure out how to use this library first.
Tapable
The Tapable export contains the following hooks
SyncHook
SyncBailHook
SyncWaterfallHook
SyncLoopHook
AsyncParallelHook
AsyncParallelBailHook
AsyncSeriesHook
AsyncSeriesBailHook
AsyncSeriesLoopHook
AsyncSeriesWaterfallHook
We can classify Tapable hooks as synchronous and asynchronous,
- A Hook that begins with sync is a synchronized Hook
- Two events that begin with async handle callbacks and do not wait for the last processing callback to finish before executing the next one
We can also classify them in other categories
bail
: When there is a return value, no subsequent event firing is performedLoop
: The event is executed repeatedly when the return value is true, and exits when the return value is undefined or nothing is returnedWaterfall
: If the return value is not undefined, the returned result will be used as the first parameter of the next eventParallel
: parallel: the second event processing callback is executed at the same time, and then the next event processing callback is executedSeries
: serial, will wait for the last is asynchronous Hook
Let’s use Tapable briefly
1. Write a Tapable test file
// tapable-test.js
const { SyncWaterfallHook } = require('tapable')
class MyTapable {
constructor() {
this.hooks = {
syncWaterfallHook: new SyncWaterfallHook(['myName'.'myAge'])}this.on()
}
/ / register
on() {
this.hooks.syncWaterfallHook.tap('myTap1'.(name, age) = > {
console.log('myTap1', name, age)
return '123'
})
this.hooks.syncWaterfallHook.tap('myTap2'.(name, age) = > {
console.log('myTap2', name, age)
})
}
/ / initialization
emit() {
this.hooks.syncWaterfallHook.call('lyj'.18)}}const tapable = new MyTapable()
tapable.emit()
Copy the code
2. Perform tapable – test. Js
node tapable-test.js
Copy the code
3. Print the results
You can see that the first registered hook returns 123 to the first argument of the second hook
Plugin Registration Principle
How to register plug-ins in Webpack, we can view the source code
- In the createCompiler method that calls the WebPack function, register all plug-ins
- When a plug-in is registered, the plug-in function or the Apply method of the plug-in object is called
- The plug-in methods receive compiler objects, which we can use to register Hook events
- Some plugins also pass in a compilation object, and we can listen for Hook events from a compilation
Implement a Plugin
We implement a plug-in AutoUploadPlugin that packages the build directory and automatically uploads it to the server
const { NodeSSH } = require('node-ssh');
class AutoUploadPlugin {
constructor(options) {
this.ssh = new NodeSSH();
this.options = options;
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync("AutoUploadPlugin".async (compilation, callback) => {
// 1. Get the output folder
const outputPath = compilation.outputOptions.path;
// 2. Connect to the server (SSH connection)
await this.connectServer();
// 3. Delete the contents of the original directory
const serverDir = this.options.remotePath;
await this.ssh.execCommand(`rm -rf ${serverDir}/ * `);
// 4. Upload files to the server (SSH connection)
await this.uploadFiles(outputPath, serverDir);
// 5. Disable SSH
this.ssh.dispose();
callback();
});
}
async connectServer() {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password
});
console.log("Connection successful ~");
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true.concurrency: 10
});
console.log('Send to server:', status ? "Success": "Failure"); }}module.exports = AutoUploadPlugin;
Copy the code
Using the plug-in
// webpack
{
plugins: [
/ /...
new AutoUploadPlugin({
host: 'xxx.xxx.xxx.xxx'.username: 'xxx'.password: 'xxx'}})]Copy the code