preface

As front-end engineering becomes more and more complex today, module packaging tools play an increasingly important role in our development, among which Webpack is one of the most popular packaging tools.

Speaking of WebPack, many friends may feel both familiar and unfamiliar, familiar because we use it in almost every project, and unfamiliar because of the complex configuration and various functions of WebPack. The nature of Webpack seems even more remote and unfathomable, especially when we use application frameworks such as umi.js to encapsulate webPack configuration.

When the interviewer asks you if you know webPack, you might be able to name a bunch of webPack loaders and plugins that you know a lot about. You might even be able to name a bunch of plugins and configurations that do on-demand loading and packaging optimization. So today we will explore the ability of Webpack boundary, try to understand some of the implementation process and principle of Webpack, do not do API engineer.

Do you know what webpack does?

It is not difficult to understand from the description on the official website that the functions of Webpack are as follows:

  • Module packaging. You can package files from different modules together and make sure they are referenced correctly and executed in an orderly manner. With packaging we can freely divide the file modules according to our own business at development time, keeping the project structure clear and readable.
  • Compile compatibility. Writing a bunch of browser-compatible code by hand has always been a headache for front-end engineers in the “ancient days”, but today this problem has been greatly reduced and passedwebpacktheLoaderMechanics that help us do more than just codepolyfillYou can also compile transformations such as.less, .vue, .jsxThis kind of format file cannot be recognized by the browser, so that we can use new features and new syntax to improve development efficiency.
  • Capacity expansion. throughwebpackthePluginMechanism, on the basis of modular packaging and compiler compatibility, we can further achieve a series of functions such as loading on demand, code compression and so on, helping us to further improve the degree of automation, engineering efficiency and the quality of packaging output.

How does module packaging work?

If an interviewer asks you how Webpack pulls these modules together and makes them work, do you know?

First, we should take a quick look at the whole packaging process of WebPack:

  • 1, readwebpackConfiguration parameters of;
  • 2, start,webpackTo createCompilerObject and begin parsing the project;
  • 3. Import files (entry) start parsing, and find the imported dependency module, recursive traversal analysis, forming a dependency tree;
  • 4. Use the corresponding dependency module files for different file typesLoaderCompile, and finally convertJavascriptFile;
  • 5. The whole processwebpackI’m going to throw some out, through the publish-subscribe modelhooksAnd thewebpackBy listening to these key event nodes, plug-ins can perform plug-in tasks to achieve the purpose of intervening in the output results.

Among them, file parsing and construction is a relatively complex process, which mainly relies on compiler and Compilation in Webpack source code.

The Compiler object is a global singleton that controls the entire WebPack-packed build process. The Compilation object is the context object for each build, which contains all the information required for that build. For each hot update and rebuild, compiler generates a new compilation object, which is responsible for the build process of the update.

The dependencies between each module depend on the AST syntax tree. After each module file is parsed by Loader, the AST syntax tree of module code will be generated through acorn library. Through the syntax tree, the module can be analyzed whether there are dependent modules, and then the compilation and parsing of the next module will continue.

The final bundle packaged by Webpack is an IIFE execution function.

