1 webpack plug-in

1.1 Construction Process

Webpack Loader is responsible for translating different types of files into modules that WebPack can receive. Webpack plug-in is very different from Loader. Webpack plug-in runs through the whole construction process, and different hook functions are triggered at each stage of the construction process. What WebPack plug-in needs to do is to do some processing in different hook functions.

Webpack a complete package build process is as follows.

  • Initialization parameters: willcliCommand line parameters andwebpackConfiguration files are merged and parsed to obtain parameter objects
  • Loading plug-in: parameter object is passedwebpackInitial to producecompilerObject to execute plug-in instantiation statements in the configuration file (for examplenew HtmlWebpackPlugin()), forwebpackEvent flow hang customhooks
  • Start compiling: ExecutecompilerThe object’srunMethod starts compiling every timerunAll compilations produce onecompilationobject
  • Determine entry: triggercompilerThe object’smakeMethod to start parsing the entry file
  • Compile module: from the entry file, callloaderTranslate the module, and then find the module that the module depends on and translate, and recursively complete the translation of all modules
  • Complete compilation: Assemble one by one according to the dependencies between entry and modulechunk, the implementation ofcompilationthesealMethod for eachchunkTo organize, optimize and encapsulate
  • Output resources: ExecutecompilertheemitAssetsMethod to output the generated file tooutputA directory of

1.2 Custom Plug-ins

The WebPack plug-in features the following.

  • independentjsModule, exposing the corresponding function
  • Function prototypeapplyMethod will injectcompilerobject
  • compilerThe corresponding object is mounted on thewebpackhook
  • The event hook callback gets the compiledcompilationObject, and if it’s an asynchronous hook it can get the corresponding hookcallbackfunction
class CustomDlugins {
  constructor() {}
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "CustomDlugins".(compilation, callback) = >{}); }}module.exports = CustomDlugins;
Copy the code

Most user-facing plug-ins are first registered with Compiler. Here are some common hooks exposed in Compiler.

hook type role
run AsyncSeriesHook Executes before the compiler starts reading records
compiler SyncHook In a newcompilationExecution before Creation
compilation SyncHook In acompilationExecute the plug-in after it is created
make AsyncSeriesHook Execute before completing a compilation
emit AsyncSeriesHook In the generated byoutputDirectory before executing, callback parameterscompilation
afterEmit AsyncSeriesHook In generating the file tooutputExecute after directory
assetEmitted AsyncSeriesHook Executed when a file is generated, providing access to output file information, callback parametersfile,info
done AsyncSeriesHook After a compilation is complete, the callback parameter is executedstats

Custom file list plug-in, package automatically generated file list, record file list, file number. The root directory contains package.json, webpack.config.js, and SRC, and SRC contains main.js.

// package.json{..."scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "4.29.4"."webpack-cli": "3.2.3"}}// webpack.config.js
module.exports = {
  entry: "./src/main.js".output: {
    filename: "./[name].js"
  },
  plugins: []}// src/main.js
console.log("hello world")
Copy the code

Then continue to create the plugins folder in the root directory, create a new filelistplugin.js file, and import the plug-in in webpack.config.js.

Note that this scenario takes place before the file is generated into the dist directory, so you register the EMIT hook on the Compiler. Emit is an asynchronous serial hook registered with tapAsync.

The emit callback retrieves the compilation object, and all files to be generated are stored in its assets property. Obtains a list of files from compilation.assets and writes them to a new file for output.

Finally, add a new file to compilation.assets.

// plugins/FileListPlugin.js
class FileListPlugin {
  constructor(options) {
    this.filename =
      options && options.filename ? options.filename : "FILELIST.md"
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync("FileListPlugin".(compilation, callback) = > {
      const keys = Object.keys(compilation.assets)

      const length = keys.length

      var content = ` #${length} file${
        length > 1 ? "s" : ""
      } emitted by webpack\n\n`

      keys.forEach((key) = > {
        content += ` -${key}\n`
      })

      compilation.assets[this.filename] = {
        source: function () {
          return content
        },
        size: function () {
          return content.length
        }
      }

      callback()
    })
  }
}

module.exports = FileListPlugin

// webpack.config.js
const FileListPlugin = require("./plugins/FileListPlugin")

module.exports = {
  ...
  plugins: [
    new FileListPlugin({
      filename: "filelist.md"}})]Copy the code

2 development optimization

2.1 webpack plug-in

2.1.1 webpack – dashboard

Webpack-dashboard is a tool for optimizing Webpack logs.

