So, although I have consulted a lot of blog columns and books in advance, many of the tutorials are several years ago, and many of the plug-ins or loaders have been upgraded and iterated and are no longer used in the past, so the new use methods need to be manually queried one by one. If there is any mistake, please advise. Gratitude!

I thought you should have known this before


1. What is Webpack, and what is its current position on the front end

Needless to say, there is no front-end project today that doesn’t use packaging tools, and webPack is the king of packaging tools, so there is no reason not to understand it.

2. Commonjs and ES6 Module should not be understood

Let’s talk about it briefly

Commonjs is used in Node, require is used for imports and module.exports is used for exports

The ES6 Module uses import and export

  • Name export
  • By default, export default is exported, and a variable named default is exported. Therefore, you do not need to declare variables like named export, but directly export the value. There can only be one file

Note that when importing a module, CommonJS gets a copy of the exported value; In the ES6 Module, it is a dynamic mapping of values, and the mapping is read-only.

There’s also some AMD and UMD module stuff, so let’s stop there. After all, this is paving the way for WebPack

3. Relationship between entry&Chunk & Bundle

I think this is pretty straightforward


4 Core concepts in WebPack

entry

Used to specify the address (relative address) of the webpack package, such as:

Single entryentry:'./src/index.js'
Or:entry:{
    main:'./src/index.js'
}  Multiple entryEntry: { main:'./src/index.js'. other:'./src/other.js' } Copy the code

output

Specifies the address and file name of the output file after packaging. The file address is absolute

A single fileoutput:{
    filename:'bundle.js'.    path:path.join(__dirname,'dist')
}
 Multiple filesoutput:{  filename:'[name].js'. path:path.join(__dirname,'dist') } Copy the code

mode

Specifies the current build environment

There are three main options

  • production
  • development
  • none

Setting mode automatically triggers some of webpack’s built-in functions, default to None if not written

loaders

Webpack recognizes.json,.js, modules by default, and other modules that we need loaders to help us put into the dependency graph

It is essentially a function that takes the source file as an argument and returns the converted result

plugins

Plugins can do what we need to do when WebPack is running at a certain stage (WebPack uses tapable to construct a lot of life cycles so that we can use plug-ins to do the right thing for us at the right time). For example, clean-webpack-plugin will delete the original output file under DIST before we pack.


One: basic use

1.1 Processing HTML, CSS, JS

usewebpack-dev-server

We also want our packaged files to be launched on a local server, and Webpack-dev-server is one such thing.

Install: NPM I webpack-dev-server -d

A new configuration of webpack.config.jsdevServerattribute

As follows:

devServer: {
  port: 3000.  contentBase: './dist'  // The address of the service (i.e. the location of our output file)
  open: true              // Automatically open the browser
Compress:true         / / gzip compression
} Copy the code

Add one to package.json to simplify the commanddevThe command

You can also create a package command, but I prefer NPX

{
    "name": "webpack-test02".    "version": "1.0.0".    "description": "".    "main": "index.js". "scripts": {  "build":"webpack". "dev": "webpack-dev-server". },  } Copy the code

usehtml-webpack-plugin

As you may have noticed above, there’s not a single HTML file. It doesn’t matter if the server is started. Then we can write an HTML template in the file (just generate the skeleton) and use the HTMl-webpack-plugin to package the template in the dist directory and import the output bundl.js

Install: NPM I html-webpack-plugin-d

Use:

const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html'.// Template file address
            filename: 'index.html'.// Specify the name of the packaged file
 hash: true.// It can also generate a hash value   }),  ]. There are also the following optionsminify: { // Compress the packed HTML file  removeAttributeQuotes: true.// Delete attribute double quotes  collapseWhitespace: true // Fold an empty line into a single line  } Copy the code

With CSS

The basic processing

Two loaders need to be installed, csS-loader (for handling @import syntax in CSS) and style-loader (for inserting CSS into the head tag)

Install: NPM I CSS-loader style-loader -d

