头图

[Summary of 4D Characters] One article thoroughly understands the core principles of Webpack

范文杰
中文
If you find the article useful, you are welcome to like and follow it, but writing is really not easy. Reprinting in any form is prohibited without the author's consent! ! !

image.png

background

Webpack is particularly hard to learn! ! !

After version 5.0, the Webpack feature set has become very large, including: module packaging, code segmentation, on-demand loading, HMR, Tree-shaking, file monitoring, sourcemap, Module Federation, devServer, DLL, multi-process, etc. To achieve these functions, the amount of code in webpack has reached an astonishing level:

  • 498 JS files
  • 18862 line comment
  • 73548 lines of code
  • 54 module types
  • 69 dependency types
  • 162 built-in plugins
  • 237 hooks

Under this order of magnitude, the cost of reading, analyzing, and learning the source code is very high, coupled with the unclear documentation on the webpack official website, resulting in extremely high costs of learning and getting started with webpack. To this end, the community has derived various scaffolds around Webpack, such as vue-cli , create-react-app , to solve the problem of "use".

But this has led to a new problem. Most people gradually become configuration engineers in terms of engineering, staying at the stage of "can use and match" but don't know how to turn in the black box, and they will be blind when they encounter specific problems. Up:

  • I want to upgrade the basic library, but I can’t run due to compatibility issues, so I just give up.
  • I want to optimize the compilation performance, but I don’t know the internal principles and I can’t get started

The reason is that there is no necessary overall understanding of the internal operating mechanism of webpack, and the problem can not be quickly located. Yes, even the essence of the problem is often not seen. The so-called cannot see the essence through the phenomenon, what is the essence? I personally abstract the entire huge system of webpack into three aspects of knowledge:

  1. build core processes
  2. role loader of
  3. plugin architecture and common routines

The three work together to form the main framework of webpack:

After understanding these three pieces of content, even if you enter the door, you have the most basic understanding of Webpack, and you can find out by the picture if you encounter any problems in your work. I should add that as an introductory tutorial, this article will not expand too many webpack code-level details-my energy does not allow it, so readers do not need to read a bunch of text to produce a particularly large psychological burden.

Core process analysis

First of all, we have to understand one point, the core function of Webpack:

At its core, webpack is a static module bundler for modern JavaScript applications.

That is, various types of resources, including pictures, css, js, etc., are translated, combined, spliced, and generated into bundler files in JS format. official website home page animation is vividly expressed this:

The core of this process completes the content conversion + resource merging , and the realization includes three stages:

  1. Initialization phase:

    1. initialization parameters : read from the configuration file, configuration object, and Shell parameters, and combine with the default configuration to get the final parameters
    2. create a compiler object : use the parameters obtained in the Compiler step to create a 06095019a05c35 object
    3. Initialize the compilation environment : Including injecting built-in plug-ins, registering various module factories, initializing RuleSet collection, loading configuration plug-ins, etc.
    4. start compiling : execute the run method of the compiler
    5. determine the entrance : Find out all the entry files entry in the configuration, compilition.addEntry convert the entry files into the dependence object
  2. Construction phase:

    1. compilation module (make) : Create a module object based on the entry corresponding to dependence , call the loader to translate the module into standard JS content, call the JS interpreter to convert the content into an AST object, find out which module the module depends on, and recurse the original Step until all the files dependent on the entry have been processed in this step
    2. completes the module compilation : After recursively processing all reachable modules in the previous step, the translated content of each module and the dependency diagram
  3. Generation stage:

    1. output resource (seal) : According to the dependency between the entry and the module, assemble into a Chunk that contains multiple modules, and then Chunk into a separate file and add it to the output list. This step can be modified Last chance to output content
    2. Write into the file system (emitAssets) : After determining the output content, determine the output path and file name according to the configuration, and write the file content to the file system

