Basic concept

Webpack is a powerful module packaging tool that allows you to introduce configuration files for a highly customized build on the front end.

By default, WebPack understands only JavaScript and JSON files, but in practice the requirements emerge and the file types vary. For example,.vue,.ts, images,.css, etc., this requires loader to enhance webPack’s ability to process files.

In webPack configuration files, we often see the following Settings.

module.exports = { ... The module: {rules: [{test: / \. Less $/ I / / match. Less at the end of the file use: [" HTML - loader ", "CSS - loader", 'less - loader'],}],}... };Copy the code

If you import a style file style.less(the code below) in your js code, WebPack is at a loss when it comes to.less files. By default, it can only handle files ending in.js and.json.

// import "./style.less";Copy the code

With loader’s ability, Webpack is able to process.less files.

For example, in the configuration code above, when a.less style file is imported into a project, WebPack first sends the file contents to the less-Loader, which converts all less styles into normal CSS styles.

The common CSS styles are then sent to the CSS-Loader for processing. The main function of the CSS-Loader is to parse the @import and image path in the CSS syntax. After processing, the imported CSS is merged together.

The merged CSS file is then passed to the HTML-Loader for processing, which eventually inserts the style content under the STYLE tag in the HTML header, thus styling the page.

From the above example, we can see that each Loader has a single responsibility and is only responsible for its own small piece. But a combination of specialized loaders can enhance WebPack’s ability to correctly identify and process all kinds of weird files, regardless of the file format.

It is also worth noting that loader executes the use array from the top of the configuration sequence.

After understanding the basic purpose of Loader, we can not help thinking, why loader is so powerful, how it is implemented?

Let’s write a custom loader by hand to further understand the value and purpose of loader.

Custom loader

With the increasing popularity of ES6, the application of async and await to handle asynchronous code is increasing (code below). The emergence of async and await makes it easy for JS to handle asynchronous operations. In addition, exceptions can be caught by try or catch.

async function start(){
  console.log("Hello world");
  await loadData();
  console.log("end world");
}
Copy the code

Suppose now that the project team wants to deploy the monitoring system for each project, once the JS in the production environment is abnormal, the error message should be timely uploaded to the background log server.

The project needs to try and catch all async functions. The expected output is as follows:

