Loader
is one of the core features of Webpack
, which is used to convert different types of files into modules that Webpack
can recognize. In other words, it transforms the original content of the modules into new content as needed to load non-js
modules. By working in conjunction with extension plugins, it injects extension logic into the specific stages of the Webpack
build process to alter the build result, thus completing a full build process.
Webpack
is a modern static module bundler for JavaScript
applications. When Webpack
processes an application, it recursively builds a dependency graph that includes every module the application needs, and then packs all these modules into one or more bundles.
Using Webpack
as a front-end build tool usually involves the following aspects:
- Code transformation: Compiling
TypeScript
intoJavaScript
, compilingSCSS
intoCSS
, and so on. - File optimization: Compressing
JavaScript
,CSS
,HTML
code, compressing and merging images, and more. - Code splitting: Extracting common code from multiple pages, extracting code that doesn't need to be executed on the initial page load for asynchronous loading.
- Module merging: In a modular project, there are many modules and files that need to be merged into one file for building purposes.
- Automatic refreshing: Listening to changes in local source code, automatically rebuilding, and refreshing the browser page, usually known as Hot Module Replacement (
HMR
). - Code validation: Validating the code for compliance with standards and ensuring that unit tests pass before the code is submitted to the repository.
- Automatic publishing: After updating the code, automatically building the production code and transferring it to the publishing system.
For Webpack
, everything is a module, and Webpack
can only process js
and json
files. Therefore, if you want to use other types of files, they need to be converted into modules recognized by Webpack
, namely js
or json
modules. This means that regardless of the file suffix, such as png
, txt
, vue
, etc., they need to be used as js
. However, directly treating them as js
is not possible because these files do not adhere to the syntax structure of js
. Therefore, we need Webpack loaders
to help us transform non-js
files into js
files, such as css-loader
, ts-loader
, file-loader
, and so on.
Here, we will write a simple Webpack loader
. Let's consider a simple scenario in which we focus on vue2
. Typically, when building a vue
project, we write .vue
files as modules. Although this single-file component approach is clear, it can lead to large files if a component is complex. Of course, vue
provides a way to reference js
and css
in .vue
files, but it can still be somewhat cumbersome to use. Therefore, we can write a Webpack loader
to separate the three parts, namely html
, js
, and css
during code writing, and then merge them in the loader
, which will then be processed by vue-loader
. Focusing on separation does not necessarily mean separating file types; splitting a single file into multiple files is simply a matter of code readability in the writing process. Here, our main focus is on writing a simple loader
, not on debating whether files should be separated. All the code mentioned in this article is available in https://github.com/WindrunnerMax/webpack-simple-environment.
Here, we will directly use the simple vue + ts
development environment set up in my previous article Exploring Webpack: Building a Vue Development Environment from Scratch. The relevant code for the environment can be found in the webpack--vue-cli
branch of https://github.com/WindrunnerMax/webpack-simple-environment, and we can simply clone and install it:
git clone https://github.com/WindrunnerMax/webpack-simple-environment.git
git checkout webpack--vue-cli
yarn install --registry https://registry.npm.taobao.org/
After this, we can run yarn dev
to see the effect. Here, let's first print the current directory structure.
webpack--vue-cli
├── dist
│ ├── static
│ │ └── vue-large.b022422b.png
│ ├── index.html
│ ├── index.js
│ └── index.js.LICENSE.txt
├── public
│ └── index.html
├── src
│ ├── common
│ │ └── styles.scss
│ ├── components
│ │ ├── tab-a.vue
│ │ └── tab-b.vue
│ ├── router
│ │ └── index.ts
│ ├── static
│ │ ├── vue-large.png
│ │ └── vue.jpg
│ ├── store
│ │ └── index.ts
│ ├── views
│ │ └── framework.vue
│ ├── App.vue
│ ├── index.ts
│ ├── main.ts
│ ├── sfc.d.ts
│ └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
Before writing a loader
, let's focus on the .vue
files in the directory structure above, because we need to split them, but how to split them is something to consider. In order to minimize the impact on normal usage, we have adopted the following approach here.
- We left the
template
part in the.vue
file, because some plugins such asVetur
will check for syntax in thetemplate
. For example, if it is separated as anhtml
file,prettier
will give errors for syntax like@click
. Also, if the.vue
file does not exist, modifications are needed for usingdeclare module "*.vue"
inTS
. So, following the principle of minimal impact, we left thetemplate
part in the.vue
file, thus preserving the.vue
declaration file. - For the
script
part, we extracted it. If it is written injs
, then we named it as.vue.js
, and forts
it is named as.vue.ts
. - For the
style
part, we also extracted it, using the same approach asscript
, naming them respectively as.vue.css
,.vue.scss
, and.vue.less
. Forscoped
, we implemented it through comments.
With the above modifications, we printed the directory structure again, focusing on the separation of .vue
files.
webpack--loader
├── dist
│ ├── static
│ │ └── vue-large.b022422b.png
│ ├── index.html
│ ├── index.js
│ └── index.js.LICENSE.txt
├── public
│ └── index.html
├── src
│ ├── common
│ │ └── styles.scss
│ ├── components
│ │ ├── tab-a
│ │ │ ├── tab-a.vue
│ │ │ └── tab-a.vue.ts
│ │ └── tab-b
│ │ ├── tab-b.vue
│ │ └── tab-b.vue.ts
│ ├── router
│ │ └── index.ts
│ ├── static
│ │ ├── vue-large.png
│ │ └── vue.jpg
│ ├── store
│ │ └── index.ts
│ ├── views
│ │ └── framework
│ │ ├── framework.vue
│ │ ├── framework.vue.scss
│ │ └── framework.vue.ts
│ ├── App.vue
│ ├── index.ts
│ ├── main.ts
│ ├── sfc.d.ts
│ └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── vue-multiple-files-loader.js
├── webpack.config.js
└── yarn.lock
Now let's start writing this loader
officially. First, we need to briefly explain the input and output of the loader
, as well as the commonly used modules.
- Simply put, a
webpack loader
is a function fromstring
tostring
. It takes a string of code as input and returns a string of code as output. - Typically, for handling various file types, there are already good solutions available as
loaders
. Theloader
we write ourselves is usually used for code processing. This means that after obtaining thesource
in theloader
, we convert it into anAST
tree, then make some modifications to thisAST
, and finally convert it back into a string of code for return. - Converting from string to an
AST
syntax tree is done in order to obtain a data structure that is easy for the computer to recognize.Webpack
comes with some built-in tools for this, such asacorn
for code toAST
conversion,estraverse
forAST
traversal, andescodegen
for convertingAST
to string code. - Since the
loader
operates from string to string, after processing the code into anAST
, it needs to be converted back to a string and then passed to the nextloader
. The nextloader
might need to perform the same conversion again, which can be time-consuming. To address this, we can usespeed-measure-webpack-plugin
for speed profiling andcache-loader
to store theAST
. loader-utils
is a commonly used utility class inloader
. The commonly used methods includeurlToRequest
for converting absolute paths to relative paths inwebpack
requests andgetOptions
for retrieving the parameters passed when configuringloader
.
Since we don't need any AST
related processing for this requirement, it's a relatively simple example. First, we need to write a loader
file and then configure it in webpack.config.js
. In the root directory, we create a vue-multiple-files-loader.js
and then in the webpack.config.js
module.rule
section, find the test: /\.vue$/
part and modify it as follows.
// ...
{
test: /\.vue$/,
use: [
"vue-loader",
{
loader: "./vue-multiple-files-loader",
options: {
// Matched file extensions
style: ["scss", "css"],
script: ["ts"],
},
},
],
}
// ...
First, after "vue-loader"
, we can see that we have written an object. The loader
property of this object is a string, which will be passed to require
in the future. This means that in webpack
, it will automatically help us require
this module, i.e., require("./vue-multiple-files-loader")
. Webpack loader
has a priority. Here, our goal is to first process the code through the vue-multiple-files-loader
and then hand it over to vue-loader
for processing. So we need to write vue-multiple-files-loader
after vue-loader
, so that the vue-multiple-files-loader
code will be executed first. We pass parameters through the options
object, which can be accessed in the loader
.
Regarding the priority of webpack loader
, when defining the loader
configuration, in addition to the loader
and options
options, there is also an enforce
option, which can accept values of pre:
pre-loader,
normal: normal-loader
, inline:
inline-loader,
post: post-loader
. The priority is pre > normal > inline > post
. For loader
of the same priority, the execution order is from right to left, from bottom to top. It's easy to understand, and as for the right-to-left ordering, it's just a choice of compose
rather than pipe
in webpack
. Additionally, when using require
, we can also skip certain loader
s: !
skips normal-loader
, -!
skips pre
and normal-loader
, !!
skips pre
, normal
and post-loader
. For example require("!!raw!./script.coffee")
. As for skipping loader
s, the official recommendation from webpack
is unless it's generated from another loader
, it's generally not recommended to skip proactively.
Now we've sorted out the creation of the vue-multiple-files-loader.js
file and the importing of the loader. So, we can start writing code using it. Typically, loaders are time-consuming applications, so we handle this loader asynchronously. By using this.async
, we inform the loader-runner
that this loader will be called back asynchronously. Once the processing is complete, we can execute the processed string code as a parameter using its return value.
module.exports = async function (source) {
const done = this.async();
// do something
done(null, source);
}
For file operations, we use promisify
so that we can better utilize async/await
.
const fs = require("fs");
const { promisify } = require("util");
const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
Now let's get back to the previous requirement. The idea is simple. First, in this loader, we only receive files ending with .vue
, which is configured in webpack.config.js
. Therefore, we only focus on .vue
files here. In this file, we need to get the directory where the file is located, then traverse it, and build a regular expression based on the options
configured in webpack.config.js
to match related files in the same directory, such as script
and style
. For files that match successfully, we read them, concatenate them according to the rules of .vue
files, and then return them for vue-loader
processing.
To start, let's handle the current directory, the current file being processed, and build the regular expression. Here, we pass scss
, css
, and ts
as options. For the file App.vue
, these will generate two regular expressions: /App\.vue\.css$|App\.vue\.scss$/
and App\.vue\.ts$
.
const filePath = this.context;
const fileName = this.resourcePath.replace(filePath + "/", "");
const options = loaderUtils.getOptions(this) || {};
const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
Next, we match the paths of script
and style
files by traversing the directory.
let stylePath = null;
let scriptPath = null;
const files = await readDir(filePath);
files.forEach(file => {
if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
});
Then, for the script
part, if a matching node exists and the original .vue
file does not have a script
tag, we asynchronously read the file and concatenate the code. If the extension is not js
, for example, if it's written in ts
, it will be treated as lang="ts"
, and then concatenated into the source
string.
if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
const extName = scriptPath.split(".").pop();
if (extName) {
const content = await readFile(scriptPath, "utf8");
const scriptTagContent = [
"<script ",
extName === "js" ? "" : `lang="${extName}" `,
">\n",
content,
"</script>",
].join("");
source = source + "\n" + scriptTagContent;
}
}
Afterwards, for the style
section, if there is a matching node and the original .vue
file does not contain a style
tag, then the file is asynchronously read and the code is concatenated. If the file extension is not .css
, for example if it is written in .scss
, then it will be processed as lang="scss"
. If the code contains the word // scoped
on a single line, then this style
section will be treated as scoped, and then concatenated into the string source
.
if (stylePath && !/<style[\s\S]*?>/.test(source)) {
const extName = stylePath.split(".").pop();
if (extName) {
const content = await readFile(stylePath, "utf8");
const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
const styleTagContent = [
"<style ",
extName === "css" ? "" : `lang="${extName}" `,
scoped ? "scoped " : " ",
">\n",
content,
"</style>",
].join("");
source = source + "\n" + styleTagContent;
}
}
After that, use done(null, source)
to trigger the callback and complete the loader
process. The relevant code is as shown below, and the complete code is in the webpack--loader
branch of https://github.com/WindrunnerMax/webpack-simple-environment
.
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");
const loaderUtils = require("loader-utils");
const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
module.exports = async function (source) {
const done = this.async();
const filePath = this.context;
const fileName = this.resourcePath.replace(filePath + "/", "");
const options = loaderUtils.getOptions(this) || {};
const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
let stylePath = null;
let scriptPath = null;
const files = await readDir(filePath);
files.forEach(file => {
if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
});
// If there is a matching node and the original `.vue` file does not contain a `script` tag
if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
const extName = scriptPath.split(".").pop();
if (extName) {
const content = await readFile(scriptPath, "utf8");
const scriptTagContent = [
"<script ",
extName === "js" ? "" : `lang="${extName}" `,
">\n",
content,
"</script>",
].join("");
source = source + "\n" + scriptTagContent;
}
}
// If there is a matching node and the original `.vue` file does not contain a `style` tag
if (stylePath && !/<style[\s\S]*?>/.test(source)) {
const extName = stylePath.split(".").pop();
if (extName) {
const content = await readFile(stylePath, "utf8");
const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
const styleTagContent = [
"<style ",
extName === "css" ? "" : `lang="${extName}" `,
scoped ? "scoped " : " ",
">\n",
content,
"</style>",
].join("");
source = source + "\n" + styleTagContent;
}
}
// console.log(stylePath, scriptPath, source);
done(null, source);
};
https://github.com/WindrunnerMax/EveryDay
https://webpack.js.org/api/loaders/
https://juejin.cn/post/6844904054393405453
https://segmentfault.com/a/1190000014685887
https://segmentfault.com/a/1190000021657031
https://webpack.js.org/concepts/loaders/#inline
http://t.zoukankan.com/hanshuai-p-11287231.html
https://v2.vuejs.org/v2/guide/single-file-components.html