Recently, I have read the source code of Webpack, and have a certain understanding of its packaging process. Next, I will share the packaging process of Webpack with you based on the source code. At the end of the article, there will be a simple implementation of Webpack, to provide you with reference and learning ~

Webpack is a static module packaging tool for modern JavaScript applications. It combines every module you need in your project into one or more bundles for use in the browser

PS: This sharing is the source of Webpack4, the current latest Webpack5 compared to version 4 has a lot of upgrades and adjustments, but the general process is not very different, to provide you to learn its main process is not too big a problem

Overall Build process

Let’s take a look at the overall construction process of Webpack. The running process of Webpack is a serial process, from start to end, the following processes will be executed successively:

The concept is introduced

Before going into the details of the process, there are a few concepts that need to be explained:

Module

The Webpack website explains the module as follows:

In modular programming, developers break up programs into chunks of discrete functionality called modules. Each module is smaller than a full program, making verification, debugging, and testing a breeze. Well-written modules provide solid boundaries of abstraction and encapsulation, allowing each module in an application to have a coherent design and a clear purpose.

Modules are instantiated from NormalModule and stored in compilations. Modules. It can be understood as an intermediate state between source files and chunk, facilitating various operations during compilation.

Chunk & ChunkGroup

Chunk is the corresponding of each output file, including import file, asynchronous loading file, optimized cutting file, etc. It can contain multiple modules; In the compilation. Chunks; A chunkGroup can contain multiple chunks (import chunks or chunks of an asynchronous module). Entrypoint is a chunkGroup that contains the entry chunk. In the compilation. ChunkGroups.

Loader

Webpack itself can only handle JavaScript modules, and if you want to handle other types of files, you need to use loader for conversion. Loader is a Node module exported as a function. This function is called when the Loader converts the resource, takes the source file as an argument, and returns the result of the conversion. In this way, we can load any type of module or file, such as CoffeeScript, JSX, LESS, or images, via require.

module.exports = function (source) { 
    / /... Process the source
     
    return newSource; 
}; 
Copy the code

Plugins

Plug-ins are pluggable modules that can perform more functions that cannot be performed by the Loader during compilation. We’ll start with another library that Plugin relies on, Tapable, which is simply an implementation of a publish-subscribe model. It provides many types of hook classes (synchronous, asynchronous, parallel, fusing, etc.) that can tap several callback functions and execute the subscribed callback function accordingly on call. For example 🌰 :

const tapable = require("tapable"); 
 
const { SyncHook } = tapable; 
 
const hook = new SyncHook(); 
 
hook.tap("MyHook".() = > { console.log("enter MyHook")}); hook.tap("MyHook2".() = > { console.log("enter MyHook2")}); hook.call();// enter MyHook enter MyHook2
Copy the code

The WebPack Plugin is strongly coupled to the Tapable mentioned above, and we can see a simple implementation of the Plugin below:

class MyPlugin { 
  apply(compiler) { 
    compiler.hooks.make.tap("MyPlugin".(compilation) = > { 
      // ... 
      compilation.hooks.optimizeChunkAssets.tap("MyPlugin".(chunks) = > { 
        // ... }}}}Copy the code

Combining the above code we can notice:

  • Plugin must define an Apply method on the prototype and accept an instance of compiler as a parameter.
  • The apply method is called when the plug-in is initialized, so we can subscribe to the hooks on Compiler and compilation, and then call the subscribe callback when the compilation process executes when we are listening.

Webpack publishes specific events at specific points in time (compile-cycle hooks), the plug-in executes specific logic when it listens for the corresponding events, and the plug-in can change the results of The Webpack run by calling the API provided by Webpack

Compiler

The Compiler object contains the current configuration for running Webpack, including entry, output, loaders, etc. This object is instantiated when Webpack is started and is globally unique. The Plugin can use this object to retrieve Webpack configuration information for processing and listen for hooks in the Compiler for further processing. As follows:

class Compiler extends Tapable { 
    constructor(context){ 
        // ... 
        this.options = {}, 
        this.hooks = { 
              run: new AsyncSeriesHook(["compiler"]), 
              compile: new SyncHook(["params"]), 
              make: new AsyncParallelHook(["compilation"]), 
              // ... }}}Copy the code

Compilation

The Compilation object represents a resource version build. There is a Compilation every time we build, for example when we start watch, we create a new Compilation every time we detect a file change, A Compilation object represents the current module resources, compile-build resources, changing files, and state information about the dependencies being tracked. The Compilation module is used by the Compiler to create new compilations (or new builds). The Compilation instance has access to all modules and their dependencies (most of which are loop dependencies). It performs a literal compilation of all modules in an application’s dependency graph. At compile time, modules are loaded, sealed, optimized, chunked, hashed, and recreated. The Compilation object also provides callbacks to plugins that need to customize their functions, so that plug-ins can choose to use extensions when doing their own customization. In a nutshell, Compilation is about building modules and chunks and optimizing the build process with plug-ins.

To sum up, the relationship of the above concepts can be better understood through the following flow chart: each file can be created as a module, several modules can be parsed and compiled to form a chunk, and entry + asynchronous chunk can form a chunkGroup.

Build process refinement

Let’s take a look at some of the details of the three big steps of initialization, compilation, and output, using the flowchart and code

Initialize the

We use the webpack package code, usually on the command line command. / node_modules/bin/webpack, corresponding implementation file is actually node_modules/webpack/bin/webpack js, It is the webpack entry file, we start from the entry file analysis: first, get the currently installed CLI; And use cli as the execution entry, such as Webpack-CLI.

Webpack4 determines that either is installed. In WebPack 5, only WebPack-CLI is supported as the CLI tool

// bin/webpack.js 
#!/usr/bin/env node 
 
const installedClis = CLIs.filter(cli= > cli.installed); 
 
// Filter out the installed CLI
if (installedClis.length === 0) { 
  / /... The cli is not installed, prompting the user to install the WEBpack CLI
} else if (installedClis.length === 1) { 
  const path = require("path"); 
  const pkgPath = require.resolve(`${installedClis[0].package}/package.json`); 
  const pkg = require(pkgPath); 
   
  // require('webpack-cli'), that is, webpack-cli is the actual implementation of webpack entry
  require(path.resolve( 
    path.dirname(pkgPath), 
    pkg.bin[installedClis[0].binName] 
  )); 
} else { 
  / /.. Warning message is displayed after multiple CLI installation
   
  // * Only webpack-CLI is supported in Webpack 5
} 
Copy the code

Json./bin/cli.js. Let’s see what cli.js does:

  • A constant array of NON_COMPILATION_CMD determines whether the command needs to be compiled or not./utils/prompt-command.
  • /utils/convert-argv merges the parameters in webpack.config.js and the command line to options if you need to go through the compilation process;
  • Then, the Webpack package is introduced, and a globally unique compiler instance is initialized with the options obtained in the previous step as the parameter.
  • If the compiler mode is not Watch mode, call the compiler.run method to start the formal compilation stage.

// webpack-cli/bin/cli.js 
 
// NON_COMPILATION_ARGS specifies directives that do not need to be compiled;
// For example init, execute webpack init to initialize a webpack configuration
const NON_COMPILATION_CMD = process.argv.find(arg= > { 
  return NON_COMPILATION_ARGS.find(a= > a === arg); 
}); 
// If it is an instruction that does not need to be compiled, require the corresponding package and execute it
if (NON_COMPILATION_CMD) { 
  //  
  return require("./utils/prompt-command")(NON_COMPILATION_CMD, ... process.argv); }// Yargs helps build scaffolding by parsing parameters
yargs.parse(process.argv.slice(2), (err, argv, output) = > { 
    let options; 
    // Parse the configuration file and command line parameters to get the final options (webpack.config & command line parameters)
    // The plugin will be automatically loaded according to some parameters
    options = require("./utils/convert-argv")(argv); 
 
    // Use the resulting options to instantiate the Compiler and initiate compilation
    function processOptions(options) { 
        // Handle the case where options is a Promise
        if (typeof options.then === "function") { 
            options.then(processOptions); 
            return; 
        } 
 
        / /... Do some Settings for outputOptions
         
        // Instantiate the globally unique Compiler object with options
        const webpack = require("webpack");  // lib/webpack.js 
        compiler = webpack(options); 
         
        / /... Determine whether plug-ins are enabled and tap some events
 
        // Perform corresponding operations respectively to configure whether the watch listening mode is configured
        if (firstOptions.watch || options.watch) { 
            // ... watch 
        } else { 
            compiler.run(...) 
        } 
    } 
     
    processOptions(options); 
})) 
Copy the code

PS: The downward arrow in the flowchart represents the operation of the current main process, and the right arrow generally represents the sub-steps of the current big step

So what did we do in the previous step when we called Webpack (options), and how did we instantiate compiler? Webpack /lib/webpack.js:

  • If options is an array, each item is treated as a build target and is instantiated with MultiCompiler.

This is useful for bundling a library for multiple targets such as AMD and CommonJS

  • If option is a normal object, the WebpackOptionsDefaulter class first adds a large number of default parameters to options to get the final options.
  • Then instantiate the globally unique compiler using the options obtained in the previous step.
  • The plugins of options configuration were iterated, and the apply method was called to initialize the plug-in.
  • Finally, through the WebpackOptionsApply class, the configured options are converted into plug-ins and enabled (for example, UglifyJSPlugin plug-in is enabled by default in production mode, without user configuration).
// webpack/lib/webpack.js 
 
const webpack = (options, callback) = > { 
  let compiler; 
  / / multiple configuration: https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations
  if (Array.isArray(options)) { 
    compiler = new MultiCompiler( 
      Array.from(options).map(options= > webpack(options)) 
    ); 
  } else if (typeof options === "object") { 
    // WebpackOptionsDefaulter adds a number of default parameters to the Options configuration object
    options = new WebpackOptionsDefaulter().process(options); 
     
    // Instantiate the globally unique compiler corresponding to a single option
    compiler = new Compiler(options.context); 
    compiler.options = options; 
     
    // Enable the default plug-in configured in options
    if (options.plugins && Array.isArray(options.plugins)) { 
      for (const plugin of options.plugins) { 
        if (typeof plugin === "function") { 
          plugin.call(compiler, compiler); 
        } else{ plugin.apply(compiler); }}}// WebpackOptionsApply converts configured options to plug-ins and enables some default plug-ins
    compiler.options = new WebpackOptionsApply().process(options, compiler); 
  }  
   
  return compiler; 
}; 
 
// Compatible with esModule and CommonJS
exports = module.exports = webpack; 
Copy the code

In general, the initialization process is roughly as follows:

  • Initialize the options parameter.
  • Instantiate Compiler objects;
  • Initialize default and configured plug-ins.
  • The compiler’s run method is called to start compiling

compile

After calling Compiler. run, the compiler officially enters the compilation stage. Core steps of this stage:

  • Enter the compile function to instantiate compilation;
  • Execute the key make hook to start the actual compilation, which calls the Loader for code parsing, recursive module dependency parsing, and so on.
  • After that, the Programmer enters seal from Compilation, which creates chunks and has a lot of hooks to handle optimizations;

Let’s look at how the run method is compiled:

  • First, after passing the beforeRun and run hooks, compile the method;
  • BeforeCompile, compile hook, the hook that actually compiles — make;
// webpack/lib/compiler.js 
 
run() { 
    const onCompiled = (err, compilation) = > { 
      this.emitAssets(compilation, err= > { 
        this.emitRecords(err= > { 
          this.hooks.done.callAsync(stats, err= > { 
            return finalCallback(null, stats); // stats calculates the compile time
          }); 
        }); 
      }); 
    }; 
 
    this.hooks.beforeRun.callAsync(this.err= > { 
      this.hooks.run.callAsync(this.err= > { 
        this.readRecords(err= > { 
          this.compile(onCompiled); 
        }); 
      }); 
    }); 
} 
 
compile(callback) { 
    const params = this.newCompilationParams(); 
    this.hooks.beforeCompile.callAsync(params, err= > { 
      this.hooks.compile.call(params); 
      // Instantiate a compilation object
      const compilation = this.newCompilation(params); 
      // * make hook to start compiling
      this.hooks.make.callAsync(compilation, err= > { 
        compilation.finish(err= > { 
          / / seal stage
          compilation.seal(err= > { 
            this.hooks.afterCompile.callAsync(compilation, err= > { 
              return callback(null, compilation); }); }); }); }); }); }}Copy the code
  • To see what happens during the make phase, we can do a global search for hooks. Make. TapAsync to find relevant plug-ins subscribed to make, It includes SingleEntryPlugin, MultiEntryPlugin, DynamicEntryPlugin, etc.
  • We take SingleEntryPlugin as an example for analysis: In the Apply method of the plug-in, the entry information and context are taken as parameters, and addEntry method is called to compile from the entry.
// SingleEntryPlugin.js 
 
apply(compiler) { 
    // make 
    compiler.hooks.make.tapAsync( 
      "SingleEntryPlugin".(compilation, callback) = > { 
        const { entry, name, context } = this; 
        constdep = SingleEntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, callback); }); }Copy the code
  • In addEntry, _addModuleChain is called to process all modules in the module chain as follows:
    • Get the corresponding moduleFactory moduleFactory based on the dependent type;
    • Call the moduleFactory create method to create the module module;
    • Call the buildModule method, which at its core parses the Module using loaders and Parser;
    • After parsing, we go back to the create callback to continue parsing the module’s dependencies and recursively parsing them.
    • After processing, go back to the make callback, execute compilation.finish, and prepare to enter the SEAL phase
// Compliation.js 
 
addEntry(context, entry, name, callback) { 
    this.hooks.addEntry.call(entry, name); 
     
    this._addModuleChain( 
      context, 
      entry, 
      module= > this.entries.push(module), 
      (err, module) = > { 
        this.hooks.succeedEntry.call(entry, name, module); 
        return callback(null.module); }); }_addModuleChain(context, dependency, onModule, callback) { 
    const Dep = / * *@type {DepConstructor} * / (dependency.constructor); 
    // Get the corresponding module factory based on the type of dependency used to create modules
    const moduleFactory = this.dependencyFactories.get(Dep); 
 
    // Semaphore limits the number of modules that can be processed at the same time. The default value is 100
    this.semaphore.acquire(() = > { 
        // * Create a module using a module factory; Modules such as JS use NormalModuleFactory to create module objects
        moduleFactory.create( 
        {/* params*/}, 
        (err, module) = > { 
          // Add to this.modules
          const addModuleResult = this.addModule(module); 
          onModule(module); // this.entries.push(module); 
         
          // The module has been built, the dependencies have been collected, and the dependent module has been processed
          const afterBuild = () = > { 
            if (addModuleResult.dependencies) { 
              this.processModuleDependencies(module.err= > { 
                callback(null.module); 
              }); 
            } else { 
              return callback(null.module); }};// If the module has already been built, build is false
          if (addModuleResult.build) { 
            /* Call module.build to start building the module. Call loader to process the source file, use Acorn to generate the AST, iterate through the AST, create Dependency (Dependency) to add Dependency array */ 
            this.buildModule(module.false.null.null.err= >{ afterBuild(); }); }); }}buildModule(module, optional, origin, dependencies, thisCallback) { 
    // If multiple files reference the same dependency, do not repeat the build if the build has been cached
    let callbackList = this._buildingModules.get(module); 
    if (callbackList) { 
      callbackList.push(thisCallback); 
      return; 
    }     
    // set cache map 
    this._buildingModules.set(module, (callbackList = [thisCallback])); 
     
    this.hooks.buildModule.call(module); 
     
    // The build phase is mainly called loaders and parser
    module.build( 
      // ... params 
      error= > { 
        module.dependencies.sort(...) ;this.hooks.succeedModule.call(module); 
        returncallback(); }); }Copy the code

Let’s dive into the _module_. Build method here to take a look at some of the core processing flows inside. Take NormalModule as an example:

  • Call the this.doBuild method, which mainly calls runLoaders and executes the configured loader to parse the Module;
  • After parsing by loader, source that can be further parsed by JS is obtained. At this time, it is handed to Paser and parsed into AST (dependent on ACron library), and the AST is traversed to find Inport, require and other statements, that is, the dependencies of the current module.

Astexplorer.net/

  • On the end of the step, will return to _addModuleChain afterBuild callback, and perform processModuleDependencies, recursive dependencies of the module, The same process of module creation, runLoaders, and Parser is performed for these dependencies until all dependencies are parsed.

The following is posted some relevant code, only retained some key steps, easy to understand and read, interested students can compare the source code to understand the details of each implementation.

// NormalModule.js 
 
build(options, compilation, resolver, fs, callback) { 
    / /... Set some values for this
     
    return this.doBuild(options, compilation, resolver, fs, err= > { 
      this._cachedSources.clear(); 
 
      const handleParseResult = result= > { 
        this._initBuildHash(compilation); 
        return callback(); 
      }; 
 
        // Convert the code processed by Loader to AST through Parser, find dependent modules, and get the final result
        / / and then return to the Compilation of js buildModule method, the recursive call processModuleDependencies method to deal with the module dependent module
        // Its callback goes back to compiler.finish
        const result = this.parser.parse( 
            this._ast || this._source.source(), 
            // ... params 
        ); 
         
        if(result ! = =undefined) { handleParseResult(result); }}); }doBuild(options, compilation, resolver, fs, callback) { 
    const loaderContext = this.createLoaderContext(/ *... * /); 
 
    runLoaders( 
      {/* param */}, 
      (err, result) = > { 
        if(result) {🐞this.buildInfo.cacheable = result.cacheable; 
          this.buildInfo.fileDependencies = new Set(result.fileDependencies); 
          this.buildInfo.contextDependencies = new Set(result.contextDependencies); 
        } 
 
        // The result returned by the loader is 0-result; 1 - sourceMap; 2 - extra
        const source = result.result[0]; 
        const sourceMap = result.result.length >= 1 ? result.result[1] : null; 
        const extraInfo = result.result.length >= 2 ? result.result[2] : null; 
 
        // Convert the loader processing result to a string source
        this._source = this.createSource( 
          this.binary ? asBuffer(source) : asString(source), 
          result.resourceBuffer, 
          sourceMap 
        ); 
 
        returncallback(); }); }Copy the code
// loader-runner 
 
runLoaders(options, callback){ 
    / / set loaderContext
    loaderContext.xxx = xxx; 
    Object.defineProperty(loaderContext, 'xxx', {... })/ / execution
    iteratePitchingLoaders(processOptions, loaderContetxt, function(err, res){ 
        // ... })}iteratePitchingLoaders(options, loaderContext, callback) {     
    // Load the loader and execute
    loadLoader(currentLoaderObject, function(err)){ 
        // Fetch the loader function
        var fn = currentLoaderObject.pitch; 
        // Execute the loader to process the file and continue iteratePitchingLoaders recursively in the callbackrunSyncOrAsync(fn, loaderContext, args, errCallback); }}Copy the code
// Parser.parse 
 
parse(source, initialState) { // source is the string of source code
    let ast; 
    if (typeof source === "object"&& source ! = =null) { 
        ast = source; 
    } else { 
        // The AST is parsed by calling acron's parse method
        ast = Parser.parse(source, { sourceType: this.sourceType, onComment: comments }); 
    } 
     
    / / triggers the callback plugin (HarmonyDetectionParserPlugin and UseStrictPlugin) according to whether have the import/export and use strict increase depends on: HarmonyCompatibilityDependency HarmonyInitDependency, ConstDependency
    if (this.hooks.program.call(ast, comments) === undefined) { 
        // Check the strict mode
        this.detectMode(ast.body); 
        // Recursively iterate through statements to handle dependencies related to imports and exports, such as import and export
        this.prewalkStatements(ast.body); 
        // handle block traversal
        this.blockPrewalkStatements(ast.body); 
        ImportDependenciesBlock; // Use the method to recurse inside the function (the method recurses in walkFunctionDeclaration), and then recurse to find a dependency on the AST.
        this.walkStatements(ast.body); }}Copy the code
  • At the end of the make phase, compilation. Seal is called in its callback to enter the seal phase, creating chunk from the compiled Module. Its main logic:
    • Generate chunk graph based on entry.
  • Generate build hash;
  • Generate resources corresponding to chunk and save them in compilation.assets.

In addition, a large part of the seal phase code is to call performance optimization related built-in plug-ins;

// Compilation.js 
 
seal(callback) { 
  this.hooks.seal.call(); 
 
  this.hooks.beforeChunks.call(); 
 
  // Walk through the entry, generate chunk of the entry, and establish a relationship among all modules, chunks, and chunkgroups
  for (const preparedEntrypoint of this._preparedEntrypoints) { 
    const chunk = this.addChunk(name); 
    const entrypoint = new Entrypoint(name); 
    this.chunkGroups.push(entrypoint); 
    GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); 
    GraphHelpers.connectChunkAndModule(chunk, module); 
    this.assignDepth(module); 
  } 
   
  // Generate and optimize the chunk dependency graph to establish the relationship between each module
  buildChunkGraph( 
    this./ * *@type {Entrypoint[]} * / (this.chunkGroups.slice()) 
  ); 
 
  this.sortModules(this.modules); 
   
  this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err= > { 
 
    this.applyModuleIds(); 
    this.sortItemsWithModuleIds(); 
 
    this.applyChunkIds(); 
    this.sortItemsWithChunkIds(); 
 
    // Generate the Hash for this build, which can be configured using the [Hash] placeholder of webpack.config
    this.createHash(); 
     
    this.createModuleAssets(); 
     
    if (this.hooks.shouldGenerateChunkAssets.call() ! = =false) { 
      this.hooks.beforeChunkAssets.call(); 
      // Generate the source code using the corresponding template based on the chunk type (enrty/ dynamic import)
      // Call the emitAsset method to save it to the assets property, which is the final list of files to be generated
      this.createChunkAssets(); 
    } 
 
    this.summarizeDependencies(); 
  }); 
} 
Copy the code

The compilation phase can be summarized as follows:

  • Instantiate compilation;
  • Perform make hook callback, recursively parse module;
  • Create chunk according to the compiled module, and generate the corresponding resource module for output;

The output

The output part, mainly in the callback of the previous SEAL phase, goes to the complier. emitAssets method and generates a specific file from the resources processed in the SEAL process.

// Compiler.js 
 
emitAssets(compilation, callback) { 
    const emitFiles = () = > { 
      asyncLib.forEachLimit( 
        compilation.getAssets(), 
        15.({ name: file, source }, callback) = > { 
            // Write to the file
            this.outputFileSystem.writeFile(targetPath, content); }}// Create the output directory recursively
    this.outputFileSystem.mkdirp(outputPath, emitFiles); 
} 
Copy the code

Create folder with outputFileSystem. Mkdirp and execute the emitFiles callback, in which asynclib. forEachLimit is executed in parallel. Write each source to the real path’s file. At this point, the entire build process is basically over

Summary of the Webpack build process

  1. Initialization parameters: read and merge parameters from configuration files and Shell statements to get the final parameters;
  2. Start compiling: initialize the Compiler object using the parameters obtained in the previous step, load all the configured plug-ins, and execute the object’s run method to start compiling.
  3. Determine entry: find all entry files according to the entry in the configuration;
  4. Module compilation: Starting from the entry file, call all configured Loader to translate the module, find out the module that the module depends on, and then recurse this step until all the entry dependent files have gone through this step;
  5. Complete module compilation: After using Loader to translate all modules in step 4, obtain the final content of each module after translation and the dependencies between them;
  6. Output resources: assemble chunks containing multiple modules one by one according to the dependency between the entry and modules, and then convert each Chunk into a separate file and add it to the output list. This step is the last chance to modify the output content.
  7. Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system.

How to implement a simple Webpack

Key points:

  • Provide a compilation configuration file similar to webpack.config.js (mywebpack.config.js)
  • Implement a Compiler to get configuration (options), instantiate Compilation, run build (run);
  • Implement a Compilation for parsing files (Run Loader), analyzing dependencies (parse into AST), and generating chunks (SEAL).
  • Support for tapable based plug-in mechanism;
  • Loader parsing files can be configured.
  • Init ->run->make->seal->emit;

Implementation code: github.com/tangxiaojun…

Webpack generated file analysis: github.com/woai3c/Fron…

Write in the last

The above leads you to combine the source code, as well as a simple Webpack implementation, the overall construction process of Webpack to do a general understanding; Each step behind the actual basic involves a lot of details, the source code is relatively large, interested partners can be combined with the construction process of this share, for some of the steps of interest to continue to in-depth understanding and learning of the source code ~ 😄

PS: There are some links posted at the bottom of the article for your reference if you are interested


Refer to the article: blog.flqin.com/categories/… Juejin. Cn/team / 694381… www.zhihu.com/people/fe_k… Implement a plug-in: segmentfault.com/a/119000002… Webpack packaging process analysis: github.com/darrell0904… Seal Encapsulates the build file core process: github.com/amandakelak… Implement a simple Webpack: juejin.cn/post/685953…