The single build process is executed in order from top to bottom. The details will be discussed below. Before that, students who are not familiar with the various technical terms mentioned above can take a look at the introduction:

  • Entry : Compilation entry, starting point for webpack compilation
  • Compiler : Compiler manager, after webpack is started, it will create compiler object, which will survive until the end and exit
  • Compilation : a single editing process manager, such as watch = true , there is only one compiler running process, but every time a file change triggers a recompilation, a new compilation object will be created
  • Dependence : dependent objects, webpack records the dependencies between modules based on this type
  • Module : All resources inside webpack will exist in the form of "module" objects, and all operations, translation, and merging of resources are performed with "module" as the basic unit
  • Chunk : When compiled ready for output, webpack will module organized according to specific rules as a one chunk , these chunk some extent correspond with the final output
  • Loader : The resource content converter is actually a converter that converts content A to B
  • Plugin : During the webpack building process, corresponding events will be broadcast at specific times. The plug-in listens to these events and intervenes in the compilation process at a specific point in time.

The webpack compilation process revolves around these key objects. For more detailed and complete information, please refer to Webpack Knowledge Graph .

Initialization phase

Basic process

Learning the source code of a project usually starts from the entrance, and slowly finds out the routine according to the diagram, so let's take a look at the initialization process of webpack:

explain:

  1. process.args + webpack.config.js into user configuration
  2. Call validateSchema verify the configuration
  3. Call getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults merge the final configuration
  4. Create compiler object
  5. Traverse the user-defined plugins collection and execute the apply method of the plug-in
  6. Call new WebpackOptionsApply().process method, load various built-in plug-ins

The main logic is concentrated in the WebpackOptionsApply class. Webpack has hundreds of plug-ins built in. These plug-ins do not need to be manually configured. WebpackOptionsApply will dynamically inject the corresponding plug-ins according to the configuration content during the initialization phase, including:

  • Inject the EntryOptionPlugin plug-in, handle the entry configuration
  • The devtool value determination processing that the subsequent plug sourcemap , optional values: EvalSourceMapDevToolPlugin , SourceMapDevToolPlugin , EvalDevToolModulePlugin
  • Inject RuntimePlugin , which is used to dynamically inject the webpack runtime according to the code content

At this point, the compiler instance is created, and the corresponding environment parameters are also preset, and then the compiler.compile function is called:

// 取自 webpack/lib/compiler.js 
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {
              compilation.seal(err => {...});
            });
          });
        });
      });
    });
  }

The Webpack architecture is very flexible, but at the cost of sacrificing the intuitiveness of the source code. For example, the initialization process mentioned above, from creating compiler instance of 06095019a0616c to calling the make hook, the logical link is very long:

  • Start webpack and trigger the createCompiler method in the lib/webpack.js file
  • createCompiler method internally calls the WebpackOptionsApply plugin
  • WebpackOptionsApply defined in the lib/WebpackOptionsApply.js file, internally according to the entry configuration, it is decided to inject the entry related plug-ins, including: DllEntryPlugin , DynamicEntryPlugin , EntryPlugin , PrefetchPlugin , ProgressPlugin ContainerPlugin
  • Entry related plugins, such as lib/EntryPlugin.js of EntryPlugin monitor compiler.make hook
  • lib/compiler.js in the compile function of this.hooks.make.callAsync
  • Trigger EntryPlugin of make callbacks, perform callback compilation.addEntry function
  • compilation.addEntry hook that has nothing to do with the main process, then call the handleModuleCreate function to formally start building the content

This process needs to pre-bury various plug-ins when webpack is initialized. After 4 files, 7 jumps before entering the theme, the foreplay is too much. If the reader does not have enough understanding of the concept, architecture, and components of webpack, The source code reading process will be painful.

Regarding this issue, I summarized some tips and suggestions at the end of the article. Those who are interested can slide to the appendix reading module.

Construction phase

Basic process

Have you ever thought about such a problem:

  • Will the Webpack compilation process parse the source code into AST? What do webpack and babel achieve respectively?
  • How to identify the dependence of resources on other resources during Webpack compilation?
  • Compared to streaming build tools such as grunt and gulp, why is webpack considered a new generation of build tools?