The root directories are webpack.config.js, package.json, and SRC, and SRC includes main.js.

// package.json{..."scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "vue": "^ 2.6.12." "."webpack": "4.29.4"."webpack-cli": "3.2.3"."webpack-dashboard": "^ 2.0.0." "}}// webpack.config.js
const DashboardPlugin = require("webpack-dashboard/plugin");

module.exports = {
  entry: "./src/main.js".output: {
    filename: "./[name].js",},plugins: [
      new DashboardPlugin()
  ],
  mode: "development"
}

// src/main.js
import vue from "vue";
console.log(vue);
Copy the code

To make Webpack-Dashboard work, you need to modify the original startup commands.

// package.json{..."scripts": {
        "build": "webpack-dashboard -- webpack"}}Copy the code

After running the build command, the console will print the following contents: Log in the upper left corner is the Log of Webpack itself, Modules in the lower left corner is the module that participated in the package, you can view the occupied volume and proportion of the module, and Problems in the lower right corner is the warning and error of the build process.

2.1.2 speed – measure – webpack – the plugin

Speed-measure-webpack-plugin (SMP) can analyze the time spent on various loaders and plugins in the whole packaging process of Webpack. Based on the analysis results, it can find out which construction steps are time-consuming for optimization and repeated testing.

SMP is used by wrapping its wrap method around the webPack configuration object.

// webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  entry: "./src/main.js".output: {
    filename: "./[name].js"
  },
  module: {
    rules: [{test: /\.js$/,
        loader: "babel-loader".exclude: /node_modules/,
        options: {
          cacheDirectory: true.presets: [["@babel/preset-env", { modules: false}]]}}]}})// src/main.js
const fn = () = > {
  console.log("hello world");
};
fn();

// package.json{..."scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "4.29.4"."webpack-cli": "3.2.3"."@babel/core": "^ 7.2.2." "."@babel/preset-env": "^ 7.3.1"."babel-loader": "^ 8.0.5"."speed-measure-webpack-plugin": "^ 1.2.2." "}}Copy the code

The console output after running the build script package is as follows, showing that the babel-Loader translation takes 1.16 seconds.

2.1.3 webpack – merge

Webpack-merge is used for projects that require multiple packaging environments to be configured.

If the project includes the local environment and production environment, the configuration of each environment is different, but there are some common parts, you need to extract the common parts.

Json, SRC, and build. SRC contains index.html, main.js, Build includes webpack.base.conf.js, webpack.dev.conf.js, and webpack.prod.conf.js.

// package.json{..."scripts": {
    "dev": "webpack-dev-server --config=./build/webpack.dev.conf.js"."build": "webpack --config=./build/webpack.prod.conf.js"
  },
  "devDependencies": {
    "webpack": "4.29.4"."webpack-cli": "3.2.3"."webpack-dev-server": "3.1.14"."webpack-merge": "^ 4.1.4." "."file-loader": "^ 1.1.6." "."css-loader": "^ 0.28.7"."style-loader": "^ 0.19.0"."html-webpack-plugin": "3.2.0"}}// src/main.js
console.log("hello world");

// src/index.html
<html lang="zh-CN">
  <body>
    <p>hello world</p>
  </body>
</html>
Copy the code

The common configurations of the development environment and production environment are as follows.

// build/webpack.base.conf.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js".module: {
    rules: [{test: /\.(png|jpg|gif)$/,
        use: "file-loader"
      },
      {
        test: /\.css$/,
        use: ["style-loader"."css-loader"]]}},plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"}})]Copy the code

The configuration of the development environment is as follows. In the process of merging module.rules, webpack-merge uses the test attribute as the identifier. When the same item is found, the later rule overwrites the previous rule, so there is no need to add redundant code.

Loaders in the following development environments include file-loader, CSS-loader, and babel-loader. Css-loader and babel-loader overwrite previous loaders and enable sourceMap.

// build/webpack.dev.conf.js
const baseConfig = require("./webpack.base.conf.js");
const merge = require("webpack-merge");

module.exports = merge.smart(baseConfig, {
  output: {
    filename: "./[name].js",},module: {
    rules: [{test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader".options: {
              sourceMap: true}}]}]},devServer: {
    port: 3000,},mode: "development"
})
Copy the code

The production environment configuration is as follows.

// build/webpack.prod.conf.js
const baseConfig = require("./webpack.base.conf.js");
const merge = require("webpack-merge");

