This chapter mainly includes the following parts:

  • React/Vue scaffolding configuration details
  • Customize the Loader/Plugin based on Webpack5
  • Implement a simple Webpack5 yourself

Before you learn this chapter, you can learn Webpack from the beginning to the master – Basic chapter, the basic chapter mainly tells the following content:

  • Webpack profile
  • At the beginning of Webpack experience
  • Basic configuration of the Webpack development environment
  • Basic configuration of the Webpack production environment
  • Webpack optimizes configuration
  • Webpack configuration details
  • Webpack5 use

For more details on Webpack configuration, you can check out Webpack’s official website

Webpack from beginner to Master – Advanced source

A, create the react – app react – project

This part is only about the project through scaffolding to create the analysis route and steps, each folder about what content respectively in the source code for annotation.

Expose the configuration file with NPM Run eject

  1. Config –>paths.js (expose paths externally)
  2. Scripts –>start.js (corresponding file for development environment)
  3. Webpack.config. js (the main content is the configuration of loader and Plugin, you can directly modify loader and plugin in this folder in the future) (core)
  4. Scripts –>build.js (production environment file, similar to the development environment file)

Vue create vue-project

Here is only about the project through scaffolding to create the analysis route and steps, the specific folder about what is annotated in the source code.

  • through vue inspect --mode=development > webpack.dev.jsThe vue development environment configuration is packaged together under the webpack.dev.js file, and the development environment code only needs to explore the webpack.dev.js file
  • through vue inspect --mode=production > webpack.prod.jsThe vue production environment configuration is packaged together under the webpack.prod.js file, and the production environment code only needs to explore the webpack.prod.js file

Development environment file webpack.dev.js Production environment file webpack.prod.js (same as development environment except for CSS and multi-threaded packaging)

3. Customize loader

3.1 Preliminary Knowledge

Loader is essentially a function

  1. Loaders are executed from the bottom up in the use array
  2. There is a pitch method in loader, and the execution sequence of pitch methods in use array is from top to bottom. Therefore, if we want to perform some functions first, we can define the pitch method first
  3. Synchronous loader
/ / way
module.exports = function (content, map, meta) {
  console.log(111);

  return content;
}
2 / / way
module.exports = function (content, map, meta) {
  console.log(111);

  this.callback(null, content, map, meta);
}

module.exports.pitch = function () {
  console.log('pitch 111');
}
Copy the code
  1. Asynchronous loader
// Asynchronous loader (recommended, loader can perform other steps during asynchronous loading)
module.exports = function (content, map, meta) {
  console.log(222);

  const callback = this.async();

  setTimeout(() = > {
    callback(null, content);
  }, 1000)}module.exports.pitch = function () {
  console.log('pitch 222');
}
Copy the code
  1. Get the Options library:

Install loader-utils: CNPM install loader-utils 6. Validate options library: import validate from schema-utils in loader and use create schema.json file to validate and use

Loader3. Js code in