These problems can basically be seen in the construction phase. Construction of phase from entry start recursive resolution resource dependency and resources, in compilation gradually constructed within the object module collection and module dependencies between, core processes:

To explain, the build phase starts with the entry file:

  1. Call handleModuleCreate module subclass according to the file type
  2. Call loader-runner warehouse runLoaders translate module content, usually from various resource types into JavaScript text
  3. Call acorn to parse JS text into AST
  4. Traverse the AST, trigger various hooks

    1. exportImportSpecifier hook in the HarmonyExportDependencyParserPlugin plug-in, and interpret the resource dependencies corresponding to the JS text
    2. Call module object to addDependency the dependent object to the module dependency list
  5. After the AST traversal is completed, call module.handleParseResult process module dependencies
  6. For module new dependence, calling handleModuleCreate , control flows back to the first step
  7. After all dependencies are resolved, the build phase ends

In this process, the data flow module => ast => dependences => module , first transfer to AST and then find dependencies from AST. This requires loaders final result must be processed can be processed acorn standard JavaScript syntax, for example, pictures, images need to be converted from binary to similar export default "data:image/png;base64,xxx" such base64 format or export default "http://xxx" such url format.

compilation processed recursively according to this process, and the content of each module and the module gradually parsed, and then the content can be packaged and output later.

Example: hierarchical progression

If there is a file dependency tree as shown in the figure below:

Among them, index.js is the entry file, which depends on the a/b file; a depends on the c/d file. After initializing the compilation environment, EntryPlugin finds the index.js file according to the entry configuration, and calls the compilation.addEntry function to trigger the build process. After the build is completed, the internal data structure will be generated:

At this time module[index.js] and the corresponding dependent objects dependence[a.js] and dependence[b.js] . The OK, which give the next clue: a.js, b.js, according to the above logic flowchart continues to call module[index.js] of handleParseResult function, processing continues a.js, b.js file, the above-described recursive process, further a, b Module:

From the a.js module, the c.js/d.js dependency is parsed, so continue to call module[a.js] of handleParseResult , and then recurse the above process:

After analyzing all the modules here, and finding that there are no more new dependencies, you can continue to advance and enter the next step.

to sum up

Recall the questions mentioned at the beginning of the chapter:

  • Will the Webpack compilation process parse the source code into AST? What do webpack and babel achieve respectively?

    • The source code will be read during the build phase and parsed into an AST collection.
    • Webpack only traverses the AST collection after reading the AST; babel performs equivalent conversion of the source code
  • How to identify the dependence of resources on other resources during Webpack compilation?

    • In the process of Webpack traversing the AST collection, it recognizes require/ import and determines the dependency of the module on other resources
  • Compared with streaming build tools such as grant and gulp, why is webpack considered a new generation build tool?

    • Grant and Gulp only execute the task flow predefined by the developer; while webpack handles the content of the resource in-depth and is more powerful in function

Generation phase

Basic process

The construction phase revolves around module , and the generation phase revolves around chunks . After the construction phase, webpack gets enough module content and module relationship information, and then it starts to generate the final resources. At the code level, it starts to execute the function compilation.seal

// 取自 webpack/lib/compiler.js 
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {
              **compilation.seal**(err => {...});
            });
          });
        });
      });
    });
  }

seal originally intended to be sealed and locked. I personally understand that in the context of , it is close to 16095019a068b3 "Put the module into the honeypot" . seal function mainly completes module to chunks , the core process:

Simply sort out:

  1. ChunkGraph object compiled this time;
  2. Traverse the compilation.modules collection, and assign module to different Chunk objects according to entry/dynamically introduced rules;
  3. compilation.modules traversing the 06095019a0694f collection, get the complete chunks collection object, call the createXxxAssets method
  4. createXxxAssets traverses module/chunk and calls the compilation.emitAssets method to record the assets compilation.assets object
  5. Trigger seal callback, control flow returns to compiler object

The key logic of this step is to chunks module according to the rules. The built-in chunk packaging rules of webpack are relatively simple:

  • entry and the modules reached by entry are combined into a chunk
  • Modules introduced using dynamic introduction statements, each combined into a chunk