module.exports = merge.smart(baseConfig, {
  output: {
    filename: "./[name].[chunkhash:8].js",},mode: "production"
})
Copy the code

2.2 Hot Replacement of modules

Live reload means that whenever the code changes, it is rebuilt and a page refresh is triggered. Webpack takes this one step further and allows you to get the latest code change, known as Hot Module Replacement (HMR), without refreshing the web page.

2.2.1 configuration

The following configuration binds each module with a module.hot object, which contains the HMR API (for example, HMR can be turned on or off for specific modules).

// webpack.config.js
const webpack = require("webpack")

module.exports = {
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true}}Copy the code

After configuration, you also need to manually invoke the API on module.hot to enable HMR. If main.js is the entry to your application, you can put code that calls the HMR API in the entry, and then main.js and all its dependent modules will have HMR enabled. When module changes are detected, HMR causes the application to re-execute main.js in the current environment, but the page itself is not refreshed.

// main.js.if (module.hot){
    module.hot.accept()
}
Copy the code

If the application logic is complex, the HMR of WebPack is not recommended because unexpected problems may occur during HMR triggering. Developers are advised to use third-party HMR solutions, such as vue-loader and react-hot-loader.

2.2.2 open HMR

The root directories are webpack.config.js, package.json and SRC, and SRC includes main.js, index.html and utils.js.

// webpack.config.js
const webpack = require("webpack");

module.exports = {
  entry: "./src/main.js".output: {
    filename: "./[name].js",},plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: "./src/index.html"})].devServer: {
    hot: true}}// package.json{..."scripts": {
    "dev": "webpack-dev-server"
  },
  "devDependencies": {
    "webpack": "4.29.4"."webpack-cli": "3.2.3"."webpack-dev-server": "3.1.14"."html-webpack-plugin": "3.2.0"}}// src/main.js
import { logToHtml } from "./utils.js";

var count = 0;
setInterval(() = > {
  count += 1;
  logToHtml(count);
}, 1000);

// src/utils.js
export function logToHtml(count) {
  document.body.innerHTML = `count: ${count}`;
}

// src/index.html
<html lang="zh-CN">
  <body></body>
</html>
Copy the code

After running the dev script command, the console output is as follows. Click http://localhost:8080/ to open the HTML.

HTML output integer and incremented by 1 per second, modify utils.js as follows, save and view HTML, page refresh, previously counted count starts again from 0 incremented by 1 per second (not partially refreshed).

// src/utils.js
export function logToHtml(count) {
  document.body.innerHTML = `count update: ${count}`;
}
Copy the code

Utils. Js restore, main.js add the following code to enable HMR.

// src/main.js.if (module.hot) {
  module.hot.accept()
}
Copy the code

Then modify utils.js again to see that the HTML is not refreshed, but partially updated, and the count value is also increased by 1.

However, there is another problem. The current HTML already has a setInterval, but after HMR, a new setInterval will be added, and the previous setInterval will not be cleared, resulting in different numbers flashing in the final HTML.

To avoid this problem, refresh the entire page when main.js changes, preventing multiple timers, but keep HMR on for other modules.

// src/main.js.if (module.hot) {
  module.hot.decline();
  module.hot.accept(["./utils.js"]);
}
Copy the code

2.2.3 HMR process

When the project runs the dev script for the first time, it starts with a build package and infuses the bundle with code for how to update the module and whether to update the module once received.

Bundles are written to memory, but not to disk because accessing code in memory is faster than accessing files in disk and reduces the performance overhead of writing code to files.

Next, Webpack-dev-server uses Express to start the local service, allowing the browser to request local resources. The WebSocket service is then started to establish two-way communication between the browser and the local service.

Click http://localhost:8081/ to open the page in the browser. At this point, the page establishes a Websocket connection with the local service, and the local service returns the hash value that was just packaged for the first time.

After obtaining the hash, the page uses it as the hash of the js and JSON on the server for the next request.

Modify the page code, Webpack listens for file changes, and starts packing and compiling again.

According to the newly generated file name, the hash value generated last time is used as the identifier of the newly generated file. By analogy, the hash value output this time will be used as the identification of the next hot replacement.

After compiling, the local service sends the packaged hash to the page through the Websocket.

Json and [hash].hot-update.js, followed by an Ajax request to retrieve a JSON file containing all the modules to be updated. The latest module code is then retrieved via a jSONP request again.

In the returned content of the JSON file, H represents the hash value generated this time, which is used as the prefix of the resource for the next file hot replacement request, and C indicates that the file to be hot replaced corresponds to the main module.