Use:

    module: {
        rules: [
            {
                test: /\.css$/.                use: ['style-loader'.'css-loader']
 },   ]  }  Insert the CSS in front of the head tag each time. To ensure that our own Settings in the template can be overridden later module: {  rules: [{  test: /\.css$/. use: [{  loader: 'style-loader'. options: {  insert: function insertAtTop(element) {  var parent = document.querySelector('head');  // eslint-disable-next-line no-underscore-dangle  var lastInsertedElement =  window._lastElementInsertedByStyleLoader;   if(! lastInsertedElement) { parent.insertBefore(element, parent.firstChild);  } else if (lastInsertedElement.nextSibling) {  parent.insertBefore(element, lastInsertedElement.nextSibling);  } else {  parent.appendChild(element);  }   // eslint-disable-next-line no-underscore-dangle  window._lastElementInsertedByStyleLoader = element;  },  }  },   'css-loader'  ]  },  ]} Copy the code
Out of the CSS

Use the mini-CSs-extract-plugin

Install: NPM I mini-CSS-extract-plugin-d

You don’t need to use style-loader anymore

Use:

const MinniCssExtractPlugin = require('mini-css-extract-plugin')

    plugins: [
        new HtmlWebpackPlugin({
            // Specify a template file
 template: './src/index.html'. // Specify the name of the output file  filename: 'index.html'. // Add hash to resolve cache   }),  new MinniCssExtractPlugin({  // Specify the name of the output file  filename: 'main.css'  }) ].  module: {  rules: [{  test: /\.css$/. use: [  MinniCssExtractPlugin.loader,// Essentially create a style tag and import the output CSS address  'css-loader'. ]  },   ]  } Copy the code
Compressing CSS and JS

Compress the CSS file output above

The plugin optimize- CSS -assets-webpack-plugin is used to compress CSS, but uglifyjs-webpack-plugin is used to compress JS

NPM I optimize- CSS-assets -webpack-plugin uglifyjs-webpack-plugin -d

Use :(note that this plugin is no longer used in plugin, it is in the optimization property)

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
    optimization: {
        minimizer: [
            new UglifyJsPlugin({
 cache: true. parallel: true. sourceMap: true  }),  new OptimizeCSSAssetsPlugin({})  ]  }, Copy the code
Adding a Vendor Prefix

You need a loader and a style tool: Postcss-loader autoprefixer

So what is postCss

It is a container for compiling plug-ins. It works by receiving source code for processing by the compiled plug-in, and then printing CSS.

Postcss-loader is the connector between PostCSS and Webpack. Postcss-loader can be used with or alone csS-loader. Notice If the postCSS-loader is used alone, it is not recommended to use the @import syntax in the CSS. Otherwise, redundant code will be generated.

PostCss also requires a separate configuration file, postcss.config.js

Also look at generating vendor prefixes using post-loader and Autoprefixer

Installation: NPM I postcss-loader autoprefixer -d

Use: webpack.config.js in the Rules of the Module

       {
                test: /\.css$/.                use: [
                    MinniCssExtractPlugin.loader,
                    'css-loader'. 'postcss-loader'. ]  }, Copy the code

Postcss. Config. Js

module.exports = {
    plugins: [
        require("autoprefixer") ({            overrideBrowserslist: ["last 2 versions"."1%" >]
        })
 ] }; Copy the code
With less

Less less loader must be installed because less is used in the less-loader

Installation: NPM I less-loader less-d

Use:

 {
                test: /\.less$/.                use: [
                    MinniCssExtractPlugin.loader,
                    'css-loader'. 'postcss-loader'. 'less-loader'  ]  } Copy the code

Sass similarly

Deal with js

Turn es6 es5

Install: NPM I babel-loader @babel/ core@babel /preset-env -d

Use:

{
                test: /\.js$/.                use: {
                    loader: 'babel-loader'.                    options: {
 presets: [  '@babel/preset-env' ].  }  }  }, Copy the code
Es7 to class syntax

NPM I @babel/plugin-proposal-class-properties -d

Use:

{
                test: /\.js$/.                use: {
                    loader: 'babel-loader'.                    options: {
 presets: [  '@babel/preset-env' ]. plugins: [ ["@babel/plugin-proposal-class-properties", { "loose" : true }]  ]   }  }  }, Copy the code

More details are available on Babel’s website

1.2 Image Processing

There are three ways to cite a picture

  • Js is introduced to create image tags
  • CSS is introduced into the url
  • Img is introduced in HTML

Using the file – loader

The third case is not available, need to use the extra loader to see below

Install: NPM I file-loader -d

The essence of the reference is that it returns the address of an image, but that image is already in the bundle

Use: js

import './index.css'
import img from '.. /public/img/img01.jpg'
const image = new Image()
image.src = img
document.body.appendChild(image)
Copy the code

CSS

div{
    p{
        width: 100px;
        height: 100px;
        transform: rotate(45deg);
 background-image: url('./img01.jpg');  } } Copy the code

Webpack. Config. Js

module.export={
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/. use: 'file-loader'  }  ]  } } Copy the code

Use url – loader

As an upgraded version of file-loader, we can set the size of an image to base64, or use file-loader to package the original image

Install: NPM I url-loader -d

In the meantime, you can use htML-withimg-loader to process images in HTML, with the esModule property set to false. Otherwise the link is not the same as the address of the image in HTML

Install: NPM I html-withimg-loader -d

Use:

{
                test: /\.html$/.                use: 'html-withimg-loader'
            }, {
                test: /\.(png|jpg|gif)$/. use: {  loader: 'file-loader'. options: {  limit: 50 * 1024. loader: 'file-loader'. esModule: false. outputPath: '/img/'.// Package and output the address  // publicPath: "// add the domain name path to the resource  }  }  }, Copy the code

1.3 eslint

1.4 Common plug-ins

cleanWebpackPlugin

Automatically deletes files in the output directory each time you pack

Install: NPM I clean-webpack-plugin -d

Use:

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
      new CleanWebpackPlugin(),
    
]
Copy the code

copyWebpackPlugin

You can have some files that are not dependent on, but need to output inverted dist

Install: NPM I copy-webpack-plugin -d

const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
            patterns: [{
 from: path.join(__dirname, 'public'),  to: 'dist' }]. }), ] Copy the code

bannerPlugin

Add copyright to code header

A built-in plug-in for Webpack

Use:

const webpack = require('webpack')
plugins:[
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
            patterns: [{
 from: path.join(__dirname, 'public'),  to: 'dist' }]. }),  new webpack.BannerPlugin('core by gxb'), ] Copy the code

Two: purchase price usage

2.1 Multi-page packaging

That is, multiple entries, each corresponding to a generation dependency tree

Configuration is very simple, with the entrance. The entry format is a chunkNmae: path

entry: {
        index: './src/index.js'.        other: './src/other.js'
    },
 output: {
 filename: '[name].js'. path: path.join(__dirname, './dist')  },  Copy the code

2.2 devtool :'source-map'

The mapping between the source code and the packaged code is the source-map that can be used to locate the source code block when problems occur in the code

It is primarily used in development environments

Configuration is as follows

devtool: 'source-map'
Copy the code

There are many types of devtool attribute values, but the most common ones are summarized below

  • Source-map generates a map. Map file, which also locates rows and columns
  • Cheap -module-source-map does not generate. Map files, which can be located to rows
  • Eval-source-map does not generate. Map files that locate rows and columns

Note that CSS, LESS, AND SCSS need to be configured in the Loader option to locate the source code

Please note that this code has not been tested.

test:\/.css$\,
use: [    'style-loader'.    {
        loader:'css-loader'. options: { sourceMap:true  }  } ] Copy the code

2.3 watch

Watch listener can also be configured in Webpack for constant packaging, that is, we do not need to input commands after modifying the code every time, we can directly save the code by C + S key.

The configuration is as follows:

module.exports = {
    watch: true.    watchOptions: {
      poll: 1000.// How many times per second
      aggregateTimeout: 500.// How many milliseconds will it take to trigger again to prevent the repeat button
 ignored: /node_modules/ // Ignore constant listening  } } Copy the code

2.4 resolve

We all know that webPack starts looking for all dependencies from the entry file, but when looking for third-party packages it always defaults to the main.js file as the entry file, but when looking for bootstrap we sometimes just need to reference it. Wouldn’t it be a bit wasteful to pack it all up

This is where Resole can specify how WebPack will find files for module objects

The configuration is as follows:

resolve:{
    modules:[path.join('node_modules')].// Specify the directory to find
    mainFields: ['style'.'main'].// Use main instead of style
    extensions: ['.js'.'.css'.'.json']// If the file does not have a suffix, then look at the js file first, then look at the CSS...
    alias:{
 components: './src/components/'// import Button from 'components/ Button './ SRC /components/ Button  } } Copy the code

👉Refer to this article for other options

2.5 Environment Splitting

Development and online environments typically require different configurations, so you can use Webpack-Merge to split configuration files into a base common, a development, and an online configuration file.

We will be able to specify the configuration files to be used for development packaging or production packaging.

Install: NPM I webpack-merge -d

It is written as follows:

basis

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    entry: './src/index.js'.  plugins: [  new HtmlWebpackPlugin({  template: './src/index.html'. filename: 'index.html'  }),  new CleanWebpackPlugin(),  ]. output: {  filename: 'bundle.js'. path: path.join(__dirname, './dist')  },  } Copy the code

Development: webpack. Dev. Js

const merge = require('webpack-merge')
const common = require('./webpack.config')

module.exports = merge(base, {
  mode: 'development'. devServer: {},  devtool: 'source-map' }) Copy the code

Online: webpack. Prod. Js

 const merge = require('webpack-merge');
 const common = require('./webpack.config.js');

 module.exports = merge(common, {
     mode: 'production'. }); Copy the code

2.6 Handling Cross-domains

My previous article summed up the idea of cross-domain, that is, using proxies to handle cross-domain: same-origin policies exist only between browsers, not between servers. So we can first send the data to a proxy server

Chestnut: Send requests like a different domain server

const xhr = new XMLHttpRequest();
xhr.open('get'.'/api/user'.true);
xhr.send();

xhr.onload = function () {
 console.log(xhr.response) } Copy the code

Server code:

const express = require('express')
const app = express()
app.get('/test', (req, res) => {
    res.json({ msg: 11 })
})
app.listen(3001.function() {  console.log('Start service');  }) Copy the code

Webpack.config.js configures the agent

    devServer: {
        port: 3000.        proxy: {
            '/api': {
                target: 'http://localhost:3001'. pathRewrite: { '^/api': ' ' }  }  },  progress: true. contentBase: './dist'. open: true  }, Copy the code

Three: optimization

3.1 noparse

When we reference some third-party packages, we don’t need to go into those packages to look for dependencies, because they are generally independent.

So there is no need to waste time parsing

The configuration is as follows:

module: {
    noParse: /jquery/.// Don't resolve some package dependencies
        rules:[]
}
Copy the code

3.2 include&exclude

We can also specify the area to look for

Such as:

rules: [
  {
    test: /\.js$/.    exclude: '/node_modules/'./ / node_modules ruled out
    include: path.resolve('src'),  // Look in the SRC file
 use: {  loader: 'babel-loader'. options: {  presets: [  '@babel/preset-env'. ]  }  }  } Copy the code

Use absolute paths instead of exclude

3.3 lgnorePluginThe plug-in

Some third-party libraries have a lot of things we don’t need, like libraries with language packs. Generally, we only need to use the Chinese package in it, and we don’t need any other language packages.

So this is a plug-in that can be made available in Webpack

const webpack = require('webpack')

plugins: [
    new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
Copy the code

3.4 Multi-Threaded Packaging

Use the happypack plugin

Install NPM I happypack

Use:

const Happypack = require('happypack')

module: {    rules: [{
            test: /\.css$/. use: 'happypack/loader? id=css'  }] } plugins:[  new Happypack({  id: 'css'. use: ['style-loader'.'css-loader']  }) ]  Copy the code

3.5 Lazy Loading (On-demand Loading)

Something that we don’t import into our dependency tree right away, we import after we use it (kind of load on demand)

Here we need @babel/plugin-syntax-dynamic-import to parse and recognize the import() dynamic import syntax

Install: NPM i@babel /plugin-syntax-dynamic-import -d

Chestnut:

Index. In js:

const button = document.createElement('button')
button.innerHTML = 'button'
button.addEventListener('click', () = > {    console.log('click')
    import ('./source.js').then(data= > {
 console.log(data.default)   })  }) document.body.appendChild(button) Copy the code

source.js

export default 'gxb'
Copy the code

webpack.config.js

{
  test: /\.js$/.  include: path.resolve('src'),
  use: [{
    loader: 'babel-loader'. options: {  presets: [  '@babel/preset-env'.]. plugins: [  '@babel/plugin-syntax-dynamic-import'  ]  }  }] } Copy the code

Source.js modules add dependencies only when we click the button, otherwise they won’t be packaged immediately

3.6 hot update

Code updates are places where the page only updates that update rather than re-rendering the entire page, i.e. re-refreshing the page

Hot update plug-ins also come with Webpack

The configuration is as follows:

devServer: {
        hot: true.        port: 3000.        contentBase: './dist'.        open: true
 },  plugins: [ new webpack.HotModuleReplacementPlugin()  ]  Copy the code

3.7 DllPlugin Dynamic link library

When we use Vue or React, we have to type the files again every time we pack them. But this third-party package doesn’t change at all when we compile it.

So what if we typed them out the first time we packed them, and then linked them into our file

These pre-packaged files are called DLLS and can be understood as a cache

How do you write a cache? The idea might go something like this

  • Put things away first
  • Get a mapping table, and then you need to look up things in the mapping table, and then you use them directly

It’s a bit of a hassle to configure, but there’s good news. DLL has been scrapped

But since I wrote the title, LET’s do it for understanding

Use the plugin 👉 autodlL-webpack-plugin directly

Install NPM I autoDLL-webpack-plugin

Use:

const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin'); 

module.exports = {
  plugins: [
 // Configure the file to be packaged as a DLL  new AutoDllPlugin({  inject: true.// Set this to true i.e. DLL bundles are inserted into index.html  filename: '[name].dll.js'. context: path.resolve(__dirname, '.. / '), // The AutoDllPlugin context must be the same directory as package.json, otherwise the link will fail  entry: {  react: [  'react'. 'react-dom'  ]  }  })  ] } Copy the code

The HardSourceWebpackPlugin can be used after the Dll, which is faster and simpler than the Dll

Install: NPM I hard-source-webpack-plugin

use

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin() 
 ] } Copy the code

3.8 Extracting common code

Especially in multi-entry files such as index.js that references A.js and other.js that references A.js, it is normal for a.js to be printed twice. B: well.. That’s not necessary

Extract:

module.exports = {
  optimization: {
    splitChunks: {             // Split code blocks for multiple entries
      cacheGroups: {           / / cache group
        common: {              // Public module
 minSize: 0.// The value is greater than 0k  minChunks: 2.// How many times have you been quoted  chunks: 'initial' // Where should I start, from the entrance  }  }  }  }, } Copy the code

Extract some third-party packages like jquery that are commonly referenced multiple times

optimization: {
  splitChunks: {              // Split code blocks for multiple entries
    cacheGroups: {            / / cache group
      common: {               // Public module
        minSize: 0.// More than how much to pull away
 minChunks: 2.// How many more times to use the extract extract  chunks: 'initial' // Where do we start, at the beginning  },  vendor: {  priority: 1.// Add weight, (first remove third party)  test: /node_modules/.// Select * from this directory  minSize: 0.// More than how much to pull away  minChunks: 2.// How many more times to use the extract extract  chunks: 'initial' // Where do we start, at the beginning  }  }  }, }, Copy the code

3.8 webpackSome built-in optimizations

Automatically understand, there is nothing in it


Four:tapableHandwriting knows

Tapable is a library similar to eventEmitter (NodeJS). Its main function is to control the publishing and subscription of various hook functions, and control the plug-in system of WebPack

4.1 the synchronization

4.1.1 SyncHookThe usage and implementation of

One of the most featureless hooks is synchronous serial

const { SyncHook } = require('tapable')

class Lesson {
    constructor() {
        this.hooks = {
 arch: new SyncHook(['name']),  }  }  start() {  this.hooks.arch.call('gxb')  }  tap() {  this.hooks.arch.tap('node'.function(name) {  console.log('node', name)  })  this.hooks.arch.tap('react'.function(name) {  console.log('react', name)  })  } }   const l = new Lesson()  l.tap(); l.start()  / / implementation class SyncHook {  constructor() {  this.tasks = []  }  tap(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } call(... arg) { this.tasks.forEach(item= > { item.cb(... arg) })  } } const syncHook = new SyncHook() syncHook.tap('node', name => {  console.log('node', name) }) syncHook.tap('vue', name => {  console.log('vue', name) }) syncHook.call('gxb') Copy the code

4.1.2 SyncBailHookThe usage and implementation of

/ * ** The subscribed handler has a return value other than undefined to stop running down* /
const { SyncBailHook } = require('tapable')

class Lesson {  constructor() {  this.hooks = {  arch: new SyncBailHook(['name'])  }  }  tap() {  this.hooks.arch.tap('node', (name) => {  console.log('node', name);  return 'error'  })  this.hooks.arch.tap('vue', (name) => {  console.log('vue', name);  return undefined  })  }  start() {  this.hooks.arch.call('gxb')  }   } const l = new Lesson() l.tap() l.start()  / * ** Method implementation* / class SyncBailHook {  // It is generally possible to take an array parameter, but it is not used below  constructor(args) {  this.tasks = []  }  tap(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } call(... arg) { let ret  let index = 0  do {  ret = this.tasks[index++].cb(... arg) } while (ret === undefined && index < this.tasks.length);  } }  const syncBailHook = new SyncBailHook() syncBailHook.tap('node', name => {  console.log('node', name);  return 'error' }) syncBailHook.tap('vue', name => {  console.log('vue', name);  }) syncBailHook.call('gxb') Copy the code

4.1.3 SyncWaterfallHookThe usage and implementation of

/ * ** The return value of the previous handler is the input of the next one* /
/ * ** The subscribed handler has a return value other than undefined to stop running down* / const { SyncWaterfallHook } = require('tapable')  class Lesson {  constructor() {  this.hooks = {  arch: new SyncWaterfallHook(['name'])  }  }  tap() {  this.hooks.arch.tap('node', (name) => {  console.log('node', name);  return 'node ok'  })  this.hooks.arch.tap('vue', (data) => {  console.log('vue', data);   })  }  start() {  this.hooks.arch.call('gxb')  }   } const l = new Lesson() l.tap() l.start()  / * ** Method implementation* / class SyncWaterfallHook {  // It is generally possible to take an array parameter, but it is not used below  constructor(args) {  this.tasks = []  }  tap(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } call(... arg) { let [first, ...others] = this.tasks  letret = first.cb(... arg) others.reduce((pre, next) = > {  return next.cb(pre)  }, ret)  } }  const syncWaterfallHook = new SyncWaterfallHook() syncWaterfallHook.tap('node', data => {  console.log('node', data);  return 'error' }) syncWaterfallHook.tap('vue', data => {  console.log('vue', data);  }) syncBailHook.call('gxb') Copy the code

4.1.4 SyncLoopHookThe usage and implementation of

/ * ** If a subscribed handler has a return value that is not undefined, it will loop through it* /
const { SyncLoopHook } = require('tapable')

class Lesson {  constructor() {  this.index = 0  this.hooks = {  arch: new SyncLoopHook(['name'])  }  }  tap() {  this.hooks.arch.tap('node', (name) => {  console.log('node', name);  return ++this.index === 3 ? undefined : this.index  })  this.hooks.arch.tap('vue', (name) => {  console.log('vue', name);  return undefined  })  }  start() {  this.hooks.arch.call('gxb')  }   } const l = new Lesson() l.tap() l.start()  / * ** Method implementation* / class SyncLoopHook {  // It is generally possible to take an array parameter, but it is not used below  constructor(args) {  this.tasks = []  }  tap(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } call(... arg) { this.tasks.forEach(item= > {  let ret;  do { ret = item.cb(... arg) } while(ret ! = =undefined);  })  } }  const syncLoopHook = new SyncLoopHook() let index = 0 syncLoopHook.tap('node', name => {  console.log('node', name);  return ++index === 3 ? undefined : index }) syncLoopHook.tap('vue', name => {  console.log('vue', name);  }) syncLoopHook.call('gxb') Copy the code

4.2 the asynchronous

Asynchronous concurrent

2AsyncParallelHook
/ * ** Asynchronous concurrency,Serial versus concurrentThat is, concurrency requires all of the handler to execute before the last callback, whereas serial requires one handler to execute before the second callback* /
const { AsyncParallelHook } = require("tapable") class Lesson {  constructor() {  this.hooks = {  arch: new AsyncParallelHook(['name'])  }  }  tap() {   this.hooks.arch.tapAsync('node', (name, cb) => {  setTimeout((a)= > {  console.log("node", name);  cb();  }, 1000);  })   this.hooks.arch.tapAsync('vue', (name, cb) => {  // Use macro tasks  setTimeout((a)= > {  console.log('vue', name)  cb()  }, 1000)   })    }  start() {  this.hooks.arch.callAsync('gxb'.function() {  console.log('end')  })  } } let l = new Lesson();   l.tap(); l.start();   / * ** implementation* Asynchronous concurrent recall features, callAsync incoming callbacks are executed after all asynchronous tasks have completed* / class SyncParralleHook {  constructor() {  this.tasks = []  }  tapAsync(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  }  // The async handler should run done to see if it is ready for the last callback callAsync(... arg) { // Take out the callback passed by callAsync  const lastCb = arg.pop()   // Start executing other asynchronous tasks  let index = 0   // the done function is used to determine whether to execute lastCb  const done = (a)= > {  // Execute lastCb according to the number of asynchronous functions  index++  if (index === this.tasks.length) {  lastCb()  }  }  this.tasks.forEach(item= >item.cb(... arg, done)) } } const hook = new SyncParralleHook() hook.tapAsync('node', (name, cb) => {  setTimeout(function() {  console.log('node', name)  cb()  }, 1000) }) hook.tapAsync('vue', (name, cb) => {  setTimeout(function() {  console.log('vue', name)  cb()  }, 1000) }) hook.callAsync('gxb'.function() {  console.log('end')  })   / * ** The handler uses microtask promis, that is, the handler can return no cb but needs to return a promise* / const { AsyncParallelHook } = require('tapable') class Lesson {  constructor() {  this.hooks = {  arch: new AsyncParallelHook(['name'])  }  }  start() {  this.hooks.arch.promise('gxb').then(function() {  console.log('end')  })  }  tap() {  this.hooks.arch.tapPromise('node', name => {  return new Promise((resove, reject) = > {  console.log('node', name)  resove()  })  })  this.hooks.arch.tapPromise('vue', name => {  return new Promise((resove, reject) = > {  console.log('vue', name)  resove()  })  })  } }  const l = new Lesson() l.tap() l.start()   / * ** implementation * tapPromise  * promise * / class AsyncParallelHook {  constructor() {  this.tasks = []  }  tapPromise(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } promise(... arg) { // The handler part of the task returns all promise values, map them into an array. Implement all of them using promise's all  const newTasks = this.tasks.map(item= >item.cb(... arg)) return Promise.all(newTasks)  } } / / test const hook = new AsyncParallelHook() hook.tapPromise('node', name => {  return new Promise((res, rej) = > {  console.log('node', name)  res()  }) }) hook.tapPromise('vue', name => {  return new Promise((res, rej) = > {  console.log('vue', name)  res()  }) }) hook.promise('gxb').then(function() {  console.log('end') }) Copy the code
4.2.2AsyncParallelBailHook

Same with synchronization

Asynchronous serial port

holdingsAsyncSeriesHook
/ * ** Asynchronous serial* Execute one by one * 
* /
 const { AsyncSeriesHook } = require('tapable')  class Lesson {  constructor() {  this.hooks = {  arch: new AsyncSeriesHook(['name'])  }  }  start() {  this.hooks.arch.callAsync('gxb'.function() {  console.log('end')  })  }  tap() {  this.hooks.arch.tapAsync('node', (name, cb) => {  setTimeout((a)= > {  console.log('node', name)  cb()  }, 1000)  })  this.hooks.arch.tapAsync('vue', (name, cb) => {  setTimeout((a)= > {  console.log('node', name)  cb()  }, 1000)  })  } } const l = new Lesson() l.tap() l.start()   / * ** implementation* Features: one function is executed before another is executed* / class AsyncSeriesHook {  constructor() {  this.tasks = []  }  tapAsync(name, cb) {  let obj = {}  obj.name = name  obj.cb = cb  this.tasks.push(obj)  } callAsync(... arg) { const finalCb = arg.pop()  let index = 0  let next = (a)= > {  if (this.tasks.length === index) return finalCb()   let task = this.tasks[index++].cb task(... arg, next) }  next()  } }  const hook = new AsyncSeriesHook() hook.tapAsync('node', (name, cb) => {  setTimeout((a)= > {  console.log('node', name)  cb()  }, 1000) }) hook.tapAsync('vue', (name, cb) => {  setTimeout((a)= > {  console.log('vue', name)  cb()  }, 1000) }) hook.callAsync('gxb'.function() {  console.log('end') }) Copy the code
4.2.4 AsyncSeriesBailHook

Refer to the synchronous

4.2.5 AsyncSeriesWaterfallHook

Refer to the synchronous


Five: Hand write a simple Webpack

5.1 Building an import File, that is, link to the local PC

To initialize a new project, NPM init-y first generates package.json

Write a bin command

   "bin": {
        "mypack": "./bin/index.js"
    },
Copy the code

./bin/index.js as our entry file

Then run the NPM link, link the NPM module to the corresponding running project, and debug and test the module conveniently

Create a project with the webpack.config.js file (call it the project file to package first — > source project). Run the ‘NPM link mypack’ command

This is to run the command NPX mypack in the project to be packaged, using the one we wrote by hand

5.2 Core construction and compiler class compilation

Go back to the handwritten Webpack project to initialize the Compiler class

Nothing in the entry file

You simply need to get the webpack.config.js file in the source project, and then call the Compiler method to pass in the configuration file’s address

#! /usr/bin/env node

const path = require('path')

// Get the configuration file
const config = require(path.resolve('webpack.config.js')) const Compiler = require('.. /lib/compiler.js')  const compiler = new Compiler(config) compiler.run() Copy the code

The basic skeleton of the Compiler class

const path = require('path')
const fs = require('fs')
const tapable = require('tapable')
class Compiler {
    constructor(config) {
 this.config = config  this.entryId = ' '  this.modules = {}  this.entry = config.entry  this.root = process.cwd() // Get the current project address  this.asserts = {} // Store chunkName and output code block  this.hooks = {  entryInit: new tapable.SyncHook(),  beforeCompile: new tapable.SyncHook(),  afterCompile: new tapable.SyncHook(),  afterPlugins: new tapable.SyncHook(),  afteremit: new tapable.SyncHook(),  }   const plugins = this.config.plugins  if (Array.isArray(plugins)) {  plugins.forEach(item= > {  // Each is an instance, just call a method on the instance and pass in the current Compiler instance  item.run(this)  })  }  }   // Building blocks  buildMoudle(modulePath, isEntry) {}  // Write the output position  emitFile() {}  run() {  this.hooks.entryInit.call()  this.buildMoudle(path.resolve(this.root, this.entry), true)  this.hooks.afterCompile.call()  this.emitFile()  } } Copy the code

Building template

The first pass in run is an entry address, and a Boolean that indicates whether the entry is valid. What we need to do is first assign the relative address of the entry to this.entryid

Then get the source code of the file and make it in the form of key-value pairs {file relative address: modified source code}

    // Building blocks
    buildMoudle(modulePath, isEntry) {
        let source = this.getSource(modulePath)

        // This. Entry
 let moduleName = '/' + path.relative(this.root, modulePath)  if (isEntry) {  this.entryId = moduleName  }   // start to modify the source code, mainly for require  let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) / / it is. / SRC   // Put the modification into the module  this.modules[moduleName] = sourceCode   // Recursively construct dependent templates  dependencies.forEach(item= > {  this.buildMoudle(path.resolve(this.root, item), false)  })   } Copy the code
 // Get the source code
    getSource(modulePath) {
        // Match the path with the matching rule in module
     
        let content = fs.readFileSync(modulePath, 'utf8')
  return content  } Copy the code

Transform the source code

There’s a couple of tools here

  • babylonTurn the source code into an AST abstract syntax tree
  • @babel/traverseUsed to replace nodes on the AST
  • @babel/generatorThe results generated
  • @babel/typesAST node’s Lodash-esque utility library

Install: NPM I Babylon @babel/traverse @babel/types @babel/generator

Change require to __webpack_require__, and modify the internal path parameters of require. Webpack is essentially a manual implementation of require

Note: this time only rewrite require, but still does not support ES6 module

// Change the source code
    parse(source, parentPath) {
        // Generate the AST syntax tree
        let ast = babylon.parse(source)

 // For access dependencies  let dependencies = []  traverse(ast, {  CallExpression(p) {  let node = p.node  // Rename the require method  if (node.callee.name === 'require') {  node.callee.name = '__webpack_require__'  let moduledName = node.arguments[0].value // get the path to the require method   // change the suffix  moduledName = moduledName + (path.extname(moduledName) ? ' ' : '.js')   // add the parent path./ SRC  moduledName = '/' + path.join(parentPath, moduledName)   // Add the dependency array  dependencies.push(moduledName)   // Replace the source code  node.arguments = [type.stringLiteral(moduledName)]  }  }  })   let sourceCode = generator(ast).code  return { sourceCode, dependencies }  } Copy the code

Output to the specified position of output

// Write to the output position
    emitFile() {

        // Get the output address
        let outPath = path.join(this.config.output.path, this.config.output.filename)
  / / template  let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))   // Populate the template data  let code = ejs.render(templateStr, {  entryId: this.entryId,  modules: this.modules  })  this.asserts[outPath] = code   console.log(code);    / / write  fs.writeFileSync(outPath, this.asserts[outPath])  }  Copy the code
(function (modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; return module.exports; } // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "<%-entryId %>"); })({ <% for(let key in modules){ %> "<%- key %>": (function (module, exports,__webpack_require__) { eval(`<%-modules[key] %>`); }), <%} %>}); </div>Copy the code

joinloader

< CSS > < CSS > < CSS > < CSS > < CSS > < CSS

Less-loader: Borrows less files and converts them to CSS files

const less = require('less')

function loader(source) {
    let css = ' '
    less.render(source, function(err, output) {
 css = output.css  })   css = css.replace(/\n/g.'\\n')  return css } module.exports = loader Copy the code

Style-loader: Create a label and put CSS in it

function loader(source) {
    let style = `
    let style = document.createElement('style')
    style.innerHTML = The ${JSON.stringify(source)}
 document.head.appendChild(style)  `  return style } module.exports = loader Copy the code

At this point the webpack. Config. Js

module.exports = {
    mode: 'development'.    entry: './src/index.js'.    output: {
        filename: 'main.js'. path: path.join(__dirname, './dist')  },  module: {  rules: [{  test: /\.less$/. use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]  }]  },  plugins: [  new TestPlugins(),  new InitPlugin()  ] } Copy the code

At this point, the source file acquisition function is transformed as follows

 // Get the source code
    getSource(modulePath) {
        // Match the path with the matching rule in module
        const rules = this.config.module.rules
        let content = fs.readFileSync(modulePath, 'utf8')
  for (let i = 0; i < rules.length; i++) {  let { test, use } = rules[i]  let len = use.length   // Match to start loader, features from back to front  if (test.test(modulePath)) {  console.log(111);   function normalLoader() {  // Take the last one first  let loader = require(use[--len])  content = loader(content)   if (len > 0) {  normalLoader()  }  }  normalLoader()  }  }  return content  } Copy the code

And add plug-ins

Write it directly in the source project webpack.config.js

const path = require('path')
class TestPlugins {
    run(compiler) {
        // Subscribe your methods to hook for use
        // Assume it runs after compilation
 compiler.hooks.afterCompile.tap('TestPlugins'.function() {  console.log(`this is TestPlugins,runtime ->afterCompile `);   })  } }  class InitPlugin {  run(compiler) {  // Put the execution period before the start parsing entry  compiler.hooks.entryInit.tap('Init'.function() {  console.log(`this is InitPlugin,runtime ->entryInit `);  })  } } module.exports = {  mode: 'development'. entry: './src/index.js'. output: {  filename: 'main.js'. path: path.join(__dirname, './dist')  },  module: {  rules: [{  test: /\.less$/. use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]  }]  },  plugins: [  new TestPlugins(),  new InitPlugin()  ] } Copy the code

In fact, handwriting logic is difficult to make clear, rather than directly look at the code, BELOW I have pasted out the whole code of the Compiler class, with the comments written in what I feel is quite detailed

Compiler class in full code

const path = require('path')
const fs = require('fs')
const { assert } = require('console')
    // Babylon is the JavaScript parser used in Babel.
    // @babel/traverse replaces, removes, and adds nodes to the AST parse traverse syntax tree
 // @babel/types Lodash-esque utility library for AST nodes  // @babel/generator Result generated  const babylon = require('babylon') const traverse = require('@babel/traverse').default; const type = require('@babel/types'); const generator = require('@babel/generator').default const ejs = require('ejs') const tapable = require('tapable')  class Compiler {  constructor(config) {  this.config = config  this.entryId = ' '  this.modules = {}  this.entry = config.entry  this.root = process.cwd() // The current project address  this.asserts = {} // Store chunkName and output code block  this.hooks = {  entryInit: new tapable.SyncHook(),  beforeCompile: new tapable.SyncHook(),  afterCompile: new tapable.SyncHook(),  afterPlugins: new tapable.SyncHook(),  afteremit: new tapable.SyncHook(),  }   const plugins = this.config.plugins  if (Array.isArray(plugins)) {  plugins.forEach(item= > {  // Each is an instance, just call a method on the instance and pass in the current Compiler instance  item.run(this)  })  }  }   // Get the source code  getSource(modulePath) {  // Match the path with the matching rule in module  const rules = this.config.module.rules  let content = fs.readFileSync(modulePath, 'utf8')   for (let i = 0; i < rules.length; i++) {  let { test, use } = rules[i]  let len = use.length   // Match to start loader, features from back to front  if (test.test(modulePath)) {  console.log(111);   function normalLoader() {  // Take the last one first  let loader = require(use[--len])  content = loader(content)   if (len > 0) {  normalLoader()  }  }  normalLoader()  }  }  return content  }   // Change the source code  parse(source, parentPath) {  // ast syntax tree  let ast = babylon.parse(source)   // For access dependencies  let dependencies = []  traverse(ast, {  CallExpression(p) {  let node = p.node  // Rename the require method  if (node.callee.name === 'require') {  node.callee.name = '__webpack_require__'  let moduledName = node.arguments[0].value // get the path to the require method   // change the suffix  moduledName = moduledName + (path.extname(moduledName) ? ' ' : '.js')   // add the parent path./ SRC  moduledName = '/' + path.join(parentPath, moduledName)   // Add the dependency array  dependencies.push(moduledName)   // Replace the source code  node.arguments = [type.stringLiteral(moduledName)]  }  }  })   let sourceCode = generator(ast).code  return { sourceCode, dependencies }  }   // Building blocks  buildMoudle(modulePath, isEntry) {  let source = this.getSource(modulePath)   // This. Entry  let moduleName = '/' + path.relative(this.root, modulePath)  if (isEntry) {  this.entryId = moduleName  }   // start to modify the source code, mainly for require  let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) / / it is. / SRC   // Put the modification into the module  this.modules[moduleName] = sourceCode   // Recursively construct dependent templates  dependencies.forEach(item= > {  this.buildMoudle(path.resolve(this.root, item), false)  })   }   // Write the output position  emitFile() {   // Get the output address  let outPath = path.join(this.config.output.path, this.config.output.filename)   / / template  let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))   // Populate the template data  let code = ejs.render(templateStr, {  entryId: this.entryId,  modules: this.modules  })  this.asserts[outPath] = code   console.log(code);    / / write  fs.writeFileSync(outPath, this.asserts[outPath])  }   run() {  this.hooks.entryInit.call()  this.buildMoudle(path.resolve(this.root, this.entry), true)  this.hooks.afterCompile.call()  this.emitFile()  }  }  module.exports = Compiler  / * ** At this point a simple Webpack is complete* / Copy the code

At the end of the day, webPack is officially in the door. Save the following for a later summary. I won’t upload the brain map

Reference acknowledgments:

👉The second Tapable in the Webpack series

👉It’s easy to understand devtool in Webpack with two examples: what does ‘source-map’ mean

👉This is the official Tapable Chinese document

👉NPM Link usage summary

👉How to develop webPack Loader

👉How to play the webpack

👉Fix webpack4

👉Webpack: Getting started, advancing, and tuning

This article is formatted using 👉 MDnice