chunk is the basic unit of output. By default, these chunks correspond to the final output resource one-to-one. According to the above rules, it can be roughly deduced that a entry will be packaged out corresponding to a resource, and the module introduced through the dynamic introduction statement also Corresponding resources will be packaged out, let's look at an example.

Example: Multi-entry packaging

If there is such a configuration:

const path = require("path");

module.exports = {
  mode: "development",
  context: path.join(__dirname),
  entry: {
    a: "./src/index-a.js",
    b: "./src/index-b.js",
  },
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  devtool: false,
  target: "web",
  plugins: [],
};

There are two entries in the example configuration, the corresponding file structure:

index-a depends on c and dynamically introduces e; index-b depends on c/d. According to the rules stated above:

  • entry and the modules reached by entry, combined into a chunk
  • uses the modules introduced by the dynamic import statement, and each is combined into a chunk

The generated chunks structure is:

I.e. according to the dependencies, chunk[a] contains index-a/c two modules; chunk[b] contains c/index-b/d three modules; chunk[e-hash] dynamic introduced e corresponding chunk.

I don’t know if you have noticed that chunk[a] and chunk[b] contain c at the same time. When this problem is put into a specific business scenario, it may be that a multi-page application, all pages rely on the same basic library, then the entry corresponding to all these pages will contain it. Basic library code, isn't it a waste? In order to solve this problem, webpack provides some plug-ins such as CommonsChunkPlugin and SplitChunksPlugin , which further optimize the structure of chunks

The role of SplitChunksPlugin

SplitChunksPlugin is an excellent example of webpack highly scalable architecture, we said above, the main flow webpack which is based entry / dynamic organization introduced in both cases chunks , which will inevitably lead to some unnecessary repetition package, webpack through plug-ins Form to solve this problem.

Looking back compilation.seal the code of the 06095019a06c0c function, roughly it can be sorted into four steps:

  1. Traverse compilation.modules and record the relationship between the chunk
  2. Trigger various module optimization hooks, this step is mainly optimized for module dependencies
  3. Traverse module build a chunk collection
  4. Trigger various optimization hooks

The above 1-3 are the implementation of the preprocessing + chunks default rule, which is beyond the scope of our discussion. Here we focus on the optimizeChunks hook triggered in the fourth step. At this time, the logic of the main process has been run, and the chunks set SplitChunksPlugin is used. This hook analyzes chunks collection and adds some general chunks according to the configuration rules:

module.exports = class SplitChunksPlugin {
  constructor(options = {}) {
    // ...
  }

  _getCacheGroup(cacheGroupSource) {
    // ...
  }

  apply(compiler) {
    // ...
    compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
      // ...
      compilation.hooks.optimizeChunks.tap(
        {
          name: "SplitChunksPlugin",
          stage: STAGE_ADVANCED,
        },
        (chunks) => {
          // ...
        }
      );
    });
  }
};

Understand? The high scalability of the webpack plug-in architecture allows the entire compilation process to be solidified. The branch logic and detailed requirements are "outsourced" to be implemented by a third party. This set of rules sets up a huge webpack ecosystem. More about the plug-in architecture For more details, the following plugin has a detailed introduction, so skip it here.

Write to the file system

After the construction phase, compilation the content and dependencies of the resource module, and knows what the "input" is; and after the seal phase is processed, compilation knows the map of the resource output, that is, knows how to "output": which modules Where to "bind" with those modules to output. seal approximate data structure after 06095019a06dc8:

compilation = {
  // ...
  modules: [
    /* ... */
  ],
  chunks: [
    {
      id: "entry name",
      files: ["output file name"],
      hash: "xxx",
      runtime: "xxx",
      entryPoint: {xxx}
      // ...
    },
    // ...
  ],
};

seal end of 06095019a06e00, the function compiler.outputFileSystem.writeFile method assets compiler.emitAssets function to write the 06095019a06e03 collection to the file system. The realization logic is relatively tortuous, but it has nothing to do with the main process, so I won't expand it here.