// 1.1 Getting Options introduced
const {
	getOptions
} = require('loader-utils');
2.1 Get validate (verify options are valid
const {
	validate
} = require('schema-utils');

// 2.3 Create schema.json file verification rules and import them
const schema = require('./schema');

module.exports = function(content, map, meta) {
	// 1.2 Obtaining options
	const options = getOptions(this);

	console.log(333, options);

	// 2.2 Checking whether options are valid
	validate(schema, options, {
		name: 'loader3'
	})

	return content;
}

module.exports.pitch = function() {
	console.log('pitch 333');
}
Copy the code

In the schema. The json code

{
	"type": "object"."properties": {
		"name": {
			"type": "string"."description": "Name ~"}},"additionalProperties": false // If this parameter is set to true, other types can be verified. If this parameter is set to false, other types cannot be verified
}
Copy the code

Webpack. Config. The js code

const path = require('path');

module.exports = {
    module: {
      rules: [{
        test: /\.js$/,
        use: [
          {
            loader: 'loader3'./ / the options section
            options: {
              name: 'jack'.age: 18}}]}]},// Configure the loader resolution rule: which folder does our loader go to?
    resolveLoader: {
      modules: [
        'node_modules',
        path.resolve(__dirname, 'loaders')]}}Copy the code

3.2 Customizing the babel-loader

  1. Creating a Verification Rule

babelSchema.json

{
  "type": "object"."properties": {
    "presets": {
      "type": "array"}},"addtionalProperties": true
}
Copy the code
  1. Create a loader

babelLoader.js

const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');

const babelSchema = require('./babelSchema.json');

// babel.transform The method used to compile code
// is a normal asynchronous method
// util.promisify Converts normal asynchronous methods into promise-based asynchronous methods
const transform = util.promisify(babel.transform);

module.exports = function (content, map, meta) {
  // Obtain the Loader options configuration
  const options = getOptions(this) | | {};// Verify the configuration of Babel options
  validate(babelSchema, options, {
    name: 'Babel Loader'
  });

  // Create asynchrony
  const callback = this.async();

  // Use Babel to compile code
  transform(content, options)
    .then(({code, map}) = > callback(null, code, map, meta))
    .catch((e) = > callback(e))

}
Copy the code
  1. BabelLoader use

webpack.config.js

const path = require('path');

module.exports = {
    module: {
      rules: [{
        test: /\.js$/,
        loader: 'babelLoader'.options: {
          presets: [
            '@babel/preset-env']]}}},// Configure the loader resolution rule: which folder does our loader go to?
    resolveLoader: {
      modules: [
        'node_modules',
        path.resolve(__dirname, 'loaders')]}}Copy the code

Iv. Custom Plugin

4.1 Preparation – Compiler hooks

4.1.2 tapable

hooks tapable

  1. Install tapable: NPM install tapable -d
  2. SyncHook, SyncBailHook 2.2 Asynchronous hooks, AsyncParallelHook, AsyncSeriesHook
  3. Register events/add callback functions in the hooks container
  4. Trigger hooks
  5. Startup file: node tapable.test.js

File tapable. Test. Js

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');

class Lesson {
constructor() {
  // Initialize the hooks container
  this.hooks = {
    // synchronize hooks, tasks will be executed in sequence
    // go: new SyncHook(['address'])
    // SyncBailHook: Exits when there is a return value ~
    go: new SyncBailHook(['address']),

    / / asynchronous hooks
    // AsyncParallelHook: AsyncParallelHook
    // leave: new AsyncParallelHook(['name', 'age']),
    // AsyncSeriesHook: asynchronous serial
    leave: new AsyncSeriesHook(['name'.'age'])}}tap() {
  // Register events in the hooks container/add callback functions
  this.hooks.go.tap('class0318'.(address) = > {
    console.log('class0318', address);
    return 111;
  })
  this.hooks.go.tap('class0410'.(address) = > {
    console.log('class0410', address);
  })

  // tapAsync is used with a callback function
  this.hooks.leave.tapAsync('class0510'.(name, age, cb) = > {
    setTimeout(() = > {
      console.log('class0510', name, age);
      cb();
    }, 2000)})// Return promise
  this.hooks.leave.tapPromise('class0610'.(name, age) = > {
    return new Promise((resolve) = > {
      setTimeout(() = > {
        console.log('class0610', name, age);
        resolve();
      }, 1000)})})}start() {
  / / trigger hooks
  this.hooks.go.call('c318');
  this.hooks.leave.callAsync('jack'.18.function () {
    // the function in the leave container is triggered when all functions in the leave container are triggered
    console.log('end~~~'); }); }}const l = new Lesson();
l.tap();
l.start();
Copy the code

4.1.2 compiler hooks

  1. How it works: asynchronous serial execution, so the following code is output in the following order: 1.1 EMIT. Tap 111 1.2 Emit. TapAsync 111 1.3 Emit. TapPromise 111 1.4 afterEmit
  2. TapAsync and tapPromise denote asynchracy
  3. Here is a brief introduction of complier, which can be written according to the documentation in the specific development process (very convenient).
class Plugin1 {
  apply(complier) {

    complier.hooks.emit.tap('Plugin1'.(compilation) = > {
      console.log('emit.tap 111');
    })

    complier.hooks.emit.tapAsync('Plugin1'.(compilation, cb) = > {
      setTimeout(() = > {
        console.log('emit.tapAsync 111');
        cb();
      }, 1000)
    })

    complier.hooks.emit.tapPromise('Plugin1'.(compilation) = > {
      return new Promise((resolve) = > {
        setTimeout(() = > {
          console.log('emit.tapPromise 111');
          resolve();
        }, 1000)
      })
    })

    complier.hooks.afterEmit.tap('Plugin1'.(compilation) = > {
      console.log('afterEmit.tap 111');
    })

    complier.hooks.done.tap('Plugin1'.(stats) = > {
      console.log('done.tap 111'); }}})module.exports = Plugin1;
Copy the code

4.2 Compilation Hooks

4.2.1 Episode: Debugging in nodeJS environment

  1. Package. Json input (– — inspect – BRK said through a breakpoint debugging,,,,,,, / node_modules/webpack/bin/webpack js “said debugging the file,,,,,, said node by node running)
"scripts": {
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  }
Copy the code
  1. Open a debugger where you need to debug
  2. Run the file through Node
  3. Right – click check in a website and click on the green icon

You can debug it, just like you would debug code on a web page

4.2.2 compilation hooks

  1. Initialize the Compilation hook
  2. Add an A.txt file to the output resource
  3. Read the contents of B.txt and add the contents of B.txt to the B.TXT file of the output resource 3.1 Reading the contents of B.TXT requires node’s readFile module 3.2 Adding the contents of B.TXT to the B.TXT file of the output resource Except using the method in 2, Two other forms can be used 3.2.1 Using RawSource 3.2.2 Using RawSource and emitAsset
const fs = require('fs');
const util = require('util');
const path = require('path');

const webpack = require('webpack');
const { RawSource } = webpack.sources;

// Make the fs.readFile method asynchronous based on promise style
const readFile = util.promisify(fs.readFile);

/* 1. Initialize the compilation hook 2. Add an A.compilation file to the output resource 3. Read the contents of B.txt and add the contents of B.txt to the B.TXT file of the output resource 3.1 Reading the contents of B.TXT requires node's readFile module 3.2 Adding the contents of B.TXT to the B.TXT file of the output resource Except using the method in 2, Two other forms can be used 3.2.1 Using RawSource 3.2.2 Using RawSource and emitAsset */

class Plugin2 {

  apply(compiler) {
    // 1. Initialize the compilation hook
    compiler.hooks.thisCompilation.tap('Plugin2'.(compilation) = > {
      // debugger
      // console.log(compilation);
      // Add resources
      compilation.hooks.additionalAssets.tapAsync('Plugin2'.async (cb) => {
        // debugger
        // console.log(compilation);

        const content = 'hello plugin2';

        // 2. Add an a.txt to the output resource
        compilation.assets['a.txt'] = {
          // File size
          size() {
            return content.length;
          },
          // File contents
          source() {
            returncontent; }}const data = await readFile(path.resolve(__dirname, 'b.txt'));

        // 3.2.1 compilation. Assets ['b.txt'] = new RawSource(data);
        / / 3.2.1
        compilation.emitAsset('b.txt'.newRawSource(data)); cb(); }}})})module.exports = Plugin2;
Copy the code

Custom CopyWebpackPlugin

Copy files from the public folder to the dist folder (ignore the index.html file).

  1. Create a schema.json verification file
{
  "type": "object"."properties": {
    "from": {
      "type": "string"
    },
    "to": {
      "type": "string"
    },
    "ignore": {
      "type": "array"}},"additionalProperties": false
}
Copy the code
  1. Create the copyWebPackplugin.js plug-in file

Download schema-utils and globby: NPM install globby schema-utils -d Copy resources from from to to. Filter ignore files 2. Read all resources in paths 3. Generate resources in Webpack format. 4. Add compilation and output

const path = require('path');
const fs = require('fs');
const {promisify} = require('util')

const { validate } = require('schema-utils');
const globby = require('globby');// globby is used to match file targets
const webpack = require('webpack');

const schema = require('./schema.json');
const { Compilation } = require('webpack');

const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources

class CopyWebpackPlugin {
  constructor(options = {}) {
    // Verify that options comply with the specification
    validate(schema, options, {
      name: 'CopyWebpackPlugin'
    })

    this.options = options;
  }
  apply(compiler) {
    // Initialize the compilation
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin'.(compilation) = > {
      // Add resource hooks
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin'.async (cb) => {
        // Copy the resource from to to
        const { from, ignore } = this.options;
        const to = this.options.to ? this.options.to : '. ';
        
        // Context is the Webpack configuration
        // The directory to run the instruction
        const context = compiler.options.context; // process.cwd()
        // Change the input path to an absolute path
        const absoluteFrom = path.isAbsolute(from)?from : path.resolve(context, from);

        // 1. Filter out ignore files
        // globby(folder to process, options)
        const paths = await globby(absoluteFrom, { ignore });

        console.log(paths); // Array of all file paths to load

        // 2. Read all the resources in paths
        const files = await Promise.all(
          paths.map(async (absolutePath) => {
            // Read the file
            const data = await readFile(absolutePath);
            // basename gets the last file name
            const relativePath = path.basename(absolutePath);
            // Combine with the to attribute
            // No to --> reset.css
            // have to > CSS /reset.css(corresponding to the to name of the webpackplugin in webpack.config.js CSS)
            const filename = path.join(to, relativePath);

            return {
              // File data
              data,
              // File name
              filename
            }
          })
        )

        // 3. Generate resources in Webpack format
        const assets = files.map((file) = > {
          const source = new RawSource(file.data);
          return {
            source,
            filename: file.filename
          }
        })
        
        // 4. Add compilation and output
        assets.forEach((asset) = >{ compilation.emitAsset(asset.filename, asset.source); }) cb(); }}})})module.exports = CopyWebpackPlugin;
