Writing in the front
Most developers will probably only use Webpack at the configuration level and will only be able to skillfully use Webpack’s various configuration options in real projects.
But to think of Webpack only at the configuration level is, in my opinion, not enough for a qualified front end engineer.
An in-depth look at Webpack will inevitably bring up the Plugin mechanism, as to exactly how the Webpack Plugin should be applied to business project architectures or, in other words, what advantages it can give us in different business scenarios.
In my opinion, the level of application of this knowledge is a major hurdle that any advanced front-end developer must cross on the path to architecture.
Next, I’ll introduce you to the world of Webpack plug-in developers by taking you step by step through the implementation of two plugins.
If you’re interested in learning more about Webpack, you can check out my previous Webpack column from The Basics of Playing with Webpack here.
Before we begin, I want to say something
If you are interested in digging into the Webpack Plugin I strongly read these two articles as a primer:
- Tapable can read just one
With Tapable, the Webpack Plugin is completely based on it.
Generally speaking, it is a publishing and subscription library similar to EventEmitter in Node. We can accept parameters and subscribe corresponding events in Plugin through Tapable, and Webpack can trigger different Tapable hooks at different times of packaging to affect the packaging results.
- Fully explore the core Plugin mechanism in Webpackage 5
In this article, some conceptual issues of the development of Webpack Plugin are explained, and the meanings of hooks provided by Webpack for plug-in developers and the corresponding execution timing are introduced.
Don’t worry, there will be a little review when it comes to the above two articles. If you are interested in learning more, I suggest you go back to the previous two posts with questions after reading the article.
CompressAssetsPlugin
Let’s start with a simple plug-in and talk about the basic steps of a plug-in development process.
Demand analysis
As we all know, when we use Webpack to package a project, we usually pack all the resources in the dist file directory, and store the corresponding HTML, CSS and JS files respectively.
At this point, suppose I need to pack all the resources generated by each package into a ZIP package at the end of each package.
Then I will save the packed ZIP to my server for backup and other functions, but of course it is not important. The important thing is that we have our goal: I need to package all the resources generated by this compilation as ZIP extra output at the end of each package.
const path = require('path');
const CompressAssetsPlugin = require('./plugins/CompressAssetsPlugin');
module.exports = {
mode: 'development'.entry: {
main: path.resolve(__dirname, './src/entry1.js'),},devtool: false.output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js',},plugins: [
new CompressAssetsPlugin({
output: 'result.zip',})]};Copy the code
The.js file above is a simple WebPack configuration file that shows how to use CompressAssetsPlugin.
It simply takes an output parameter, which represents the name of the generated ZIP.
I don’t need to be verbose about webPack basics. If you don’t know the basics, check out react-WebPack5-typescript to build engineered multi-page applications. Next I’ll take you through implementing the CompressAssetsPlugin plug-in.
The principle of analysis
There are two core objects around the Webpack packaging process:
- compiler
Compiler is created when Webpack starts packaging and holds all the initial configuration information for the packaging.
It creates the Compilation object to package the module each time a package is performed. For example, in Watch (devServer) mode, a Compilation object is generated every time the content of the file changes, and the Compiler object is always one, unless you terminate the package command and call webpack again.
- compilation
The compilation represents the process of building a resource. The compilation object allows you to access/modify modules, assets, and chunks generated by this package through a set of apis.
The official website introduces both objects at this link.
Here, we need to uniformly package the output resource files into ZIP after each package is about to be generated, mainly using the following contents:
- JS Zip
This is a JS generated zip package library, we will use this library to generate zip content.
- compiler Emit Hook
The Emit Hook on the Compiler object is executed before the asset is printed to the Output directory; in short, this Hook is called every time the build file is about to be packaged.
- Compilation object methods
During the process of packaging, we need to obtain the resources to be generated by the packaging. We can use the compilation.getAssets() method to obtain the content of the resource files generated by the original packaging and output the generated zip into the packaging result through the compilation.emitAssets().
implement
Now that we’ve covered the basics, let’s go ahead and implement the plugin.
First, any Webpack Plugin is a module that exports a class or function that must have a prototype method named Apply.
When the Webpack () method is called to begin the packaging, the Compiler object is passed to the Apply methods of each plug-in and the corresponding hooks are called to register them.
To implement the basics, let’s create a compressassetsplugin.js file in the./plugins directory:
const pluginName = 'CompressAssetsPlugin';
class CompressAssetsPlugin {
// Parameters passed in the configuration file are saved in the plug-in instance
constructor({ output }) {
// Accept the output parameter passed in from the outside
this.output = output;
}
apply(compiler) {
The registration function is executed when WebPack is about to output the contents of the packaged file
compiler.hooks.emit.tapAsync(pluginName, (compilation,callback) = > {
// dosomething}}})module.exports = CompressAssetsPlugin;
Copy the code
Above we set up a simple plug-in infrastructure, we in the apply method by the compiler. The hooks. The emit. TapAsync registered function to an event, the event function will be done in every time the package is generated files when this function is called.
The event function we registered with tapAsync accepts two arguments:
-
The first parameter, the Compilation object, represents the related objects for this build
-
The callback argument corresponds to the asynchronous event function we registered with tapAsync, and when callback() is called, the registered event is completed.
Having registered an event function when packaging the outgoing file, let’s populate the function with more concrete business logic — get the resources for this compilation and use JSZip to generate the compressed package for output to the final directory.
const JSZip = require('jszip');
const { RawSource } = require('webpack-sources');
/* Get all the packaged resources */
const pluginName = 'CompressAssetsPlugin';
class CompressAssetsPlugin {
constructor({ output }) {
this.output = output;
}
apply(compiler) {
// AsyncSeriesHook is called before writing assets to output
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) = > {
// Create a zip object
const zip = new JSZip();
// Get all assets generated by this package
const assets = compilation.getAssets();
// Loop through each resource
assets.forEach(({ name, source }) = > {
// Call the source() method to get the corresponding source code
const sourceCode = source.source();
// Add the resource name and source code content to the zip object
zip.file(name, sourceCode);
});
GenerateAsync generates a zip package
zip.generateAsync({ type: 'nodebuffer' }).then((result) = > {
// Create a compressed package using new RawSource
// Also output the generated Zip package to this.output through the compilation.emitAsset method
compilation.emitAsset(this.output, new RawSource(result));
// Call callback to end the event functioncallback(); }); }); }}module.exports = CompressAssetsPlugin;
Copy the code
I annotated each line of the code above, but the main idea was to control the packaging results by manipulating the Webpack Compilation Api in conjunction with Compiler Hook.
There are two things I want to emphasize:
- For the parameters returned by compilation.getAssets(), we pass
asset.source.source()
Method to get the source code for the module to be generated.
You might wonder what exactly is returned from compilation.getAssets(). Before, it was hard to get a clear grasp of how the various apis in WebPack should be utilized without going deep into webpack source code.
But TypeScript changes that. When you need to look up an object or method on a temporary basis, you cantypes.d.ts
Quickly look up the corresponding methods and properties.
We can clearly see the source attribute on the Asset resource. We can get the contents of the corresponding resource module through source.source(), which stores a string | buffer.
Of course, if you are interested, I strongly suggest that you can take the time to read part of the source code along with the Webpack auxiliary article. You can also refer to the whole process analysis of the core packaging principle of Webapck5 in the source code process of Webpack.
- webpack-sources
This library is a WebPack built-in library that contains Source and a series of source-based subclass objects.
Here we create a resource file (Source) object using new RawSource() that does not require sourceMap mapping and output the corresponding resource through compilation.emitasset (name, Source).
Now that our CompressAssetsPlugin has been implemented, let’s run a package command to verify the result:
You can see that a result.zip is printed in the output configuration directory after packaging.
At this point, we can implement some simple Webapck plugins, and then I will take you into the Plugin mechanism to implement a more complex Plugin.
ExternalWebpackPlugin
Next I will take you through the development of a slightly more complex Plugin that will involve some knowledge of the AST (Abstract syntax tree). But it’s beside the point, and it doesn’t matter if you don’t know much about it.
background
There is a configuration option for externals in Webpack:
module.exports = {
/ /...
externals: {
jquery: 'jQuery',}};Copy the code
Externals means “excluding dependencies from the output bundle.”
For example, if the dependency module jqery is found during module compilation using the configuration Webpack above, jquery will not be packaged into the module dependency, but will be assigned to the jquery module as an external module dependency using jquery from the global object.
A more detailed configuration of externals can be found here.
For those of you who are still confused about externals for the first time, let’s take an example.
For example, using the configuration file above, there are module dependencies in the code like this:
import $ from 'jquery'
Copy the code
When Webpack encounters the introduction of a jquery module, it does not package the jquery module dependency code into the business code. Instead, it looks for jquery as an external module in a variable named jquery based on the externals configuration.
This is the packaged code in import statement development mode above, and we can see that Webpack handles it as module.exports = jquery for the jquery module.
The externals configuration is usually particularly useful when packaging some libraries with WebPack, so let’s move on to the requirements analysis of the ExternalWebpackPlugin once we understand its use.
Demand analysis
Let’s first talk about the requirements that ExternalWebpackPlugin needs to implement.
Source externals configuration mode
In general, if we need to import some internal dependency modules as CDN instead of packaging them in business code using externals, we need to go through two steps:
- Externals is configured in the WebPack configuration.
For example, if we use Vue and Lodash libraries in our code, we do not want to package these two libraries in the business code, but want to introduce them into the generated HTML file in the form of CDN. We need to do this:
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development'.entry: {
main: path.resolve(__dirname, './src/entry1.js'),},devtool: false.output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js',},externals: {
vue: 'Vue'.lodash: '_',},plugins: [
new HtmlWebpackPlugin({
template: '.. /public/index.html',})],}Copy the code
We have configured externals in webpack.config.js to tell WebPack that if it encounters the introduction of vue or Lodash modules during packaging, it does not need to package the contents of these two modules into the final output code.
In the global context, the Vue variable is assigned to the Vue module, and the _ is assigned to the Lodash module.
At this point we’ve configured externals, but that’s not enough. Since the two global variables Vue and _ do not exist in the packaged and compiled code at this time, we need to add the CDN links corresponding to these two modules in the HTML file generated at last.
- The generated HTML file injects the CDN in externals to configure external links.
HtmlWebpackPlugin is used to specify the template for the generated HTML file. Let’s look at the HTML file public/index.html:
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<! -- Manually import the corresponding module CDN -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>
<body>
</body>
</html>
Copy the code
If we want to introduce some modules in business code as CDN, we usually need to go through the necessary two phases.
Existing problems
In my opinion, there are the following two unreasonable points:
- First of all, the configuration steps in my opinion should be as simple as possible.
If we need to change the dependent module into CDN form, we need to synchronize the modification in externals and the generated HTML file every time, which undoubtedly increases the tedious steps.
- Secondly, there may be the problem of CDN redundant loading.
If loDash is used within the same project, I want to package the LoDash modules used within the project as external dependencies.
I may not be using LoDash at this point, but there is no guarantee that other developers in the project are using LoDash. When I configure LoDash in externals, I have to introduce the CDN of LoDash into the HTML file.
It’s possible that we didn’t end up using LoDash in the project, but we still redundantly introduced its CDN in HTML. Of course this can be done orally before going live or with team members, but wouldn’t it be nice to be more intelligent?
In view of the above two existing problems, let’s conceive a plug-in to solve these two problems.
Design the plug-in
First, let’s look at the way we need to write the plugin:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExternalsWebpackPlugin = require('./plugins/externals-webpack-plugin');
module.exports = {
mode: 'development'.entry: {
main: path.resolve(__dirname, './src/entry1.js'),},devtool: false.output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js',},externals: {
vue: 'Vue'.lodash: '_',},plugins: [
new HtmlWebpackPlugin(),
new ExternalsWebpackPlugin({
lodash: {
/ / the CDN links
src: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js'.// Replace the module variable name
variableName: '_',},vue: {
src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js'.variableName: 'vue',},}),],};Copy the code
Here we deduce the process from the results, and we conceive an ExternalsWebpackPlugin to solve the above two problems.
Parameter design
Let’s talk about plugin parameters first:
{
lodash: {
/ / the CDN links
src: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js'.// Replace the module variable name
variableName: '_',},vue: {
src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js'.variableName: 'Vue',}}Copy the code
We support passing in an Object as a parameter with each attribute named the module name that the code needs to handle as an external dependent module.
For example, the loDash attribute in the passed object above indicates that if we introduce a dependency named LoDash into our code, I will use the LoDash dependency as an external dependency module.
The lodash module is also replaced with the variableName property, similar to externals: {lodash: ‘_’}.
Finally, it will help us generate CDN links by dynamically injecting SRC attribute values into the generated HTML file.
The problem the plug-in needs to solve
- The configuration steps are simplified.
In view of the above mentioned introduction of external chain using CDN originally requires two steps, we can design a plug-in to simplify this step by passing parameters in the plug-in.
- CDN redundant loading.
In view of the redundancy that may be caused by the source-generated CDN mode, we will save the external dependent modules only used in the code through the analysis of AST abstract syntax tree in ExternalsWebpackPlugin, and only inject the CDN links of these used modules when generating HTML files.
The principle of analysis
Next I’ll take you through some of the webpack principles used by the ExternalWebpackPlugin.
- NormalModuleFactory Hook
The NormalModuleFactory module is a module that is closely related to generating modules, and it is through this module that compiler objects process compiled module requests.
For module request processing, we can change the logic when importing modules by registering events through a series of hooks in NormalModuleFactory.
Import vue from ‘vue’. Simply put, such an import statement is a module request.
We need to register the event function with NormalModuleFactory Hook. When webPack processes a dependency module within the module, the corresponding Hook will be triggered to determine: If the incoming module-matching plug-in needs to be passed in as an external dependency module, then it will not compile directly as an external module.
- JavaScriptParser Hook
NormalModuleFactory has several hooks for module processing. The NormalModuleFactory Hook object also has a HookMap attribute parser.
I’ll explain the Parser property a little bit here. Compiler objects use the NormalModuleFactory module for module processing.
Since it is module introduction, there is essentially a depth-first process inside Webpack to identify and introduce new modules that need to be compiled, which is what Parser does.
The parser hook on the NormalModuleFactory property is a list of hooks provided to plug-in developers when compiling the AST for each module.
Here, JavaScriptParser Hook is used internally to analyze the dependency module import statements of imported modules, and judge them when AST is generated to save the external dependency modules used.
By saving only used external dependencies, we mean that if we don’t use LoDash in our code and we pass in the CDN configuration of LoDash in the plugin parameter, then we analyze the code through the AST. If I don’t get a module import statement like import _ from ‘lodash’/const _ = require(‘lodash’), then I won’t generate the corresponding CDN link in the HTML file.
Acron syntax is used internally in Webpack to parse and process the abstract syntax tree.
If you don’t quite understand HookMap you can check it out here.
You can also check out the rules generated by the AST online.
- HtmLWebpackPlugin Hook
We need to rely on HtmlWebapckPlugin to compile the final output HTML file.
HtmlWebpackPlugin through HtmlWebpackPlugin. GetHooks (compilation) method to expand the number of column hook other plugin developers to inject logic in the generated HTML file, You can check this picture for specific Hook and timing:
Image from HtmlWebapckPlugin NPM address.
Here, we will use the Hook provided by this plug-in to automatically inject the CDN of the external module in the generated HTML file.
NormalModuleFactory and JavaScriptParser
NormalModuleFactory Hook and JavaScriptParser Hook for the first time, some of you may not know very much about them. Here I would like to talk with you a little bit.
I will try to be simple to talk to you about the trigger time of this object hook. For example, the entry file of the packaging code we need is as follows:
// index.js entry file
import module1 from './module1'
import module2 from './module2'
Copy the code
Webpack will first enter the entry file, where the relevant hooks registered on the NormalModuleFactory Hook are the processing hooks for module resource requests.
JavascriptParser Hooks parse module contents using AST only when the dependency file needs to be compiled using NormalModuleFactory Hook after entering the entry file.
If you start the NormalModuleFactory hook and determine that the module does not need to be compiled, you will not go to the module dependent parser stage.
Take the above as an example:
-
To run the compile command, first analyze the entry file index.js module request and call the NormalModuleFactory Hook part of the Hook.
-
The index.js file (which is also a Module) is compiled, an AST is performed, which is what the Parser instance object is for, and a series of JavascriptParser Hooks are triggered when the module (index.js) is analyzed.
The two steps are repeated when import module1 from ‘./module1’ is encountered.
NormalModuleFactory is used to generate modules, and AST processing of the current module is an essential part of generating and compiling modules. . So we can pass NormalModuleFactory hooks. The parser. Somehook registered the event when a AST processing function, you can understand it so simple.
Or the relationship between the parent set and the subset, if that makes any sense to you.
implement
Foreshadowing so much, let us follow the train of thought step by step to achieve it!
Initialize the
First, let’s look at the plug-in initialization phase:
const pluginName = 'ExternalsWebpackPlugin'
class ExternalsWebpackPlugin {
constructor(options) {
// Save the parameters
this.options = options
// Save all library names that need to be converted to externals outside the CDN
this.transformLibrary = Object.keys(options)
// Parsing dependencies are introduced to save libraries used in code that need to be converted to an external CDN
this.usedLibrary = new Set()}apply(compiler) {
// do something}}module.exports = ExternalsWebpackPlugin;
Copy the code
At the beginning of the ExternalsWebpackPlugin, in the plugin builder we initialize the parameters that the plugin needs to use:
- this.options
Needless to say, this saves configuration objects that are passed in from the outside.
- this.transformLibrary
Save the names of the dependencies in the code that need to be converted to the CDN form, here converted to [‘lodash’,’vue’].
- this.usedLibrary
It is a Set object that stores the external dependency libraries used in our code. For example, if we pass in loDash and vue as plugin arguments but only use and vUE instead of LoDash in our code, then only one VUE will be stored in this object.
Transforming external dependencies
The next thing we need to do is process the request in our module, for each module when we package.
If this. TransformLibrary contains this module, we need to skip compilation of the imported module and convert it to external dependencies.
Let’s take a look at the code:
const { ExternalModule } = require('webpack');
const pluginName = 'ExternalsWebpackPlugin';
class ExternalsWebpackPlugin {
constructor(options) {
// Save the parameters
this.options = options;
// Save all library names that need to be converted to externals outside the CDN
this.transformLibrary = Object.keys(options);
// Parsing dependencies are introduced to save libraries used in code that need to be converted to an external CDN
this.usedLibrary = new Set(a); }apply(compiler) {
// normalModuleFactory will trigger the event listener after it is created
compiler.hooks.normalModuleFactory.tap(
pluginName,
(normalModuleFactory) = > {
// called before initialization of the parse module
normalModuleFactory.hooks.factorize.tapAsync(
pluginName,
(resolveData, callback) = > {
// Get the name of the imported module
const requireModuleName = resolveData.request;
if (this.transformLibrary.includes(requireModuleName)) {
// If the current module needs to be treated as an external dependency
// First get the variable name to which the current module needs to be transposed
const externalModuleName =
this.options[requireModuleName].variableName;
callback(
null.new ExternalModule(
externalModuleName,
'window',
externalModuleName
)
);
} else {
Does normal compilation need to handle nothing for external dependenciescallback(); }}); }); }}module.exports = ExternalsWebpackPlugin;
Copy the code
At first glance, you might be a little confused by the above code, but it’s okay to learn what you don’t know. Let’s examine the above code a little bit.
First compiler. Hooks. NormalModuleFactory. Tap we registered the first function to an event after the compiler to create normalModuleFactory module.
When this function is called, WebPack passes the NormalModuleFactory object as an argument, and we can use the hooks on the NormalModuleFactory to listen for the compiler object’s hooks when processing modules to implement the logical processing.
NormalModuleFactory. Hooks. Factorize, this hook will in normalModuleFactory call before initializing the formation, function of its event listeners will accept a resolveData as parameters.
In a nutshell, we have registered an execution function here via the hook that Webapck provides to developers, This function is called after the Compiler object creates the NormalModuleFactory and before the NormalModuleFactory initializes each module.
In other words, let’s say we have code that says:
import Vue from 'vue'
At this point, When WebPack parses this code and encounters a module request (VUE), it calls our registered function before the initial parse.
ResolveData: resolveData:
This is the resolveData type definition in the source code, you can print it out at runtime.
Here we need to use the Request property in resolveData, which represents the name of the module currently being resolved.
For example, if you import Vue from ‘Vue’, resolveData.request gets’ Vue ‘.
Let’s take a look at this code:
const { ExternalModule } = require('webpack');
// ...
normalModuleFactory.hooks.factorize.tapAsync(
pluginName,
(resolveData, callback) = > {
// Get the name of the imported module
const requireModuleName = resolveData.request;
if (this.transformLibrary.includes(requireModuleName)) {
// If the current module needs to be treated as an external dependency
// First get the variable name to which the current module needs to be transposed
const externalModuleName = this.options[requireModuleName].variableName;
callback(
null.new ExternalModule(externalModuleName, 'window', externalModuleName)
);
} else {
Does normal compilation need to handle nothing for external dependenciescallback(); }});// ...
Copy the code
We obtained the dependency module name of requireModuleName inside the function. At this time, we first checked whether the module to be resolved needs to be processed as externals external module.
- If you do not need to handle the externals module, the module is not in this. TransformLibrary.
At this point, we call the second callback argument of the registration function directly, and do no logical return to indicate that the Compiler object continues to process the module.
- If you want to handle the externals module, the module is stored in this.transformLibrary.
Options [requireModuleName]. VariableName is passed in to the module during configuration.
Callback (null, new ExternalModule(externalModuleName, ‘window’, externalModuleName)) Telling Webpack that this module doesn’t need to be compiled, I return you an instance object of ExternalModule, which is treated as an external dependency.
As for the second sentence, those of you who are not familiar with plug-in developers may not know what it means. Here I tell you a little bit about the meaning:
- First, in tapable’s asynchronous registration method tapAsync’s listener function, callback() is called to indicate that the asynchronous listener function is finished.
Callback (error,result) takes two arguments:
If there is an error we pass an error message to the first argument, obviously we have no error here we just pass null.
The second parameter represents the return value of the event function. If the event function has a return value, WebPack will process the module with the return value of the registration function instead of the module content.
Here we need to modify the original logic of how WebPack handles this module to make it an external dependency module, so we return an instance of ExternalModule to tell Webpack that this module is an external dependency module.
- Secondly about
new ExternalModule(externalModuleName, 'window', externalModuleName)
。
We can create an external dependency module using ExternalModule built into WebPack. Its constructor takes three arguments:
The first argument, request, represents the name of the variable generated by the ExternalModule external dependency module when it is created. For example, if loDash is an external dependency module and we need to fetch it from _, we pass in _.
The second parameter type indicates the object in which the variable corresponding to the first parameter is mounted when ExternalModule is created. For example, when we import loDash via CDN, we refer to lodash as window._, and the _ of the first argument is mounted under the window object. By default, the first variable is fetched directly from global.
The third parameter, userRequest, indicates that WebPack generates a unique moduleId name when packaging the file, which is generated automatically by default. The moduleId can be simply referred to as the generated moduleId after packaging.
If you are interested in learning more about the packaging process, you can check out my Webapck5 core packaging principle full process analysis.
Let’s take a look at what the so-called ExternalModule looks like with WebPack wrapped:
The so-called return new ExternalModule(externalModuleName, ‘window’, externalModuleName), using lodash as an example returns the module shown in the figure.
When a lodash module is used in the code, Webpack goes to window[‘_’] to find the module’s contents.
Eliminate unused modules
The next step is to make a judgment call when generating the AST, save only the external dependencies that are used, and discard modules that are passed in by the plug-in configuration but not used in the code.
For a better understanding, we’ll break this code into two parts:
// ...
apply(compiler) {
// normalModuleFactory will trigger the event listener after it is created
compiler.hooks.normalModuleFactory.tap(
pluginName,
(normalModuleFactory) = > {
// Call the matching module as external externalModule before initializing the parse module
normalModuleFactory.hooks.factorize.tapAsync(
pluginName,
(resolveData, callback) = > {
// Completed logic...});// Triggers an AST phase call when the module is compiled
normalModuleFactory.hooks.parser
.for('javascript/auto')
.tap(pluginName,(parser) = > {
// when the module import statement import is encountered
importHandler.call(this, parser);
// when the module introduces the statement require
requireHandler.call(this, parser); }); }); }// ...
Copy the code
We’re in the same code above normalModuleFactory object to monitor a parser hook, different is normalModuleFactory hooks. It is a hookMap parser properties of
Hookmap. for(‘javascript/auto’) is used to find a hook named ‘javascript/auto’. The hookMap hook from parser is available here.
The ‘javascript/auto’ hook from Parser is simply executed when the PARSER on a Complier object compiles a JS file.
Therefore, we registered the corresponding event function through JavaScriptParser hook. When WebPack converts JS files into AST, it calls the listener function that performs the registration.
Let’s look at the next importHandler/requireHander these two functions:
// ...
function importHandler(parser) {
parser.hooks.import.tap(pluginName, (statement, source) = > {
// Parse import statements in the current module
if (this.transformLibrary.includes(source)) {
this.usedLibrary.add(source); }}); }function requireHandler(parser) {
// Parse the require statement in the current module
parser.hooks.call.for('require').tap(pluginName, (expression) = > {
const moduleName = expression.arguments[0].value;
// when the module passed is used in the require statement
if (this.transformLibrary.includes(moduleName)) {
this.usedLibrary.add(moduleName); }}); }// ...
Copy the code
Part of AST is covered here, but it’s simple.
We’ll start with importHandler, which takes a paser object as an argument. Parser.hooks. Import.tap, The event function registered by this hook is called for every import statement encountered during code parsing (converting code into an AST).
For example, when encountering a module request statement import _ from lodash inside a module, WebPack converts this code into something like this during parsing:
The parser.links.import. tap registered event function is called after the conversion, passing in:
-
Statement, the entire ImportDeclaration object in the preceding statement.
-
Source, which represents the name of the imported module, such as import _ from ‘lodash’, whose value is lodash.
The logic inside the function is actually not complicated. During module parsing, we register the listener function and obtain the source value passed in when the import statement is parsed to determine whether the currently introduced module has this.transformLibrary. We’ll add it to this.usedLibrary.
The requireHandler function is much the same as importHandler logic, except that it internally handles the importHandler statements introduced by require.
The structure of the AST transformation can be printed out in code or viewed on this online site.
After all modules have been parsed, the names of the external dependencies used in the code are stored in this.usedLibrary. Also, don’t forget that it is a Set object, so the interior is never repeated.
Inject CDN scripts
The logic we inserted above through a series of module analyses has been completed:
-
Convert the matched module to an external dependency externals.
-
Only the external dependency library name used in the code passed in is retained.
Next, let’s complete the final step by inserting the CDN external link corresponding to the module being used when generating the final HTML file based on the contents of this.usedLibrary.
Let’s take a look at the implementation code:
const HtmlWebpackPlugin = require('html-webpack-plugin');
/ /...
apply() {
// ...
compiler.hooks.compilation.tap(pluginName, (compilation) = > {
// Get the compilation Hooks of the HTMLWebpackPlugin extension
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(
pluginName,
(data) = > {
// Add additional scripts
const scriptTag = data.assetTags.scripts
this.usedLibrary.forEach((library) = > {
scriptTag.unshift({
tagName: 'script'.voidTag: false.meta: { plugin: pluginName },
attributes: {
defer: true.type: undefined.src: this.options[library].src, }, }); }); }); });// ...
}
Copy the code
Here we use the HtmlWebpackPlugin to provide an additional extension of the alterAssetTags hook on the Compilation object.
When the HTML file is finally generated, loop through this.usedLibrary, loop through external dependent CDN links, and add CDN links to the HTML file.
About the data. AssetTags. Scripts:
This is the print of it, and so far only one script has been saved which is the JS file generated from the package of the project entry file.
What we’re doing here is we’re adding the corresponding CDN tag to this object, and when the package is complete the HTML file will generate the corresponding script tag based on the content of assettags.scripts.
I’m going to write ExternalsWebpackPlugin. We’ve implemented all the logic for it, and it’s not that hard, right?
MSC
Next I’ll take you to verify our ExternalsWebpackPlugin.
We use webpack.config.js to pass in the configuration of vue and LoDash libraries in ExtendsPlugin.
Also, in the entry file SRC /entry1.js, just introduce the Lodash module:
import _ from 'lodash';
Copy the code
Let’s rerun the package command to see the result:
This is the output index. HTML file. We configured two CDNS in the plug-in configuration, but because the code did not use VUE internally, the final HTML file only mounted the CDN link used in LoDash.
This is a snippet of the webPack generated js file. You can see that we have achieved the desired effect for the LoDash module, which does not compile LoDash into the final output but looks for it in the form of an external dependency module called Window [‘_’].
At this point, the ExternalsWebpackPlugin is complete!
Written in the end
First of all, thanks to everyone who can see this, Webapck plug-in development is new to most front-end developers.
This article may be unfamiliar to those who are new to plug-in development, but on the other hand, learning is a process of starting from scratch.
In the face of massive front-end engineering, a short Webpack article is not enough.
The author’s wish is more hope that we take this as a starting point, put into practice to find the optimization point in the business.
I will bring you more front-end engineering practice in the Webpack column from the principle of play, interested friends continue to pay attention to.