Resource form circulation

OK, the logic level structure of the main process has been sorted out above, here combined with the resource form circulation perspective to re-examine the entire process to deepen understanding:

  • compiler.make stage:

    • entry file dependence Eligibility form compilation dependency list, dependence the object is recorded entry information type, path, etc.
    • According to dependence call the corresponding factory function to create the module object, and then read in the file content corresponding module loader-runner to convert the content, if the conversion result has other dependencies, continue to read the dependent resources, repeat this process until all dependencies are converted to module
  • compilation.seal stage:

    • Traverse the module module to different chunk according to the entry configuration and the way of introducing resources
    • Traversing chunk set, call compilation.emitAsset method for marking chunk output rules, i.e. into assets set
  • compiler.emitAssets stage:

    • assets to the file system

Plugin analysis

Many information on the Internet classifies the plug-in architecture of webpack as an "event/subscription" model. I think this generalization is biased. The subscription mode is a loosely coupled architecture. The publisher only publishes event messages at a specific time. Subscribers do not or rarely interact directly with the event. For example, when we usually use HTML events, it is often only at this time. Trigger business logic, rarely call context operations. The webpack's hook system is a strongly coupled architecture. It will be accompanied by sufficient context information when the hook is triggered at a specific time. In the hook callback defined by the plug-in, it can and can only interact with the data structure and interface behind these contexts to produce The side effect , which in turn affects the compilation status and subsequent processes.

To learn the plug-in architecture, you need to understand three key issues:

  • WHAT: What is a plug-in
  • WHEN: What hook will be triggered at what time
  • HOW: How affect the compilation status in the hook callback?

What: What is a plug-in

From the morphological point of view, the plug-in is usually a class apply

class SomePlugin {
    apply(compiler) {
    }
}

apply function will get the parameter compiler when it runs. From this, you can call the hook object to register various hook callbacks, for example: compiler.hooks.make.tapAsync , where make is the hook name, and tapAsync defines how the hook is called. Webpack's plug-in architecture is based on this model. Built, plug-in developers can use this mode to insert specific code in the hook callback. Various built-in objects of hooks have 06095019a07117 attributes, such as compilation objects:

class SomePlugin {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
            compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});
        })
    }
}

The core logic of the hook is defined in the Tapable warehouse, and the following types of hooks are defined internally:

const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
        SyncLoopHook,
        AsyncParallelHook,
        AsyncParallelBailHook,
        AsyncSeriesHook,
        AsyncSeriesBailHook,
        AsyncSeriesWaterfallHook
 } = require("tapable");

Different types of hooks will have slightly different calling methods according to their parallelism, fusing method, synchronization and asynchrony. Plug-in developers need to write different interaction logics according to these characteristics. This part is also very rich. Let’s talk later.

When: When will the hook be triggered

After understanding the basic form of webpack plugins, the next question needs to be clarified: at what time will webpack trigger which hooks? I think this piece is the most knowledgeable part. After all, there are 237 hooks in the source code, but the official website only introduces less than 100, and the official website’s description of each hook is too short, personally I haven’t read it. It's so rewarding, so it's necessary to start talking about this topic. Let's look at a few examples:

  • compiler.hooks.compilation

    • Timing: Triggered after the compilation is started and the compilation object is created
    • Parameters: the currently compiled compilation object
    • Example: Many plugins obtain compilation instances based on this event
  • compiler.hooks.make

    • Timing: Triggered when compiling officially starts
    • Parameters: also the current compiled compilation object
    • Example: The built-in EntryPlugin implements the initialization of the entry module based on this hook
  • compilation.hooks.optimizeChunks

    • Timing: In the seal function, it will be triggered after the chunk
    • Parameters: chunks collection and chunkGroups collection
    • Example: SplitChunksPlugin plug-in realizes chunk split optimization based on this hook
  • compiler.hooks.done

    • Timing: Triggered after compilation is complete
    • Parameters: stats object, including various statistical information during the compilation process
    • Example: webpack-bundle-analyzer plug-in realizes package analysis based on this hook