Copy the code
  1. Used in webpack.config.js

Customizing Webpack

5.1 Webpack Execution process

  1. Initialize Compiler: Webpack (config) gets the Compiler object
  2. Start compiling: The Compiler object run method is called to start compiling
  3. Identify entry: Locate all entry files according to the entry in the configuration.
  4. Compiling module: Starting from the entry file, call all configured Loader to compile the module, and then find the modules that the module depends on, recursively until all modules are loaded in
  5. Complete module compilation: After compiling all modules using Loader in Step 4, the compiled final contents of each module and their dependencies are obtained.
  6. Output resources: according to the dependency between the entry and modules, assemble the chunks containing multiple modules one by one, and then convert each Chunk into a separate file and add it to the output list. (Note: This step is the last chance to modify the output.)
  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

5.2 Preparations

  1. Create folder myWebpack
  2. Create SRC –>(add.js/count.js/index.js) and write the corresponding js code
  3. Create config–>webpack.config.js to write basic webpack configuration (entry and Output)
  4. Create a lib folder to write the main configuration for WebPack
  5. Create script–>build.js (import the core myWebpack code in the lib folder and the basic webpack configuration in the config file and call run() to start packing)
  6. To facilitate startup, the console enters commandsnpm init -yPull the package.json file and change the part of scripts in the file to"build": "node ./script/build.js"Enter a command on the terminalnpm run buildWill run the /script/build.js file to add to scripts"debug": "node --inspect-brk ./script/build.js"Enter a command on the terminalnpm run debugWill debug the code in the /script/build.js file, as described in Chapter 4