async function start(){ try{ console.log("Hello world"); await loadData(); console.log("end world"); }catch(error){ console.log(error); logger(error); // Handle error messages}}Copy the code

The value of engineering comes in large projects where manually adding tries and catches is inefficient and error-prone. We can customize a loader to automatically add exception catching to all async functions in the project.

Loader based API

Create a file called error-loader.js in the project folder and write the following test code (the code below).

Loader is essentially a function. The content parameter is a string that stores the contents of the file. Then the loader function can be exported for use by Webpack.

When the webpack configuration file is set to rules (code below), just point the loader in use to the file path of the exported Loader function, so that Webpack can refer to the loader smoothly. In addition, we can add the options property to the loader function.

// exports = function (content){console.log(this.query); // exports = function (content){console.log(this.query); // { name: 'hello' } return content; // exports = {module:{rules:[{test:/\.js$/, use:[ { loader:path.resolve(__dirname,"./error-loader.js"), options:{ name:"hello" } } ] } ] } }Copy the code

Once the project is packaged and WebPack detects a.js file, it passes the file’s code string to the error-loader.js exported Loader function for execution.

The loader function we wrote above does nothing to the code string content and returns the result. So the purpose of our custom loader is to perform various data operations on the content source code and return the finished results.

For example, we can use regular expressions to remove all console.log statements from the content, so that the resulting package does not contain console.log.

In addition, when we develop some complex loader, we can accept the parameters passed in the configuration file. For example, webpack.config.js passes an object {name:”hello”} to error-Loader, so the custom loader function can get the this.query parameter.

In addition to directly returning content with return, the Loader function can use this.callback(below) to achieve the same effect.

This.callback can pass the following four arguments. The arguments passed by this.callback are sent to the next Loader function, and each loader function forms a pipeline that processes the code into the desired result.

  • The first parameter is an error message, and there is no error to fill innull
  • The second parameter iscontentIs also the target for data manipulation
  • The third parameter issourceMapIt will package the code and the source code link up, convenient developer debugging, generally throughbabelGenerated.
  • The fourth parameter ismetaAdditional information, optional.
module.exports = function (content){
  this.callback(null,content);  
}
Copy the code

The above content is written in a synchronous manner, in case the loader function needs to do some asynchronous operations in the following way.

This.async () is called and returns a callback function. After the asynchronous operation is complete, you can continue to use the callback to return the content.

Module. exports = function (content,sourceMap,meta){const callback = this.async(); SetTimeout (()=>{// simulate asynchronous operation callback(null,content); }}, 1000)Copy the code

Exception capture is written by the Loader

With some basic apis covered above, it’s time to develop a loader that catches async function exceptions.

The first argument to the Loader function is content, which we can modify using regular expressions. However, if you implement more complex functions, regular expressions can become very complex and difficult to develop.

The main approach is to convert code strings into objects. We perform data operations on the objects and return the completed objects to strings.

We can use the Babel related tools to help us achieve this goal, the code is as follows (if you are not familiar with Babel, you can ignore this section, we will have the opportunity to analyze Babel separately later).

The @babel/ Parser module first converts the source code content into an AST tree and then traverses the AST tree with @babel/traverse looking for async function nodes.

After the async function node is found, the @babel/types module wraps the async function with a try and catch expression and replaces the old node.

Finally, @babel/ Generator module is used to convert the ast tree after operation into object code return.

const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const generate = require('@babel/generator').default; const t = require("@babel/types"); const ErrorLoader = function (content,sourceMap,meta){ const ast = parser.parse(content); Traverse (ast,{FunctionDeclaration(path){// Determine if the current node is async const isAsyncFun = t.isFunctionDeclaration(path.node,{async:true}); if(! IsAsyncFun){// Stop the operation if it is not async function return; } const bodyNode = path.get("body"); If (tisblockStatement (bodyNode)){const FunNode = bodyNode.node.body; If (funnode.length == 0) {// empty function return; } if(FunNode.length ! 1 = = | | t.i sTryStatement (FunNode [0])) {/ / function has not been a try... Const code = 'console.log(error); `; Const resultAst = t.resultStatement (bodyNode.node, t.catchclause (t.indentifier ("error")), T.b lockStatement (parser. Parse (code). The program. The body))) / / will replace the original transformation after the node bodyNode. ReplaceWithMultiple ([resultAst]); Callback (null,generate(ast).code,sourceMap,meta); } module.exports = ErrorLoader;Copy the code

The code address

Loader source code parsing

Understand the implementation of custom Loader, then we read some of the usual work is very common loader source code, feel clear about their underlying implementation principle.

less-loader

Less – Loader simplified source code as follows, its execution process is very simple. The less plug-in is loaded by require(“less”), which is then called to compile the source code output.

All less syntax is compiled into CSS, and callback is called to return the result.

async function lessLoader(source) {

  const options = this.getOptions(schema);
  
  const callback = this.async();
  
  const implementation = require("less");

  const lessOptions = getLessOptions(this, options, implementation);
  
  let result;

  try {
    result = await implementation.render(source, lessOptions);
  } catch (error) {
    callback(new LessError(error));
    return;
  }

  const { css, imports } = result;
  
  let map = typeof result.map === "string" ? JSON.parse(result.map) : result.map;

  callback(null, css, map);
}

export default lessLoader;

Copy the code

file-loader

File-loader is usually used to process images, fonts, and other formats of files. The execution process can be summarized as follows:

  • file-loaderFirst of all byinterpolateNameFunction based on the configurationnameProperties andcontentContent generation file name
  • You have the file name pathurlAnd then according to the user configurationoptions, generate targetoutputPathandpublicPath
  • The last executionthis.emitFileFunction, set upwebpackThe hook function, tooutputPathPath Create file content
// import path from 'path'; import { getOptions, interpolateName } from 'loader-utils'; export default function loader(content) { const options = getOptions(this); / / access to configuration items const context = options. The context | | this. RootContext; const name = options.name || '[contenthash].[ext]'; / / according to the name of the configuration and the content of the content to generate a hash file name const url = interpolateName (this name, {the context, the content, the regExp: options.regExp, }); let outputPath = url; OutputPath if (options.outputPath) {if (typeof options.outputPath === 'function') {outputPath = options.outputPath(url, this.resourcePath, context); } else { outputPath = path.posix.join(options.outputPath, url); }} // publicPath is equal to the root path of webPack configuration. Let publicPath = '__webpack_public_path__ + ${JSON.stringify(outputPath)}`; PublicPath if (options.publicPath) {if (typeof options.publicPath === 'function') {publicPath = options.publicPath(url, this.resourcePath, context); } else {/ / the user configuration publicPath stitching on the filename publicPath = ` ${options. PublicPath. EndsWith ('/')? options.publicPath : `${options.publicPath}/` }${url}`; } publicPath = JSON.stringify(publicPath); } if (typeof options.emitFile === 'undefined' || options.emitFile) { this.emitFile(outputPath, content, null); // Call the webPack hook function to create a file} return '${esModule? 'export default' : 'module.exports ='} ${publicPath}; `; } export const raw = true;Copy the code

vue-loader

When front-end students do daily VUE project development, they usually use single-file components (code below).

A single-file component consists of three parts: Template, script, and style. With these three tags, HTML, JS, and CSS can be written in a single file, usually in a.vue format.

A vue file cannot be parsed by Webpack. It is through the vue-Loader that Webpack parses a single file component into code that the browser can execute.

<template>
 <div class="main">hello world</div>
</template>
<script>
export default {}
</script>
<style>
 .main{
  color:red;
 }
</style>
Copy the code

The simplified source code of Vue-Loader is as follows. We can tease out its operation mechanism from the source code.

module.exports = function (source) { const loaderContext = this const { request, sourceMap, } var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var SourceRoot, needMap: sourceMap}) // If the file contains a different type, such as foo.vue? Type = template&id = XXXXX / / type = template | script | style / / selectBlock will seek corresponding to different type of loader to load the if (incomingQuery. Type) { return selectBlock( descriptor, loaderContext, incomingQuery, !! } // Let templateImport = 'var render, staticRenderFns` let templateRequest if (descriptor.template) { //... templateImport = `import { render, } from ${request} '} let scriptImport = 'var script = {}' if (descriptor. Script) {//... ScriptImport = (' import script from ${request}\n '+' export * from ${request} '// support named exports)} // Process styles  let stylesCode = `` if (descriptor.styles.length) { stylesCode = genStylesCode(/*... */) } /* import { render,staticRenderFns } from "./foo.vue? vue&type=template&id=32euy323lang=pug&"; import script from "./foo.vue? vue&type=script&lang=js&"; import style0 from "./foo.vue? vue&type=style&index=0&module=true&lang=css&" */ let code = ` ${templateImport} ${scriptImport} ${stylesCode} var component = normalizer( script, render, staticRenderFns, ... ) export default component.exports; `; code = code.trim() + `\n` return code; }Copy the code

Vue-loader actually executes two rounds, the first round is executed as a code string (the code below).

The three most critical variables in this code :templateImport, scriptImport, and stylesCode the last compiled data structure corresponds to the part of the comment.

As you can see from the commented code,foo.vue has been imported three more times and is followed by a key argument, type, which is used to specify whether it is template, script, or style.

/* import { render,staticRenderFns } from "./foo.vue? vue&type=template&id=32euy323lang=pug&"; import script from "./foo.vue? vue&type=script&lang=js&"; import style0 from "./foo.vue? vue&type=style&index=0&module=true&lang=css&" */ let code = ` ${templateImport} ${scriptImport} ${stylesCode} var Component = Normalizer (// generate vue component script, render, staticRenderFns,...) export default component.exports; ` return code;Copy the code

These three imports trigger a second round of vue-Loader execution, where the code directly returns the result when it goes to the selectBlock(code below).

The template, script, and style will be processed by the corresponding Loader and the result will be returned.

After the above three pieces of code are processed, Vue’s API can be invoked to generate components and compiled into code that can be executed by the browser.

if (incomingQuery.type) { return selectBlock( descriptor, loaderContext, incomingQuery, !! options.appendExtension ) }Copy the code

css-loader

CSS – Loader is very powerful, it can import all the CSS using @import syntax, it can also handle the image URLS that the CSS introduces, and it can achieve CSS modularity.

The following is the simplified csS-Loader source code. As we can see from reading the source code, csS-Loader implements these functions mainly by calling the PostCSS plug-in.

Css-loader first defines an array of plugins, which load csS-modules, @import, URLS, and ICSS plugins, and then provide them to PostCSS in the form of parameters, thus making CSS-Loader equipped with the corresponding capabilities.

export default async function loader(content, map, meta) { const plugins = []; const callback = this.async(); let options; const replacements = []; const exports = []; // Handle csS-modules if (shouldUseModulesPlugins(options)) {plugins.push(... getModulesPlugins(options, this)); } // Handle @import if (shouldUseImportPlugin(options)) {plugins.push(importParser({... })); If (shouldUseURLPlugin(options)) {plugins.push(urlParser({... })); } if (needToUseIcssPlugin) {plugins.push(icssParser({... })); } const { resourcePath } = this; let result; try { result = await postcss(plugins).process(content, {... }); } catch (error) {callback(error); return; } const importCode = getImportCode(imports, options); // Import dependencies let moduleCode; try { moduleCode = getModuleCode(result, api, replacements, options, this); } catch (error) {callback(error); return; } const exportCode = getExportCode(// exports, replacements, needToUseIcssPlugin, options); callback(null, `${importCode}${moduleCode}${exportCode}`); }Copy the code