These are the three learning elements of the hook I summarized: trigger timing, passing parameters, and sample code.

Trigger timing

The timing of the trigger is closely related to the webpack working process. Generally, from start to finish, the compiler object triggers the following hooks successively:

The compilation object is triggered successively:

Therefore, if you understand the main process of webpack work mentioned earlier, you can basically figure out "when will the hooks be triggered".

parameter

Passing parameters are strongly related to specific hooks. The official website does not provide further explanation on this aspect. My approach is to directly search for the call statement in the source code. For example, for compilation.hooks.optimizeTree , you can search for the hooks.optimizeTree.call keyword in the webpack source code to find the call code :

// lib/compilation.js#2297
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
});

Combined with the context where the code is located, it can be determined that the optimized collections chunks and modules

Find example

Webpack's hooks vary in complexity. I think the best way to learn is to query how to use these hooks in other plugins with a purpose. For example, in compilation.seal internal functions have optimizeModules and afterOptimizeModules pair looks dual hook, optimizeModules literally can be understood for optimization has compiled the modules , that afterOptimizeModules it?

The only use searched from the webpack source code is ProgressPlugin , and the general logic is as follows:

compilation.hooks.afterOptimizeModules.intercept({
  name: "ProgressPlugin",
  call() {
    handler(percentage, "sealing", title);
  },
  done() {
    progressReporters.set(compiler, undefined);
    handler(percentage, "sealing", title);
  },
  result() {
    handler(percentage, "sealing", title);
  },
  error() {
    handler(percentage, "sealing", title);
  },
  tap(tap) {
    // p is percentage from 0 to 1
    // args is any number of messages in a hierarchical matter
    progressReporters.set(compilation.compiler, (p, ...args) => {
      handler(percentage, "sealing", title, tap.name, ...args);
    });
    handler(percentage, "sealing", title, tap.name);
  }
});

Basically, it can be guessed that the afterOptimizeModules is to notify the end of the optimization behavior.

apply is a function, it only has input from the design, and webpack does not care output. Therefore, the plug-in can only call various methods of the type entity to change the configuration information of the entity and change the compilation behavior. E.g:

  • compilation.addModule : add a module, you can add custom modules in addition to the original module construction rules
  • compilation.emitAsset : The literal translation is "submit assets", the function can understand to write the content to a specific path

At this point, the working mechanism and writing of the plug-in have been introduced in a very superficial way. Let's go back and talk about it in detail.

How: How to affect compilation status

After solving the above two problems, we will be able to understand "how to insert specific logic into the webpack compilation process", and then the key point is how to affect the compilation status? To emphasize, webpack's plug-in system is very different from the usual subscription/publishing model. It is a very strongly coupled design. Webpack determines when and how to execute hooks callbacks; and inside the hooks callbacks, you can pass Modifying the state, calling the context api, etc. will generate side effect .

For example, the EntryPlugin plugin:

class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options, context } = this;

      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {
        callback(err);
      });
    });
  }
}

The above code snippet calls two interfaces that affect the state of the compilation

  • compilation.dependencyFactories.set
  • compilation.addEntry

The specific meaning of the operation can be ignored first. The point to understand here is that webpack will pass the context information this (compiler object). In the callback, you can call the method of the context object or directly modify the properties of the context object. In this way, a side effect is generated on the original process. So if you want to write a plug-in skillfully, in addition to understanding the timing of the call, you also need to understand which APIs we can use, for example:

  • compilation.addModule : Add modules, you can add custom modules in addition to the module
  • compilation.emitAsset : Literally translated as "submit assets", the function can understand and write the content to a specific path
  • compilation.addEntry : Add entry, the function is the same as the configuration of entry
  • module.addError : Add compilation error message
  • ...

Loader introduction

The role and implementation of Loader are relatively simple and easy to understand, so just a brief introduction. Recall where the loader takes effect in the compilation process:

In the flowchart, runLoaders will call the loader set configured by the user to read and translate resources. The previous content can be strange, but in theory, standard JavaScript text or AST objects should be output after translation, so that webpack can continue to process module dependencies.