5.3 Parsing files using Babel

  1. Create the file lib–>myWebpack1–>index.js
  2. Download three Babel packages from Babel

NPM install@babel /parser -d is used to parse code into ast abstract syntax tree NPM install@babel /traverse -d is used to traverse AST abstract syntax tree code NPM install @babel/ core-d is used to compile syntax in code that is not recognized by the browser. 1. Read the contents of the entry file 2. Parse it into an AST abstract syntax tree 3. Collect dependencies 4. Compile code: Compile syntax in code that the browser does not recognize

index.js

const fs = require('fs');
const path = require('path');

/ / the Babel of libraries
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

function myWebpack(config) {
  return new Compiler(config);
}

class Compiler {
  constructor(options = {}) {
    this.options = options;
  }
  // Start webpack packaging
  run() {
    // 1. Read the contents of the import file
    // Entry file path
    const filePath = this.options.entry;
    const file = fs.readFileSync(filePath, 'utf-8');
    // 2. Parse it into an AST abstract syntax tree
    const ast = babelParser.parse(file, {
      sourceType: 'module' // The modular solution for parsing files is ES Module
    })
    // debugger;
    console.log(ast);

    // Get the file folder path
    const dirname = path.dirname(filePath);

    // Define a container for storing dependencies
    const deps = {}

    // 3. Collect dependencies
    traverse(ast, {
      // The program. Body is iterated through the AST to determine the statement type
      // If type: ImportDeclaration fires the current function
      ImportDeclaration({node}) {
        // File relative path: './add.js'
        const relativePath = node.source.value;
        // Generate an absolute path based on the entry file
        const absolutePath = path.resolve(dirname, relativePath);
        // Add dependenciesdeps[relativePath] = absolutePath; }})console.log(deps);

    // 4. Compile code: Compile the syntax in the code that the browser does not recognize
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']})console.log(code); }}module.exports = myWebpack;
Copy the code

5.4 modular

In the process of code development, we pay attention to modular development. The codes with different functions are created in different files: myWebpack2–>parser.js (parsed code) /Compiler.js (compiled code) /index.js (main file)

5.5 Collecting all dependencies

The build function is used to build the code, modules in the run function collects all dependencies through recursive traversal, and depsGraph is used to organize dependencies into a dependency graph (the specific code functions are commented in the code).

5.6 Generating packaged Bundles

The entire myWebpack–>Compiler.js code is located in the bundle section of myWebpack–>Compiler.js

const path = require('path');
const fs = require('fs');
const {
  getAst,
  getDeps,
  getCode
} = require('./parser')

class Compiler {
  constructor(options = {}) {
      // Webpack configures the object
      this.options = options;
      // All dependent containers
      this.modules = [];
    }
    // Start webpack packaging
  run() {
    // Entry file path
    const filePath = this.options.entry;

    // Get the information about the entry file for the first build
    const fileInfo = this.build(filePath);

    this.modules.push(fileInfo);

    // Iterate over all dependencies
    this.modules.forEach((fileInfo) = > {
      /** { './add.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/add.js', './count.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/count.js' } */
      // Retrieve all dependencies for the current file
      const deps = fileInfo.deps;
      / / traverse
      for (const relativePath in deps) {
        // Depends on the absolute path of the file
        const absolutePath = deps[relativePath];
        // Process dependent files
        const fileInfo = this.build(absolutePath);
        // Add the result to modules, and the next iteration will iterate over it.
        this.modules.push(fileInfo); }})console.log(this.modules);

    // Collate dependencies to better dependency diagrams
    /* { 'index.js': { code: 'xxx', deps: { 'add.js': "xxx" } }, 'add.js': { code: 'xxx', deps: {} } } */
    const depsGraph = this.modules.reduce((graph, module) = > {
      return {
        ...graph,
        [module.filePath]: {
          code: module.code,
          deps: module.deps
        }
      }
    }, {})

    console.log(depsGraph);

    this.generate(depsGraph)

  }

  // Start building
  build(filePath) {
    // 1. Parse the file into an AST
    const ast = getAst(filePath);
    // 2. Get all the dependencies in the AST
    const deps = getDeps(ast, filePath);
    // 3. Parse the AST into code
    const code = getCode(ast);

    return {
      // File path
      filePath,
      // All dependencies of the current file
      deps,
      // The code after the current file is parsed
      code
    }
  }

  // Generate output resources
  generate(depsGraph) {

    /* index.js code "use strict"; \n' + '\n' + 'var _add = _interopRequireDefault(require("./add.js")); \n' + '\n' + 'var _count = _interopRequireDefault(require("./count.js")); \n' + '\n' + 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' + '\n' + 'console.log((0, _add["default"])(1, 2)); \n' + 'console.log((0, _count["default"])(3, 1)); * /

    const bundle = '(function (depsGraph) {// require Function localRequire(relativePath) {function localRequire(relativePath) {function localRequire(relativePath) { Return require(depsGraph[module]. Deps [relativePath]); Var exports = {}; var exports = {}; (function (require, exports, code) { eval(code); })(localRequire, exports, depsGraph[module].code); Return exports; // return exports; // return exports; } // require('The ${this.options.entry}'); }) (The ${JSON.stringify(depsGraph)})
    `
      // Generate the absolute path to the output file
    const filePath = path.resolve(this.options.output.path, this.options.output.filename)
      // Write to the file
    fs.writeFileSync(filePath, bundle, 'utf-8'); }}module.exports = Compiler;
Copy the code

Reference data: www.bilibili.com/video/BV1cv… Thanks to Silicon Valley for the video explanation!!