The returned content of the js file is the code for this modification.

After receiving the request data, the page will compare the new and old modules to decide whether to update the module. Note that if an error occurs during the hot update, the hot update will revert to Live Reload, which is a browser refresh to get the latest packaging code.

3 Packing Tools

3.1 a RollUp

RollUp is also a JavaScript module wrapper that is more focused on JavaScript packaging and less versatile than WebPack. But RollUp has always been able to pack smaller and faster packages than other packaging tools. RollUp has algorithmic advantage support for code tree shaking and ES6 modules. So we usually use Webpack for application development and RollUp for library development. Unlike webPack’s typical internal project installation, RollUp can be installed globally directly.

npm i rollup -g
Copy the code

The root directory contains package.json, rollup.config.js, and SRC. SRC is main.js. In rollup.config.js, output.format is the module form of outputting resources, which is not available in Webpack. The following uses CJS (CommonJs), in addition to amd, ES (ES Module), UMD, IIFE (self-executing function), System (SystemJs loader format).

// package.json{..."scripts": {
    "build": "rollup -c rollup.config.js"}}// rollup.config.js
module.exports = {  
    input: "src/main.js".output: {    
        file: "dist/bundle.js".format: "cjs"}}// src/main.js
console.log("hello world")
Copy the code

Run the build script and output bundle.js in root dist. You can obviously see that the bundle is very clean, RollUp adds no extra code, whereas the webPack package adds a lot of extra code to the same source code.

// dist/bundle.js
'use strict';

console.log("hello world");
Copy the code

Additionally, the Tree Shaking feature was originally implemented by RollUp, based on static analysis of ES6 modules to find modules that are not referenced, and then exclude them from the generated bundles.

3.2 Parcel

Parcel is a relative newcomer to JavaScript packaging, and in tests on its website, it builds several times faster than WebPack and comes with zero configuration out of the box.

Parcel optimizes packaging speed in three main ways, including using workers to perform tasks in parallel, file system caching, and resource compilation process optimization.

For example, Webpack can use multi-core to compress multiple resources at the same time during resource compression. Babel-loader will cache the compilation result to the project hidden directory, and determine whether to use the cache compiled last time by the modification time and state of the file.

Webpack uses loader to handle different types of resources. Loader is essentially a function whose input and output are strings. For example, babel-loader, enter ES6+ and output ES5 after syntax conversion. The general process is to parse the ES6 string content into an AST (abstract syntax tree), syntactic transform the AST, generate ES5 code, and return a string.

If multiple loaders are added after babel-loader, the general process is as follows. A large number of String and AST conversions are involved. Loaders do not affect each other and each performs its own duties. Although there may be some redundancy, it is beneficial to maintain the independence and maintainability of Loader.

Resource Input ↓ loader1 (String-> AST) --> Syntax conversion --> (AST ->String)
                                                 ↓
loader2   (AST -> String<-- syntax conversion <-- (String -> AST)
                 ↓
loader3   (String-> AST) --> Syntax conversion --> (AST ->String) ↓ Resource outputCopy the code

Parcel, on the other hand, doesn’t explicitly expose the concept of a loader. Unlike WebPack, which can combine loaders arbitrarily, Parcel doesn’t require much AST/String conversion.

For each step, the AST that was parsed and converted in the previous step will be used directly in the next step, and only in the last step will the AST be converted back to String. For a large project, parsing the AST is time-consuming, and optimizing this will save a lot of time.

Resource input ↓ process1 (String-> AST) --> Syntax conversion ↓ (AST returned by PROCESS1) process2 Syntax conversion ↓ (AST returned by process2) Process3 Syntax conversion --> (AST ->String) ↓ Resource outputCopy the code

A Parcel can also be installed globally.

npm i -g parcel-bundler
Copy the code

The root directory contains package.json and SRC, and SRC contains index.js and index.html. A Parcel can use an HTML file as the entry point to your project, and then go further to find dependent resources.

Parcel doesn’t have its own configuration file, but essentially splits the configuration and leaves it to specific tools like Babel and PostCss to manage separately. For example. Babelrc, Parcel uses it as a configuration for ES6 code parsing when packaged.

// package.json{..."scripts": {
    "dev": "parcel ./src/index.html"."build": "parcel build ./src/index.html"}}// src/index.html
<html lang="zh-CN">
  <body>
    <p>hello world</p>
    <script src="./index.js"></script>
  </body>
</html>

// src/index.js
console.log("hello world");
Copy the code

In the previous