After understanding this basic logic, the loader's responsibilities are clearer. It is nothing more than converting content A into content B, but it is quite specific at the level of specific usage. There are concepts such as pitch, pre, post, inline and so on. Cope with various scenarios.

To help understand, here is an example: Webpack case-analysis of the principle of vue-loader .

appendix

Source code reading skills

  • avoid the heavy and light: pick the soft persimmon, for example, although the initialization process is winding, it is relatively the least conceptual and logical. From here to understand the entire working process, you can learn some common routines of webpack, such as hooks Design and function, coding rules, naming conventions, loading logic of built-in plug-ins, etc., are equivalent to entering a door first
  • learn to debug: use the ndb single-point debugging function to track the running of the program. Although there are many ways to debug node, I personally recommend ndb , which is flexible and simple. It is a big killer debugger
  • understand architecture: to some extent can be webpack architecture reduced to compiler + compilation + plugins , process webpack run there will be only a compiler ; and each compilation - including calling compiler.run function or watch = true files change when, will create a compilation object. Understand the design, responsibilities, and collaboration of these three core objects, and you can almost understand the core logic of webpack
  • small: plugin The key is "hooks", I suggest to pay attention to it strategically and ignore it tactically! After all, hooks are the key concept of webpack and the foundation of the entire plug-in mechanism. It is impossible to bypass hooks when learning webpack, but the corresponding logical jumps are too circumscribed and unintuitive. When looking at the code, it is complicated. Sex will increase dramatically, my experience is:

    • Take a careful look at the documentation of the tapable repository, or take a rough look at tapable , understand the concepts of synchronous hooks, asynchronous hooks, promise hooks, serial hooks, parallel hooks, etc., and have a finer understanding of the event model provided by tapable Call strategic importance
    • Don’t panic when you encounter hooks that you don’t understand. In my experience, I don’t even know what this class does. It’s too difficult to understand these hooks. It’s better to skip the meaning of the hook itself and see which plugins use it. Then go to the plug-in and add the debugger follow-up logic, there is a high probability that you will also know the meaning of the hook. This is called tactical neglect.
  • maintain curiosity: maintain strong curiosity and resilience during the learning process, be good at \& dare to ask questions, and then summarize your own answers based on the source code and community information. There may be many questions, such as:

    • Why do loader design pre, pitch, post, inline?
    • compilation.seal function has designed many optimized hooks. Why do we need to distinguish them so finely? What do webpack designers expect from different hooks?
    • Why do you need so many module subcategories? When are these subclasses used?

Module and Module subclasses

As can be seen from the above, the core process of the webpack construction phase basically revolves around module . I believe that readers who have contacted and used module should already have a perceptual understanding of module , but the logic to implement 06095019a07a70 is very complicated and heavy. of.

Take webpack\@5.26.3 as an example, there are 54 subclasses Module ( webpack/lib/Module.js

Unable to copy loading content

It is too tiring to understand the role of these types one by one. We need to grasp the essence: module the role of 06095019a07aae?

module is the basic unit of webpack resource processing. It can be considered that webpack's path analysis, reading, translation, analysis, and packaging output of resources are all carried out around the module. There are many articles that say module = file . In fact, this statement is not accurate. For example, the subclass AsyncModuleRuntimeModule is just a piece of built-in code, which is a resource and cannot be simply equivalent to the actual file.

Webpack is very extensible, including the processing logic of the module. For example, the entry file is an ordinary js. At this time, the NormalModule object is first created. When parsing the AST, it is found that the file also contains asynchronous loading statements, such as requere.ensure , then the corresponding The local will create the AsyncModuleRuntimeModule module and inject the asynchronously loaded template code. The 54 module subclasses in the above class diagram are all designed to adapt to various scenarios.

image.png

阅读 4.3k
avatar
范文杰
字节跳动 前端工程师
1.3k 声望
6.7k 粉丝
0 条评论
avatar
范文杰
字节跳动 前端工程师
1.3k 声望
6.7k 粉丝
文章目录
宣传栏