() => {// webpackBootstrap var __webpack_modules__ = ({' file-a-path ': ((modules) => { // ... }) 'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... }) }) // The module cache var __webpack_module_cache__ = {}; // The require function function __webpack_require__(moduleId) { // Check if module is in cache var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule ! == undefined) { return cachedModule.exports; } // Create a new module (and put it into the cache) var module = __webpack_module_cache__[moduleId] = { // no module.id  needed // no module.loaded needed exports: {} }; // Execute the module function __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId"); // Return the exports of the module return module.exports; } // startup // Load entry module and return exports // This entry module can't be inlined because the eval devtool is used. var __webpack_exports__ = __webpack_require__("./src/index.js"); })Copy the code

Compared to WebPackage 4, the bundle bundled with WebPackage 5 is quite streamlined. In the packaged demo above, there are only three variables and one function method in the immediate function. __webpack_modules__ holds the JS content of each compiled file module. __webpack_module_cache__ is used for module caching. __webpack_require__ is a set of dependency import functions implemented within Webpack. The last sentence is the starting point for the code to run, starting with the entry file and launching the entire project.

It is worth mentioning that the __webpack_require__ Module introduces functions. We usually use ES Module or CommonJS specification to export/import dependent modules in modular development. When webpack is compiled, It will be replaced with its own __webpack_require__ to realize the import and export of modules, so as to realize the module caching mechanism, and erase some differences between different module specifications.

Do you know what sourceMap is?

When it comes to sourceMap, many people will immediately think of the Devtool parameter in the Webpack configuration and the eval, eval-cheap-source-map and other optional values and what they mean. In addition to knowing the differences between different parameters and the performance differences, we can also take a look at how sourceMap is implemented.

SourceMap is a technology that maps compiled, packaged, and compressed code back to source code. Since packaged and compressed code is not readable, it is a very bad experience to debug problems in obtruded code in case of errors or problems during development. SourceMap can help us quickly locate source code and improve our development efficiency. SourceMap is not really a WebPack-specific feature, but rather Webpack supports sourceMap, as JQuery also supports souceMap.

Since it is a source code mapping, it is necessary to have a map file to mark the location of the corresponding source code in the obtruded code. Usually this map file ends with a.map and the data structure inside it looks something like this:

{"version" : 3, // Source Map version" file": "out.js", // Output file (optional) "sourceRoot": "", // Source file root directory (optional) "sources": ["foo.js", "bar.js"], // Source file list "sourcesContent": [null, null], // Source content list (optional, same order as source file list) "names": [" SRC ", "maps", "are", "fun"], / / the mappings to use symbolic names list "the mappings" : "A, AAAB;; ABCDE;" // String with encoding mapping data}Copy the code

The mappings data has the following rules:

  • Each group of a line in the build file is marked with “;” Space;
  • Separate each paragraph with a comma.
  • Each segment consists of 1, 4, or 5 variable-length fields;

With this mapping file, we just need to add this comment at the very end of our compression code to make sourceMap work:

//# sourceURL=/path/to/file.js.map
Copy the code

With this comment, the browser retrieves the mapping file via sourceURL, parses it through the interpreter, and maps the source code to the obfuscated code. So sourceMap is also a browser-dependent technology.

If we look closely at the bundles that Webpack packages, we can see that in the default development mode, the code at the end of each _webpack_modules__ file module, //# sourceURL=webpack://file-path? To achieve support for sourceMap.

SourceMap mapping table generation has a complex set of rules. If you are interested in the sourceMap mapping table generation, you can read the following article to understand the principle of soucrMap implementation:

Research on the principle of Source Map [1]

Source Maps under the Hood — VLQ, Base64 and Yoda[2]

Have you written Loader? How to write loader?

In fact, Webpack can only process JS module code by default. During the packaging process, all files encountered will be parsed as Javascript code by default. Therefore, when the project has non-JS files, we need to perform necessary conversion before continuing to execute the packaging task, which is also the significance of the Loader mechanism.

Loader configuration we should be very familiar with:

// webpack.config.js module.exports = { // ... other config module: { rules: [ { test: /^your-regExp$/, use: [ { loader: 'loader-name-A', }, { loader: 'loader-name-B', } ] }, ] } }Copy the code

According to the configuration, Loader supports multiple configurations for each file type in the form of an array. Therefore, when Webpack converts this file type, it calls each Loader in sequence and the content returned by the previous loader is used as the input parameter of the next loader. Therefore, the development of Loader needs to follow some norms. For example, the return value must be the standard JS code string to ensure the normal operation of the next Loader. At the same time, the development needs to strictly follow the “single responsibility” and only care about the output of Loader and its corresponding output.

This context in the loader function is provided by Webpack, which can obtain various information data required by the current loader through the relevant properties provided by this object. In fact, this object refers to a Loader-Runner specific object called loaderContext. Interested partners can read the source code.

module.exports = function(source) { const content = doSomeThing2JsString(source); // If the loader configures the options object, this.query will point to options const options = this.query; Console. log('this.context'); // Console. log('this.context'); / * * this. The callback parameter: * error: error | null, when loader error to throw an error * content: String | Buffer, after loader compiled need to export the * sourceMap: the content of the generated for the convenience of debugging the compiled contents of the source map * ast: */ this.callback(null, content); */ this.callback(null, content) // or return content; }Copy the code

For more detailed development documentation, please refer to the Loader API on the official website [3].

Have you written a Plugin? Describe briefly the idea of writing a plugin?

If Loader is responsible for file conversion, Plugin is responsible for functionality extension. Loader and Plugin, as two important parts of Webpack, assume two different responsibilities.

As mentioned earlier, WebPack is based on a publish-subscribe model, which broadcasts a number of events during the runtime lifecycle. By listening for these events, plug-ins can perform their own plug-in tasks at specific stages to achieve their desired functionality.

Given the publish-subscribe model, it is important to know which event hooks Webpack provides for plug-in developers to use. Compiler and Compilation are two core Webpack objects. Compiler exposes hooks related to the entire Webpack lifecycle (Compiler-hooks [4]), while Compilation exposes lower-grained event hooks related to modules and dependencies (Compilation hooks[5]).

The event mechanism of Webpack is based on a Tapable event stream scheme implemented by Webpack itself (Github [6]).

// Tapable const {SyncHook} = require(" Tapable "); Constructor () {this.hooks = {accelerate: new SyncHook(["newSpeed"]), brake: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; } / *... */ } const myCar = new Car(); Tap ("WarningLampPlugin", () => warninglamp.on ()));Copy the code

The development of Plugin is the same as the development of Loader, which needs to follow some development norms and principles:

  • A plug-in must be a function or an includeapplyMethod in order to accesscompilerInstance;
  • Passed to each plug-incompiler 和 compilationObjects have the same reference, and if you change their properties in one plug-in, it will affect later plug-ins.
  • Asynchronous events need to be notified by calling the callback function when the plug-in has finished processing the taskWebpackGo to the next flow, otherwise it will get stuck;

With that in mind, it’s not that difficult to develop a Webpack Plugin.

Class MyPlugin {apply (compiler) {// Find the appropriate event hook, Tap ('MyPlugin', compilation => {// compilation: console.log(compilation); // compilation: console.log(compilation); // do something... }}})Copy the code

For more detailed development documentation, please refer to the Plugin API on the official website [7].

The last

This article is also combined with some excellent articles and the source of Webpack itself, roughly said a few relatively important concepts and processes, the implementation details and design ideas also need to be combined with the source code to read and slowly understand.

Webpack as an excellent packaging tool, it changes the traditional front-end development mode, is the cornerstone of modern front-end development. Such a good open source project, there are many good design ideas and concepts can draw lessons from, naturally we should not only stay in the use of API level, try to read the source code with the problem, understand the process and principle of implementation, also can let us learn more knowledge, understand more deeply, be adept in project application.