State of the art for Webpack: Loader #482
h-a-n-a
started this conversation in
Show and tell
Replies: 1 comment
-
nice summary of webpack's implementation |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Summary
Explain how webpack loader works. Even though it's a little bit long and tedious, It's still a teeny-tiny peek at the loader system of Webpack.
Glossary
Request Related
!style-loader!css-loader?modules!./style.css
request
: The request with inline loader syntax retained. Webpack will convert relative URLs and module requests to absolute URLs for loaders and files requested. e.g.!full-path-to-the-loader-separated-with-exclamation-mark!full-path-to-styles.css
Resource Related
resource
: The absolute path to the requested file withquery
andfragment
retained but inline loader syntax removed. e.g.absolute-path-to-index-js.js?vue=true&style#some-fragment
resourcePath
: The absolute path to the requested file only. e.g.absolute-path-to-index-js.js
resourceQuery
: Query with question mark?
included. e.g.?vue=true&style
resourceFragment
: e.g.#some-fragment
inline match resource:
module.rules
to another, which is able to adjust the loader chain. We will cover this later.virtualResource
:Others but also important to note
data:
. Also known as,data:
import. Doc to Node.js'javascript/auto'
|'javascript/dynamic'
|'javascript/esm'
|'json'
|'webassembly/sync'
|'webassembly/async'
|'asset'
|'asset/source'
|'asset/resource'
|'asset/inline'
, for those types you can use it without a loader. From webpack version 4.0+, webpack can understand more thanjavascript
alone.Guide-level explanation
Loader configuration
The way that webpack controls what kind of module that each loader would apply is based on
module.rules
Here is a simple option for the configuration of
vue-loader
.module.rules[number].test
is a part rule to test whether a rule should be applied. Forvue-loader
alone, It's kind of confusing how webpack pass the result to the rule ofcss
, we will cover this later. But for now, It's good to notice there is not only atest
option alone to test if a rule should be applied. You can find it here for full conditions supported. Here're some examples of other conditions you can use.Examples
Vue(1 to n)
In a single file component(SFC) of Vue, there are commonly three blocks or more blocks(custom blocks) contained. The basic idea of implementing this loader is to convert it into JavaScript / CSS and let webpack handle the chunk generation(e.g. Style should be generated into a separate
.css
file)⬇️⬇️⬇️⬇️⬇️⬇️
Vue-loader
will firstly turn into the*.vue
file into something like that.You may find it weird how webpack handles these imports and build the transformed code. But if I change the code a little bit, you will find the idea.
and if we tweak the configuration a little bit to this, webpack will know exactly how to work with these import statements.
We added a few loaders to handle the splitting. I know it's still kind of weird here, but please stick with me and we will find a better way out.
script
block from a SFC file.style
block from a SFC file.template
block from a SFC file and convert it into JavaScript.You will find it's really noisy only to transform a
*.vue
file, four loaders were introduced and I believe none of you would like to separate a simple loader into four. It's a real bummer! It will be great to use a single loadervue-loader
alone. The current vue loader implementation uses resourceQuery to handle this. But how?Loader optimizations I
We know that webpack uses a few conditions to handle whether a rule should be applied. Even with
rule.test
alone, thethis.reousrceQuery
is still available toloaderContext
which developer could access it withthis
in any loader function(Don't worry if you still don't catch this. You will understand this after). Based on that, we change therule
to this:This indicates "If an import specifier is encountered, please pass me to vue-loader"! If you remember the import transformation above, we could adjust the transformation a little bit to this:
Before
After
These requests will match the
test: /.vue$/
above flawlessly and in the loader we can handle like this:You can see the loader for the example above will be used for four times.
*.vue
file, transform the code to a few import statementsvue
.Is this the end? No! Even if you wrote the code like this, it will still fail to load.
css-loader
and thenmini-css-extract
(if you want to generate CSS for chunk) orstyle-loader
(if you want to append it directly to the DOM). After all, you have to make the result of style to pass these loaders.Pass the code to the corresponding loaders
We tweak the configuration a little bit again.
It looks a bit more like the "normal" Webpack configuration. Note that the
rule.test
is based on the file extension, sovue-loader
did a little bit of hack here.Webpack uses
resourcePath
to match amodule.rules
. So this hack will let webpack treat blocks accordingly as if they are real files with extensions ofjs
|css
|...
.Finally! But this is only a proof of concept, for the real implementation. You should definitely check out the
vue-loader
yourself.Loader Optimization II
Well done! We implemented a simple and rudimentary version of
vue-loader
. However, the real pain-in-the-ass part of this implementation is hacking the extension to match the configuration. But since almost every user would have otherjs
|css
files included in the project, so vue team decide to use this kind of strategy to reuse the user configuration.Except for hacking the extension, webpack then provided a more legit way to handle this kind of rule matching problem which is known as inline match resource (We covered it in the glossary part).
inline match resource
Webpack can do almost anything with an import specifier like the loader chaining we covered in the glossary part. Inline source match is another case. By taking the advantage of it, you can force an import statement to go through a
module.rules
by introducing the!=!
syntax. For example, if we want to force acss
file to go through aless
loader, it will be look like this:The slice before the
!=!
is a way to modify the extension of a single file and force it to match themodule.rules
and this transformation is often done in a loader, or you will make your application code specialized for Webpack only.After going through the basic example, let's see how we're going to optimize out the hack used in
vue-loader
.Webpack will internally use the match resource part(before
!=!
) as the data to match loaders. In order to letvue-loader
match the resource. We have two options:1. Loose test
We removed the
$
to allow resources with.vue
included matching this rule. Personally speaking, this is not a good idea, because a loose match might cause mismatches.2. Inline loader syntax
This technique is to take advantage of the inline loader syntax to force the loader to go through the vue loader. This tackles down the tangible mismatching ideally and we can still retain the test regex
/\.vue$/
as-is.Final art and conclusion
Configuration
Loader
Conclusion
Vue-loader is quite complex. The basic needs of the loader are:
*.vue
file request into a number of parts. For each block, explicitly change the resource matching mechanism (using inline match resource). The killer inline match resource not only gives us great composability with user-defined loaders, but also the ability to interact with webpack supported native types, and we will cover this part late.vue-loader
again for a block, the code of each block is returned and let webpack handle the changed matched resource(e.g../App.vue.css
) with user-defined loaders (Webpack did this internally).Use natively supported module types
We know that webpack only supports
JavaScript
in the old time, from the version of4.0.0
+(changelog)Simplified pre-processor's configuration
Before
After
With
experiments.css
on, webpack can experimentally understand the parsing and generating ofcss
files which gets rid ofcss-loader
andstyle-loader
. For the full list of natively supportedRule.type
, you can find it here.Asset modules
Rule.type === "asset"
indicates the asset will be automatically tested whether it's going to be inlined or emitted as a file on the real file system. The possible options are:'asset'
|'asset/source'
|'asset/resource'
|'asset/inline'
Svgr
Webpack loader will read the source to a UTF-8 string by default. For SVG files, this would fit the webpack load defaults.
Again here we use double-pass to firstly convert each request to the request part with inline match resource, and do the real request with query
?svgr=true
, and let inline match resource handle thejsx
conversion. Before that, we have to call a third-partyjsx
transformer, could be ESBuild for example, for which we cannot reuse othermodule.rules
set by the user-side. Inline match resource saved our ass again!Scheme imports
Webpack handles
data:
imports for JavaScript internally.Asset transform and rename
Default resource reading override
Asset could be formatted in both text(
*.svg
) or binary (*.png
/*.jpg
). For loaders, webpack provides you an optionraw
to override the default and built-in resource reading strategy from UTF-8string
toBuffer
:Transform and rename
Image there is a need to transform an asset formatted with
png
tojpg
. There is two abilities that webpack needs to support:raw
content, or aBuffer
. We can simply override the defualt resource reading behavior by exportingraw
(covered before).png
andjpg
Configuration
png
, we want to use apng
tojpg
loader, which will be covered in this article.jpg
, we want to use a third-partyjpg-optimizer
, which will not be covered in this article.type: "asset/resource"
: As soon as all the loaders have gone through, we want webpack to emit the file as an external resource on the file system regardless of the file size(type: "asset"
will automatically detect the size of an asset to determine whether an asset will be inline-included for dynamically imported from file system).jpg
files converted frompng
, we want them to apply with thejpg-optimizer
too(i.e. reuse the loaders defined inmodule.rules
)Loader
We use double-pass again, firstly we convert the extension to
.jpg
which will apply the matched rules(in this casetest: /\.jpg/
), after the transformation ofpng-to-jpg-loader
. Generated asset module filename will be based on the inline match resource, which isxxxx.jpg
in this case.AST reuse
Webpack provides a way to pass metadata(the forth parameter) among the chaining loaders doc. The most commonly used value is
webpackAST
which accepts anESTree
compatible(webpack internally usesacorn
) AST, which hugely improves the performance since webpack instead of parsing the returned code to AST again, will directly use the AST(webpackAST
) returned from a loader(But the work of a complete walking of an AST can not be omitted as it's necessary for webpack for do some analysis for its dependencies and will be only done once, so it is not a big overhead.)Good to note that only
ESTree
is compatible, so you cannot pass a CSS AST, or webpack will complain with"webpackAst is unexpected for the CssParser"
. It will be ok if you don't get this, let's move to the reference-level explanation for analysis in-depth.Reference-level explanation
This is the reference-level explanation part of webpack's internal loader implementation.
Loader composability
The high-level idea of previously talked inline match resource is to let loader developers to customize the behavior of matching to match the pre-defined
module.rules
. It's an API to write composable loaders. But what does composition mean? For those users who are familiar with React hooks and Vue composable APIs, you may get this faster. Actually, webpack provides a lot of ways to help loader developers and users do the composition.User-defined loader flows
Webpack users can take the advantage of
module.rules[number].use
with a loader list for each request that matches the corresponding conditions. Note that I use the wording ofrequest,
not thefile
, which can include a request todata:text/javascript
not the files on the real file system only. (In Parcel bundler, it's called pipelines, but this will not be covered in this article.)Apparently, user-declared loader flow is not able to cover up every case that a loader wants. You can see from the previous examples,
vue-loader
wants to split a file into many blocks, and remain the reference to it.svgr-loader
wants to do the transformation first and let other loaders deal with thejsx
.svg-loader
wants to use the internal ability ofAsset Module
to let Webpack decide whether an asset is inlined or emitted to the real file system. and there are more to come... Based on the complexity of the loader, Webpack also provides a syntax to allow loader implementors to do the composition by themselves.The syntax for loader composition
Inline loader syntax (Chaining loaders)
The inline loader syntax executes each loader for each request from right to left. Webpack handles the interaction with user-defined loaders carefully. So by default, the user-defined normal loader will be executed prior to the inline loaders, you can disable this behavior by prefixing
!
, (full reference could be found here doc).The custom specifier is parsed before the
module.rules
as the inline loader syntax interferes the user-defined loaders(See the source code). Then, webpack will get themodule.rules
combined with the required conditions to calculate the matching rule set (See the source code).At the moment, you cannot change the matching behavior with the syntax, loaders are always matched with the provided resourcePath, etc, which leads to a bunch of hack code in the implementations of loaders (see this code snippet in
vue-loader
). The possibilities for changing the matching behavior leaves to the later-coming inline match resource.Nevertheless, the architecture of Loader at this moment is sound and solid. Another good example is the implementation-nonrelative filter(i.e. the filtering logic of Loader is not declared in the loader itself), which is the fundamental root of loader composition, or the implementor will do a lot of hacks. (It's way too dirty to talk about here, but you can take the rollup svgr plugin as a reference)
In conclusion, inline loader syntax gives us a chance to control the loader flow with user-defined rules.
Inline match resource
To extend the matching ability, inline match resource enables loader implementors to reuse some of the user-defined configurations with more flexibilities.
On top of the previous example, webpack also provides a way to make use of the natively-supported module types.
Given the configuration above, the overview of the complete flow will be like this:
module.rules
inwebpack.config
, in this case is[]
style.less
as UTF-8 stringcss
.CSS
parser, and later at the code generation step the registered nativeCSS
generator generates the result.For asset modules, you can also use this:
The first part, also known as
matchResource
will be used as a part of thefilename
of the final code generation. (See the source code)Performance optimizations
Before moving on to the detailed implementations, here's some glossary to support your understanding the architecture as a whole.
Glossary
NormalModuleFactory
: A factory used to create aNormalModule
, which basically exposes acreate
method.NormalModule
: A module in Webpack most of the time is aNormalModule
, but with different implementations ofparser
/generator
/Module Type
, the module could be almost any kind, and also exposes abuild
method. For example, aNormalModule
with JavaScript parser, JavaScript generator, andtype ===javascript/auto
will be regarded as a module with JavaScript-related functionalities. Also, good to note that a module may not exist on the real file system, takingdata:
for example.The module creation workflow
When an import statement is detected, webpack will initialize a module creation. Based on the type of Dependency (an abstraction of webpack, it's not important here), webpack can find the linked ModuleFactory(The abstraction class), in most cases, the derived factory is
NormalModuleFactory
, which exposes acreate
method.Prepare data needed for module creation
The
NormalModuleFactory#create
is used to provide enough information to create a realNormalModule
, and create theNormalModule
. In thecreate
method, webpack basically does these things(some non-loader related stuff will be omitted):normal/post/pre
loader is going to be included. docLoaderContext
). For the full source code you may refer to thismodule.rules
defined in the configuration, and get the matched rules. This is also a part of the module creation data..webpack[css]
would changeRule.type
. Also store the match resource data, since it might affect the filename generation for asset modules.Create a module based on the prepared data
After the data needed for module creation is prepared,
NormalModuleFactory
willnew NormalModule
with the data provided. It contains basically every that aNormalModule
needs (see the source code). Most importantly, theloaders
. It contains every loader parsed and ordered from thecreate
step.The module build step
The module build step is kind of clear. Webpack will invoke the
build
method for eachNormalModule
instance, which invokesloader-runner
(see the source code) to go through every loader that was analyzed from the create step. It's clear to know that the composition of loaders is happening on the same module.A peek of the support of Module Types
As far as this article goes, It might be getting a little bit tedious. But have you ever wondered how webpack supports these module types natively? I think It's still worth telling you about it to get a more complete understanding of the AST optimizations. For the support of JavaScript, webpack's JavaScript plugin will register different types of parser and generators for each module types, which will be used as the
parser
/generator
to aNormalModule
(see the source code).Reusing AST in Webpack
Based on the parser and generator we introduced before, webpack did a little hack around the fourth parameter of
this.callback
(from loaderContext), withwebpackAST
, after each loader call, thewebpackAST
will be stored in the context of loader, and passed again to the next loader. Finally, the AST will be passed to theparser
(It could be any type, based on the module type, but webpack makes it a JavaScript only for AST) (see the source code).Here's an issue about trying to use SWC's AST to get rid of the time sensitive code parsing from Acorn Parser, but they are facing some AST compatibility issues and performance issues about the overhead of interop with native code(Rust).
References
loader plugin api design (Analysis) #315
RFC-011 Supports
data:text/javascript
protocol #457Webpack:
matchResource
with natively-supported module types docWebpack: Loader context doc
Webpack: Module rules doc
SWC-loader for performance optimizations issue
Beta Was this translation helpful? Give feedback.
All reactions