I always thought that my Webpack was the level of copy and paste, and my knowledge of Webpack was really vague, even pure small white. So some time ago, I began to study Webpack systematically.

After finishing the study, I took time to organize the notes, and it took me more than a week. Finally, I think I can share it, so that my friends who are still vague about Webpack can learn it.

Of course, by the end of this article, you’ll see that there is much more to Webpack and more to learn, so this is just the beginning, starting from scratch.

Module, Chunk, and bundle

Before we learn webpack, we need to understand three terms — Module, chunk, and bundle.

Go through the concept

module

Let’s take a look at webPack’s official interpretation of Module:

Modules are discrete chunks of functionality that provide a smaller interface than a complete program. Well-written modules provide solid boundaries of abstraction and encapsulation that allow each module in an application to have a coherent design and a clear purpose.

In fact, a module module is a code file we write, such as JavaScript file, CSS file, Image file, Font file, etc., they are all belong to the Module module. One of the things about a module is that it can be used.

chunk

Again, look at the official interpretation:

This WebPack-specific term is used internally to manage the bundle process. An output bundle consists of blocks with several types (such as Entry and Child). Typically, blocks correspond directly to output bundles, but some configurations do not produce a one-to-one relationship

In fact, chunk is an intermediate product of The WebPack packaging process. Webpack will generate chunk according to the import relationship of the file. That is to say, a chunk is composed of one module or more modules, depending on whether other modules are introduced.

Bundle

First, the official interpretation:

The bundle is generated by a number of different modules and contains the final version of the source file that has been loaded and compiled.

A bundle is the end product of a Webpack. Typically, a bundle corresponds to a chunk.

conclusion

Module, chunk, and bundle are different names for the same code in different scenarios:

  • What we wrote wasmodule
  • webpackDeal with all the timechunk
  • The final generation for use isbundle

Practice a

Let’s go through a small demo. Now we have a project with the following path:

SRC / ├ ─ ─ index. CSS ├ ─ ─ index. The js ├ ─ ─ common. Js └ ─ ─ utils. JsCopy the code

Then we have two entry files, index.js and utils.js, in which we introduce index. CSS and common.js. Then I pack index.bundle. CSS, index.bundle.js and utils.bundle.js through WebPack.

Well, with that background, we can look at Modules, chunks, and bundles.

First, the code we write is module, that is, index. CSS, common.js, index.js and utils.

Second, we have two entry files, index.js and utils.js, and they are packaged separately into bundles, thus forming two chunk files during the WebPack packaging process. The chunk formed by index.js also contains the modules introduced by index.js — common.js and index.css.

Finally, we pack the bundle files index.bundle. CSS, index.bundle.js, and uitls.bundle.js.

Finally, we can summarize the relationship among the three: A budnle corresponds to a chunk, and a chunk corresponds to one or more modules.

Initialize the Webpack project

Next, we’ll learn webPack step by step, using WebPack 5 in this article.

First, create a new project folder and initialize the project.

yarn init -y
Copy the code

Then install WebPack. When we use WebPack, we also need to install Webpack-CLI.

Since WebPack is only used in the development environment, we only need to add it to devDependencies.

#Webpack -> 5.47.0, webpack-cli-> 4.7.2
yarn add webpack webpack-cli -D
Copy the code

Then create a new SRC path in your project and create an index.js:

console.log("Hello OUDUIDUI");
Copy the code

Then NPX Webpack is executed, and webPack packaging is performed. Your project will now have an additional dist folder, and in the dist folder you will see a main.js file with the same code as index.js.

Of course, we can edit the script command in package.json:

"scripts": {
  "dev": "webpack"
}
Copy the code

Then run yarn dev. The package can also be successfully packaged.

Webpack configuration file

For those of you who have used Webpack, webpack actually has a configuration file — webpack.config.js.

But why did we successfully package without editing the configuration file during the initial test? This is because WebPack has a default configuration, and when it detects that we have no configuration file, it defaults to using its own default configuration.

const path = require('path');

module.exports = {
  entry: './src/index.js'.output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',}};Copy the code

First, let’s take a quick look at these default configurations.

Entry and output

The entry option is used to configure the entry file, which can be a string, array, or object type. By default, WebPack only supports JS and JSON files as entry files, so other types of files will be saved if imported.

The output option sets the output configuration. This option must be an object type and cannot be in any other type format. In the output object, the two required options are the export path path and the export bundle name filename. Path must be an absolute path.

The configuration of entry and output varies according to different application scenarios.

One entry, one output

Our most common approach is to have a single entry file and package it into a single bundle file. In this case, entry can be in the form of a string, similar to the default configuration file:

entry: './src/index.js'
Copy the code

Multiple entry and single output

When our project needs multiple entry files, but only one output bundle, we can use an array of entries:

entry: ['./src/index_1.js'.'./src/index_2.js']
Copy the code

Note: There is only one chunk

Multiple entries, multiple outputs

When we have multiple entry files for a project, and they need to be packaged separately, which means multiple bundles will be exported, we need to use an object form for our entry and the name of the chunk corresponding to the object key.

entry: {
  index: "./src/index.js"./ / chunkName as index
  main: "./src/main.js"     / / chunkName for main
}
Copy the code

In this case, our output.filename cannot be written dead, and webpack provides us with a placeholder [name], which is automatically replaced by the corresponding chunkName.

output: {
   path: path.resolve(__dirname, 'dist'),
   filename: '[name].js'  // The [name] placeholder is automatically replaced with chunkName
},
Copy the code

Based on the configuration above, the index.js and main.js will be packaged.

supplement

In the single-entry single-output scenario, entry can also be in the form of an object, thereby defining chunkName, and output.filename can also be automatically matched using the [name] placeholder. You can also use arrays, but not necessarily.

When entry uses an array or string, chunkName defaults to main, so if output.filename uses [name] as a placeholder, it is automatically replaced with main.

mode

In the previous packaging tests, the command line will send a warning:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
Copy the code

This is because WebPack requires us to configure the mode option.

Wepack gives us three options, namely None, Development, and Production, with the default being Production.

The difference is that WebPack comes with its own code compression and optimization plug-ins.

  • None: Do not use any default optimization options.

  • Development: NamedModulesPlugin and NamedChunksPlugin, which give names to modules and chunks respectively, default to an array, and the corresponding chunkName is just a subscript, which is not good for debugging.

  • Production: for a production environment, a plug-in for code compression and code performance optimization is turned on, resulting in a much smaller package than None and Development.

When we set mode, we can get the current environment in process.env.node_env

So we can configure mode on the configuration file:

const path = require('path');

module.exports = {
    entry: './src/index.js'.output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',},/ / open source - the map
    devtool: "source-map"
};
Copy the code

Webpack also gives us another way to configure it on the command line by adding –mode:

// package.json
"scripts": {
  "dev": "webpack --mode development"."build": "webpack --mode production"
}
Copy the code

devtool

After talking about mode, when it comes to development debugging, sourceMap is not difficult to think of. We can use devTool to open it in the configuration file.

const path = require('path');

module.exports = {
    entry: './src/index.js'.output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',},/ / open source - the map
    devtool: "source-map"
};
Copy the code

Once packaged, you will have a main.js.map file in your dist.

Of course, the official not only provides such an option, specific can go to the official website to see, here say a few other more commonly used options.

  • None: sourceMap is not generated;

  • Eval: Each module executes with eval(), not recommended in build environments;

  • Cheap-source-map: sourceMap is generated, but there is no column map. Therefore, the sourceMap is only reminded of the line in the code, but not the column.

  • Inline-source-map: sourceMap is generated, but instead of a map file, sourceMap is placed in a package file.

module

As mentioned earlier, the webPack entry file can only receive JavaScript and JSON files.

However, we often have other types of files such as HTML, CSS, images, fonts and so on, and we need to use a third party Loader to help WebPack parse these files. In theory, you can process any type of file as long as you have the appropriate loader.

There are many loaders available on webpack website, which can meet our daily needs. Of course, we can also go to Github to find others’ written loaders or use our own handwritten loaders.

For loader configuration, it is written in the Module option. The module option is an object that has a rules property, which is an array in which we can configure multiple matching rules.

The matching rule is an object with a test attribute and a use attribute. The test attribute is usually a regular expression that identifies the file type, and the use attribute is an array of loaders that are used for that file type.

module: {
    rules: [{test: /\.css$/.// Identify the CSS file
          use: ['style-loader'.'css-loader']  // Three loaders used for CSS files}}]Copy the code

The order of the use array is required, and WebPack executes the loader from back to forward. That is, the above example webpack executes csS-loader first and then style-loader.

Second, when we need to provide configuration for the corresponding loader, we can choose to write the object:

module: {
    rules: [{test: /\.css$/,  
          use: [
            'style-loader', 
            {
              	/ / the name of the loader
              	loader: 'css-loader'./ / loader option
              	options: {... }}}}Copy the code

We will talk about the use of Modules in actual application scenarios later.

plugins

Webpack also provides a plugins option that allows us to use third-party plug-ins, so we can use third-party plug-ins for packaging optimization, resource management, injection of environment variables, and more.

Likewise, webPack officially provides a number of plugins.

The plugins option is an array where you can put multiple plugins.

plugins: [
  new htmlWebpackPlugin(),
  new CleanWebpackPlugin(),
  new miniCssExtractPlugin(),
  new TxtWebpackPlugin()
]
Copy the code

For plugins arrays, there is no requirement for the sorting position, because in the plugin implementation, WebPack hooks through the packaging process lifecycle, so the plug-in logic already sets which tasks need to be performed in which lifecycle.

Implement the common application scenarios

HTML template

When we were a Web project, we had to have HTML files to implement the pages.

For other types of files, such as CSS, images, files, etc., we can import the entry JS file, and then parse and package through the Loader. For AN HTML file, you can’t just import it into the entry file and parse and package it. Instead, you need to import the bundle into the HTML file and use it.

Therefore, there are only two operations we need to implement: one is to copy an HTML file to the packaging path, and the other is to automatically import the packaged bundle file into the HTML file.

At this point we need to use a plug-in to implement these functions — html-webpack-plugin.

# 5.3.2
yarn add html-webpack-plugin -D
Copy the code

After installing the plugin, create a new index. HTML file under the SRC file.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Demo</title>
</head>
<body>
    <div>Hello World</div>
</body>
</html>
Copy the code

We don’t need to introduce any modules into this yet.

Next, configure WebPack. Plugins are usually a class, and we need to create an instance of the plug-in in the plugins option.

For the htmlWebpackPlugin plugin, we need to pass in some configuration: the HTML template address template and the packaged filename filename.

const path = require('path');
/ / introduce htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js'.output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',},plugins: [
      	// Use htmlWebpackPlugin
        new htmlWebpackPlugin({
         	 // Specify the HTML template
            template: './src/index.html'.// Customizes the file name of the package
            filename: 'index.html'}})];Copy the code

Next, perform the packaging and you’ll see that an index. HTML is generated under the dist file. Webpack will automatically import the bundle file:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Demo</title>
<script defer src="main.js"></script></head>
<body>
    <div>Hello World</div>
</body>
</html>
Copy the code

If we have multiple chunks, we can specify which chunks to introduce into the HTML. The htmlWebpackPlugin configuration has a chunks option, which is an array. You just need to add the chunkName that you want to import.

const path = require('path');
/ / introduce htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: {
      	index: './src/index.js'.main: './src/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',},plugins: [
        new htmlWebpackPlugin({
            template: './src/index.html'.filename: 'index.html'.chunks: ["index"]  // Only index chunk is imported}})];Copy the code

After the packaging is complete, index.html, index.js, and main.js will appear under the dist file, but index.html will only introduce index.js.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
<script defer src="index.js"></script></head>
<body>The HelloWorld!</body>
</html>
Copy the code

If we want to implement multiple pages, all we need to do is create a new htmlWebpackPlugin instance.

Clearing the packing path

Before each package, we actually need to empty the files in the package path.

Webpack also overwrites files if they have the same name, but in practice we always add hashes to the package names, so the cleanup has to be done.

At this point we need to use a plug-in, the clean-webpack-plugin.

yarn add clean-webpack-plugin -D
Copy the code

Then simply import it into the configuration file and configure it in your plugins to use it.

const path = require('path');
/ / introduce CleanWebpackPlugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    entry: './src/index.js'.output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js'.publicPath: ' '
    },
    plugins: [
      	/ / use CleanWebpackPlugin
        new CleanWebpackPlugin(),
    ]
};
Copy the code

In some cases, we don’t need to completely empty package path, this time we can use to an option, call cleanOnceBeforeBuildPatterns, it is an array, the default is / / * * *, is cleaning up the output. The path path of all things. You can type in files you just want to delete, and we can type in files we don’t want to delete, just add one in front of it! .

It is important to note that cleanOnceBeforeBuildPatterns this option is pack can delete the file path, only need you with absolute path. Therefore, The CleanWebpackPlugin also provides an option for testing — dry. The default is false. When you set this to true, it will not actually delete the file, but will only print the deleted file on the command line, which is better to avoid accidental deletion during testing.

const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    entry: './src/index.js'.output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: '[name].js'.publicPath: ' '
    },
    plugins: [
        new CleanWebpackPlugin({
          	// Dry: true // Open testable, will not actually perform the delete action
            cleanOnceBeforeBuildPatterns: [
                '* * / *'.// Delete all files in the dist path
                `! package.json`.// Do not delete the dist/package.json file],}),]};Copy the code

Webpack local services

When we use WebPack, it’s not just for packaging files. Most of us also rely on WebPack to build local services and take advantage of its hot update capabilities, allowing us to develop and debug code better.

Let’s install webpack-dev-server:

#Version for 3.11.2
yarn add webpack-dev-server -D
Copy the code

Then execute the following code to start the service:

npx webpack serve
Copy the code

Or in package.json:

"scripts": {
  "serve": "webpack serve --mode development"
}
Copy the code

Then run it through YARN serve.

At this point, WebPack will turn on the http://localhost:8080/ service by default (see the code you run back), and the service will point to dist/index.html.

But you’ll notice that you don’t actually have any files in your dist, because WebPack keeps all the files that are compiled in real time in memory.

Webpack – dev – the benefits of the server

Webpack comes with the –watch command, which dynamically monitors changes in files and packages them in real time, and outputs new packages.

There are several disadvantages to this approach. One is that webPack repackages the entire file every time you change the code, making each update much slower. Second, it doesn’t do hot updates, where every time you change your code and webPack is recompiled and packaged, you have to manually refresh the browser to see the latest page results.

Webpack-dev-server, on the other hand, effectively fixes both of these problems. Its implementation uses Express to start an HTTP server to serve resource files. The HTTP server and client then use the WebSocket communication protocol. When changes are made to the original file, the Webpack-dev-server compiles it in real time and renders the final compiled file to the page in real time.

Webpack dev – server configuration

In webpack.config.js, there is a devServer option that is used to configure webpack-dev-server. Here are a few common configurations.

port

We can use port to set the server port number.

module.exports = {
  
    ...
  
    / / configuration webpack - dev - server
    devServer: {
        port: 8888.// Customize the port number}};Copy the code

open

There is an open option in devServer, which defaults to false. When set to true, it will automatically open the browser for you every time you run webpack-dev-server.

module.exports = {
  
    ...
  
    / / configuration webpack - dev - server
    devServer: {
        open: true.// Open the browser window automatically}};Copy the code

proxy

This option is used to set up a locally developed cross-domain broker. I won’t talk about cross-domain here, but how to configure it.

The value of proxy must be an object from which we can configure one or more cross-domain proxies. The simplest way to write the configuration is to add the API address to the address.

module.exports = {
  
    ...
  
    devServer: {
      	// Cross-domain proxy
        proxy: {
          '/api': 'http://localhost:3000',}}};Copy the code

At that time, when you request/API/users will agent to http://localhost:3000/api/users.

If you don’t need to pass the/API, you need to use the object notation to add some configuration options:

module.exports = {
    / /...
    devServer: {
      	// Cross-domain proxy
        proxy: {
            '/api': {
              target: 'http://localhost:3000'.// Proxy address
              pathRewrite: { '^/api': ' ' },   // Override the path}},}};Copy the code

At that time, when you request/API/users will agent to http://localhost:3000/users.

One is changeOrigin. By default, the proxy keeps the source of the host header. This behavior can be overwritten when set to true. There is also the Secure option, which needs to be set to false if your interface uses HTTPS.

module.exports = {
    / /...
    devServer: {
      	// Cross-domain proxy
        proxy: {
            '/api': {
              target: 'http://localhost:3000'.// Proxy address
              pathRewrite: { '^/api': ' ' },   // Override the path
              secure: false./ / using HTTPS
              changeOrigin: true   // Overwrite the host source}},}};Copy the code

CSS

Next, I’ll talk about how WebPack parses CSS.

Parsing a CSS file

As you can see in the previous example, the loaders we need to parse CSS are csS-Loader and style-loader. Css-loader is used to parse CSS files, while style-Loader is used to render CSS to the DOM node.

First let’s install:

 #CSS - loader - > 6.2.0; Style - loader - > 3.2.1
 yarn add css-loader style-loader -D
Copy the code

And then we’ll create a new CSS file.

/* style.css */
body {
  background: # 222;
  color: #fff;
}
Copy the code

Then introduce the following in index.js:

import "./style.css";
Copy the code

Next we configure WebPack:

module.exports = {
   ...
  
  module: {
    rules: [{test: /\.css$/.// Identify the CSS file
        use: ['style-loader'.'css-loader']  // Use csS-loader first, then style-loader}}],... };Copy the code

At this point, if we package it, we can see that there is only main.js and index.html in the dist path. But open index. HTML and you’ll see that the CSS is in effect.

This is because the style-loader inserts the CSS code into main.js.

Packaging CSS files

If we don’t want to put CSS code in JS and want to export a CSS file directly, we have to use another plugin, mini-css-extract-plugin.

# 2.1.0
yarn add mini-css-extract-plugin -D
Copy the code

It is then imported into the configuration file and introduced in the plugins.

const miniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
  
    plugins: [
      	// Use miniCssExtractPlugin
        new miniCssExtractPlugin({
          	filename: "[name].css"  // Set the name of the EXPORTED CSS, [name] placeholder for chunkName}})];Copy the code

Next, we need to change the loader. Instead of using style-loader, we use the loader provided by miniCssExtractPlugin.

const miniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
  
    module: {
        rules: [{test: /\.css$/./ / use miniCssExtractPlugin. Replace style - loader loader
                use: [miniCssExtractPlugin.loader,'css-loader']]}},plugins: [
        new miniCssExtractPlugin({
          	filename: "[name].css"}})];Copy the code

Next, if you package it, there will be an extra main.css file in the dist path, and it will be automatically introduced in index.html.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
<script defer src="main.js"></script><link href="main.css" rel="stylesheet"></head>
<body>The HelloWorld!</body>
</html>
Copy the code

Add the browser prefix to the CSS

When we use new CSS features, we may need to consider browser compatibility issues, and we may need to add browser prefixes to some CSS properties. This kind of work, in fact, can be handed over to WebPack. To be precise, it’s postCSS.

Postcss is to CSS what Babel is to JavaScript: it focuses on transforming CSS, such as adding prefix compatibility, compressing CSS code, and so on.

First we need to install postCSS and post-CSS-Loader.

#Postcss -> 8.3.6, postcsS-Loader -> 6.1.1
yarn add postcss postcss-loader -D
Copy the code

Next, we introduce postcsS-Loader into the WebPack configuration file first, which is executed in order before the CSS-Loader.

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

The next step in configuring PostCSS is not in the WebPack configuration file. Postcss has its own configuration file. We need to create a new postCSS,config.js, in the project root path. Then there is also a configuration item, called plugins.

module.exports = {
    plugins: []}Copy the code

This also means that PostCSS itself supports many third-party plug-ins.

For the prefixes we want to implement now, we need to install a plug-in called AutopreFixer.

# 1.22.10
yarn add autoprefixer -D
Copy the code

Then we just need to introduce it into the postCSS configuration file, and it will have a configuration option called overrideBrowserslist, which is used to fill in the version of the applicable browser.

module.exports = {
    plugins: [
        // Compile the CSS to accommodate multiple browsers
        require('autoprefixer') ({// Override the browser version
          	// Last 2 versions: compatible with the latest two versions of each browser
          	// > 1%: the global browser usage exceeds 1%
            overrideBrowserslist: ['last 2 versions'.'> 1%']]}})Copy the code

So for the overrideBrowserslist option, we can look at BrowserSlist, but I won’t go into that here.

Json, or browserslist configuration file.browserslistrc. This way we can use it if we want to use other plugins, such as Babel, that are also compatible with browsers.

/ / package. Json file{..."browserslist": ['last 2 versions', '> 1%']
}

Copy the code
Browserslsetrc file last 2 Versions > 1%Copy the code

But if you configure it in multiple places, overrideBrowserslist has the highest priority.

Next, let’s modify style.css to use the newer features.

body {
    display: flex;
}
Copy the code

Then wrap it up and see what main.css looks like when it’s packaged.

body {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
}
Copy the code

Compress CSS code

When we need to compress CSS code, we can use another postCSS plug-in, CSsnano.

# 5.0.7
yarn add cssnano -D
Copy the code

Then again, in the postCSS configuration file, import:

module.exports = {
    plugins: [...require('cssnano')]}Copy the code

To wrap it up, look at main.css.

body{display:-webkit-box;display:-ms-flexbox;display:flex}
Copy the code

Parse the CSS preprocessor

In our actual development today, we use more CSS preprocessors like Sass, Less, or Stylus. HTML cannot parse such files directly, so we need to use the corresponding Loader to convert them into CSS.

Next, I’ll take sass as an example and show you how to parse sass using WebPack.

First we need to install sass and Sass-Loader.

#Sass -> 1.36.0, sass-loader -> 12.1.0
yarn add sass sass-loader -D
Copy the code

Then we add the sass matching rule to the module. The sass-loader should be executed first, and we need to convert it to CSS before we can perform the following operations.

rules: [
  ...
  
  {
    test: /\.(scss|sass)$/,
    use: [miniCssExtractPlugin.loader, 'css-loader'.'postcss-loader'.'sass-loader']}]Copy the code

Then we create a new style.scss in the project.

$color-white: #fff;
$color-black: # 222;

body {
    background: $color-black;

    div {
        color: $color-white; }}Copy the code

Then introduce it in index.js.

import "./style.css";
import "./style.scss";
Copy the code

The content of the SCSS file is parsed into it, and if we introduce multiple CSS or CSS preprocessor files, the miniCssExtractPlugin will also bundle it into a bundle file.

body{display:-webkit-box;display:-ms-flexbox;display:flex}
body{background:# 222}body div{color:#fff}
Copy the code

Other static resource processing

When we use other static resources such as images, videos, or fonts, we need url-loader and file-loader.

#Url - loader - > 4.4.1. File - loader - > 6.2.0
yarn add url-loader file-loader -D
Copy the code

First we introduce an image into the project, and then into index.js.

import pic from "./image.png";

const img = new Image();
img.src= pic;
document.querySelector('body').append(img);
Copy the code

Then I use urL-loader first.

module.exports = {
  ...
  
  module: {
    rules: [{test: /\.(png|je? pg|gif|webp)$/,
        use: ['url-loader']}]};Copy the code

Then perform the packaging.

You will notice that there is no image file in the dist directory, but you can see it when you open the page. By default, url-Loader converts static resources to Base64.

Of course, the URl-Loader option provides a property called LIMIT, which means that we can set a threshold for the size of the file. When the file size exceeds this value, url-Loader will not convert to base64 and will simply package it into a file.

module.exports = {
  ...
  
  module: {
    rules: [{test: /\.(png|je? pg|gif|webp)$/,
        use: [{
          loader: 'url-loader'.options: {
            name: '[name].[ext]'.// Use a placeholder to set the export name
            limit: 1024 * 10  // Set the base64 conversion threshold. If the value is greater than 10K, base64 is not converted}}]}]}};Copy the code

At this point, if we pack it up again, the image file will appear in the dist folder.

File-loader is similar to URl-loader, but it exports files by default and does not export base64.

module.exports = {
  ...
  
  module: {
    rules: [{test: /\.(png|je? pg|gif|webp)$/,
        use: ['file-loader']}]};Copy the code

The dist folder will still be packed as an image file, but the name will be changed to the hash value. You can use the options option to set the exported name.

module.exports = {
  ...
  
  module: {
    rules: [{test: /\.(png|je? pg|gif|webp)$/,
        use: [{
          loader: 'file-loader'.options: {
            name: '[name].[ext]'.// Use a placeholder to set the export name}}]}]}};Copy the code

For the video file and font file, the same method is used, but the test is modified.

module.exports = {
  ...
  module: {
    rules: [
      / / picture
      {
        test: /\.(png|je? pg|gif|webp)$/,
        use: {
          loader: 'url-loader'.options: {
            esModule: false.name: '[name].[ext]'.limit: 1024 * 10}}},/ / font
      {
        test: /\.(woff2? |eot|ttf|otf)(\? . *)? $/,
        use: {
          loader: 'url-loader'.options: {
            name: '[name].[ext]'.limit: 1024 * 10}}},// Media file
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\? . *)? $/,
        use: {
          loader: 'url-loader'.options: {
            name: '[name].[ext]'.limit: 1024 * 10}}}};Copy the code

However, there is a question, is it possible to package images directly into index.html?

The answer is no, and we can test that. First, import the image into index.html.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <img src="./image.png">
</body>
</html>
Copy the code

In this case, we need to use another plugin — html-withimg-Loader.

# 0.1.16
yarn add html-withimg-loader -D
Copy the code

Then we add a rule.

{ test: /\.html$/,loader: 'html-withimg-loader' }
Copy the code

At this time, after successful packaging, the dist file successfully packaged the image out, but when the page was opened, the image was still not displayed. And then if you go through the debugging tools, you’ll see

<img src="{"default":"image.png"} ">
Copy the code

This is because html-Loader uses CommonJS for parsing, while URl-Loader uses esModule for parsing by default. So we need to set the URl-loader.

{
  test: /\.(png|je? pg|gif|webp)$/,
    use: {
      loader: 'url-loader'.options: {
          esModule: false.// EsModule parsing is not applicable
          name: '[name].[ext]'.limit: 1024 * 10}}}Copy the code

At this point, repackage and the page will successfully display the image.

Webpack5 Resource module

Webpack.docschina.org/guides/asse…

In WebPack5, there is a new resource module, which allows you to use resource files (fonts, ICONS, etc.) without having to configure an extra loader.

In the previous example, we used either url-loader or file-loader for static resources, but in WebPack5, we even need to manually install and use both loaders and set a type property directly.

{
  test: /\.(png|jpe? g|gif|svg|eot|ttf|woff|woff2)$/i,
  type: "asset/resource",}Copy the code

After the test is packaged, the static files will be directly packaged into files and automatically imported, the effect is the same as the file-loader.

The type value provides four options:

  • asset/resource:Send a separate file and export the URL. By usingfile-loaderThe implementation.
  • asset/inlineExport the data URI of a resource. By usingurl-loaderThe implementation.
  • 支那asset/source : ** Export source code of the resource. By usingraw-loaderThe implementation.
  • asset:Automatically choose between exporting a data URI and sending a separate file. By usingurl-loaderAnd configure the resource volume limiting implementation.

Also, we can set the output bundle static file name in output:

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: '[name].js'.// Set the static bundle file name
  assetModuleFilename: '[name][ext]'
}
Copy the code

JavaScript escape

Not only does CSS need to be escaped, but JavaScript needs to be escaped to be compatible with multiple browsers, so we need to use Babel.

# 8.2.2
yarn add babel-loader -D
Copy the code

In the meantime, we need to use the plugins in Babel for JavaScript compatibility:

#@ Babel/preset - env - > 7.14.9; @ Babel/core - > 7.14.8; @ the core - js - > 3.16.0
yarn add @babel/preset-env @babel/core core-js -D
Copy the code

Next, we need to configure the WebPack configuration file.

{
  test: /\.js$/,
  use: ['babel-loader']}Copy the code

Then we need to configure Babel. Of course we can configure it directly in webpack.config.js, but Babel also provides the configuration file.babelrc, so we’ll do it directly here.

Create a.babelrc in the root path.

{
  "presets": [["@babel/preset-env",
      {
      	// Browser version
        "targets": {
          "edge": "17"."chrome": "67"
        },
         // Configure the corejs version, but require additional corejs installation
        "corejs": 3.// Load condition
        // entry: you need to enter @babel/polyfill in the entry file, and then Babel is loaded on demand based on usage
        // Usage: Automatically load on demand without import
        // false: import file, load all
        "useBuiltIns": "usage"}}]]Copy the code

Next, let’s test this by modifying index.js.

new Promise(resolve= > {
    resolve('HelloWorld')
}).then(res= > {
    console.log(res);
})
Copy the code

Then run yarn build to package.

Before using Babel, the packaged main.js was as follows.

!function(){"use strict";new Promise((o= >{o("HelloWorld")})).then((o= >{console.log(o)}))}();
Copy the code

The above packaging code uses the Promise directly and does not allow for compatibility with earlier browsers. Then when we open Babel and execute the packaging command, we see that there is a lot more code.

In the packaging code, you can see that WebPack uses a polyfill to implement the Promise class and then calls it, thus making it compatible with earlier browsers without the promise property problem.

Document classification

In our current test code, our SRC folder looks like this:

├ ─ ─ the SRC │ ├ ─ ─ Alata - Regular. The vera.ttf │ ├ ─ ─ image. The PNG │ ├ ─ ─ index. The HTML │ ├ ─ ─ index. The js │ ├ ─ ─ style.css. CSS │ └ ─ ─ style.css. SCSSCopy the code

For a normal project, we would use folders to sort it out, it’s not that hard, so let’s just sort it out.

├ ─ ─ the SRC │ ├ ─ ─ index. The HTML │ ├ ─ ─ js │ │ └ ─ ─ index. The js │ ├ ─ ─ the static │ │ └ ─ ─ image. The PNG │ │ └ ─ ─ Alata - Regular. The vera.ttf │ └ ─ ─ style │ ├── style. CSS │ ├─ style. SCSSCopy the code

Next, the files that need to be packaged are also classified, so it’s not too complicated here, just use an assets folder to put all the static files inside, and then put index.html outside.

├ ─ ─ dist │ ├ ─ ─ assets │ │ ├ ─ ─ Alata - Regular. The vera.ttf │ │ ├ ─ ─ image. The PNG │ │ ├ ─ ─. Main CSS │ │ └ ─ ─ the main, js │ └ ─ ─ index. The HTMLCopy the code

Here is the code for introducing the font in style.css:

@font-face {
    font-family: "test-font";
    src: url(".. /static/Alata-Regular.ttf") format('truetype')}body {
    display: flex;
    font-family: "test-font";
}
Copy the code

First, we put the packaged JavaScript file into the Assets folder. We only need to modify output.filename.

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: 'assets/[name].js'
}
Copy the code

Second, we will put the CSS file into the assets path, because we use miniCssExtractPlugin to package the CSS, so we only need to configure the filename of miniCssExtractPlugin:

plugins: [
  ...
  new miniCssExtractPlugin({
    filename: "assets/[name].css"})]Copy the code

The last is the static resources, here we use static module, so modify the output directly. AssetModuleFilename can:

output: {
  path: path.resolve(__dirname, 'dist/'),
  filename: 'assets/[name].js'.assetModuleFilename: 'assets/[name][ext]'
},
Copy the code

At this point, package, preview the page, found that the normal introduction and use.

Hash value

In general, we pack files with a hash of the file name, which gives us the benefit of avoiding caching.

Webpack also provides three hashing strategies, which we’ll look at next:

preparation

In order to better compare the differences between the three, here is the project and configuration adjustment.

// index.js
import pic from ".. /static/image.png";

const img = new Image();
img.src = pic;
document.querySelector('body').append(img);

// main.js
import ".. /style/style.scss";
import ".. /style/style.css";

console.log('Hello World')


// webpack.config.js
entry: {
  index: './src/js/index.js'.main: './src/js/main.js'
},
Copy the code

Hash strategy

The hash policy is based on the project. That is, whenever a file in a project changes, the bundle file name of that file will be changed, and the file name of all js and CSS files will also be changed.

Let’s start with an example:

First we need to add a [hash] placeholder to all places where the filename is set. We can also set the length of the hash by adding a colon and a length value, such as [hash:6].

module.exports = {
    entry: {
        index: './src/js/index.js'.main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[hash:6].js'.assetModuleFilename: 'assets/[name]-[hash:6][ext]'
    },
    module: {... },plugins: [...new miniCssExtractPlugin({
            filename: "assets/[name]-[hash:6].css"})]};Copy the code

At this point, let’s pack it and look at the package file:

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - 7503 - BC. PNG │ ├ ─ ─ the index - 7 fa71a. Js │ ├ ─ ─ the main - 7 fa71a. CSS │ └ ─ ─ The main - 7 fa71a. Js └ ─ ─ index. The HTMLCopy the code

Then I’ll just change my style.css and repack it.

The file names of index.js, main.js, and main.css will all change, but the static files will not change.

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - 7503 - BC. PNG │ ├ ─ ─ the index - 4 b2329. Js │ ├ ─ ─ the main - 4 b2329. CSS │ └ ─ ─ The main - 4 b2329. Js └ ─ ─ index. The HTMLCopy the code

Then we find another image, overwrite image.png, and repackage it.

In this case, the file names of index.js, main.js, main. CSS will still be changed, and image.png will also be changed.

├── Assets │ ├─ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises The main - 46 acaa. Js └ ─ ─ index. The HTMLCopy the code

From the above example, we can simply conclude:

  • If you modify the project files, all of themjs,cssThe filename of the package file will change, albeit from multiple sourceschunk.
  • If you modify a static file, the package file name of the static file will change, and alljs,cssThe filename of the package file will also change.

Chunkhash strategy

The chunkhash policy is based on chunks. If a file is changed, only the package file name of the chunk-related file changes.

Let’s look at it again by example:

First we will change the configuration files to Chunkhash. Note here that Chunkhash does not apply to static files, so static files still use hashes.

module.exports = {
    entry: {
        index: './src/js/index.js'.main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[chunkhash:6].js'.assetModuleFilename: 'assets/[name]-[hash:6][ext]'
    },
    module: {... },plugins: [...new miniCssExtractPlugin({
            filename: "assets/[name]-[chunkhash:6].css"})]};Copy the code

Pack one first:

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - f3f2ec. PNG │ ├ ─ ─ the index - 6 be98e. Js │ ├ ─ ─ the main - a15a74. CSS │ └ ─ ─ The main - a15a74. Js └ ─ ─ index. HTMLCopy the code

Then we first modify style.css. When we wrap it up, we can see that main.css and main.js have changed, but index.js is not a chunk, so there is no change.

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - f3f2ec. PNG │ ├ ─ ─ the index - 6 be98e. Js │ ├ ─ ─ the main - 88 f8ea. CSS │ └ ─ ─ The main - 88 f8ea. Js └ ─ ─ index. HTMLCopy the code

Again, let’s override image.png and pack it up.

Image.png will change, of course, and then index.js will change, because they are a chunk, but main.css and main.js will not change.

├── Assets │ ├─ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises │ ├─ exercises The main - 88 f8ea. Js └ ─ ─ index. HTMLCopy the code

To summarize:

  • If you modify the project file, the corresponding project filechunkthejs,cssThe file name of the package file will change.
  • If you modify a static file, the package file name of the static file will change and the corresponding static file name will be introducedchunkthejs,cssThe filename of the package file will also change.

Contenthash strategy

The last is the Contenthash policy, which is in units of its own content, so that when a file changes, first the name of its own package changes, and second, the package of the file that introduced it changes.

Practice an experiment:

We changed all the hash placeholders to Contenthash.

module.exports = {
    entry: {
        index: './src/js/index.js'.main: './src/js/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist/'),
        filename: 'assets/[name]-[contenthash:6].js'.assetModuleFilename: 'assets/[name]-[contenthash:6][ext]'
    },
    module: {... },plugins: [...new miniCssExtractPlugin({
            filename: "assets/[name]-[contenthash:6].css"})]};Copy the code

Then pack it up first.

├── Assets │ ├─ ├─ basic.html.htm │ ├─ ├─ html.html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm │ ├─ ├─ html.htm The main - c437b0. Js └ ─ ─ index. HTMLCopy the code

First of all, let’s change the image. Find a new image and overwrite image.png, and then pack it up.

First, the name of image.png must change because it has. Second, index.js will change because it introduces image.png, and the name of image.png will change, so the name of the code will change, so the index.js name will also change.

Main.js and main.css will not change because they do not reference image.png.

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - f3f2ec. PNG │ ├ ─ ─ the index - e241d6. Js │ ├ ─ ─ the main - 02 a4b4. CSS │ └ ─ ─ The main - c437b0. Js └ ─ ─ index. HTMLCopy the code

Next, let’s modify main.js and package it.

We can see that only the main.js package changes, but main.css in the same chunk does not change, because main.css does not reference main.js.

├ ─ ─ assets │ ├ ─ ─ Alata - Regular - e83420. The vera.ttf │ ├ ─ ─ image - f3f2ec. PNG │ ├ ─ ─ the index - e241d6. Js │ ├ ─ ─ the main - 02 a4b4. CSS │ └ ─ ─ The main - d1f8ed. Js └ ─ ─ index. HTMLCopy the code

Here’s a quick summary:

  • Whether you modify a project file or a static file, the file name of the package itself will change, and then the file name of the corresponding package that references the file will change, recursively upward.

Multiple packaging configurations

Usually our projects have both a development environment and a production environment.

We also saw earlier that WebPack provides a mode option, but it is unlikely that we would say that mode is set to development at development time, and then production until the package is ready. Of course, as we said earlier, we can match the mode option by using the –mode command.

However, if the differences between the development and production WebPack configurations go beyond the mode option, we may need to consider multiple packaging configurations.

Multiple WebPack profiles

Our default WebPack configuration file is called webpack.config.js and webPack will look for it by default.

However, if we do not use this file name and change it to webpack.conf.js, webPack normally uses the default configuration, so we need to use a –config option to specify the configuration file.

webpack --config webpack.conf.js
Copy the code

Therefore, we can configure a development environment configuration webpack.dev.js and a generation environment configuration webpack.prod.js, and then execute the different configuration files with instructions:

// package.json
 "scripts": {
   "dev": "webpack --config webpack.dev.js"."build": "webpack --config webpack.prod.js",}Copy the code

Single configuration file

If you don’t want to create so many configuration files, we can just use webpack.config.js to implement multiple packages.

Using the –mode option as described above, we can actually get this variable in webpack.config.js, so we can use this variable to return different configuration files.

// Argv. mode can get the mode option configured
module.exports = (env, argv) = > {
  if (argv.mode === 'development') {
    // Return the configuration options for the development environment
    return{... }}else if (argv.mode === 'production') {
    // Return to the production environment configuration options
    return{... }}};Copy the code

Optimize the Webpack configuration

Reasonable allocationmodeOptions anddevtooloptions

The mode and DevTool options have already been discussed, but each option can be packaged at different speeds, so you can configure it according to your actual needs, generate it when you need it, and save it when you don’t need it.

Narrow down the file search

Alias options

In the configuration file, there is actually a resovle.alias option, which creates import and reuquire aliases to make module imports easier and webPack can find imported files faster when packaging.

// webpack.config.js
const path = require('path');

module.exports = {
  ...
  
  resolve: {
    alias: {
      // Configure the alias of the style path
      style: path.resolve(__dirname, 'src/style/')}}};Copy the code
/ / use
import "style/style.scss";
import "style/style.css";
Copy the code

Include and exclude options

When we use loader, we can configure include to specify that only files in the path are parsed, and exclude to specify that files in the path are not parsed.

const path = require('path');

module.exports = {
  ...
  
  module: {
    rules: [{test: /\.css$/,
        use: [miniCssExtractPlugin.loader, 'css-loader'.'postcss-loader'].include: [path.resolve(__dirname, 'src')]  // Only the CSS in the SRC path is parsed
      }
      {
        test: /\.js$/,
        use: 'babel-loader'.exclude: /node_modules/   // Do not parse js in node_modules}}};Copy the code

NoParse options

We can configure only files that do not need to be parsed in the module.noParse option. Often we ignore large plug-ins to improve build performance.

module.exports = {
  ...
  module: {
    noParse: /jquery|lodash/,}};Copy the code

Use HappyPack to start the multi-process Loader

In the process of building WebPack, in fact, most of the time is consumed by Loader parsing. On the one hand, it is because of the large amount of data to convert files; on the other hand, because of the single-threaded feature of JavaScript, it needs to be processed one by one instead of concurrent operation.

We can use HappyPack to split this part of the task into multiple sub-processes for parallel processing, which then send the results to the main process, thus reducing the overall build time.

Github.com/amireh/happ…

# 5.0.1
yarn add happypack -D
Copy the code
// webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
const HappyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});

module.exports = {
  ...
  module: {
    rules: [{test: /\.js$/,
        use: [{
          loader: 'happypack/loader? id=happyBabelLoader'}}}]],plugins: [
    new HappyPack({
      id: 'happyBabelLoader'.// Id corresponding to loader
      // Use the same as the loader configuration
      loaders: [{loader: 'babel-loader'.options: {}}],threadPool: HappyThreadPool  // Share the process pool}})];Copy the code

Use the Webpack-parallel-Uglify-plugin to enhance code compression

Webpack will enable code compression plugins when mode is production. Webpack also has an optimization option that allows you to override native plugins with your favorite plugins.

Therefore, we can override the native code compression plug-in with the Webpack-parallel-Uglify-plugin, which has the advantage of being able to execute in parallel.

Github.com/gdborton/we…

# 2.0.0
yarn add webpack-parallel-uglify-plugin -D
Copy the code
// webpack.config.js
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin")

module.exports = {
  ...
  
  optimization: {
    minimizer: [
      new ParallelUglifyPlugin({
        // Cache path
        cacheDir: '.cache/'.// Compression configuration
        uglifyJS: {
          output: {
            comments: false.beautify: false
          },
          compress: {
            drop_console: true.collapse_vars: true.reduce_vars: true}}})]}};Copy the code

Configure the cache

We recompile all the files each time we execute a build, and if we can cache these repeated actions, it will help speed up the next build.

Most loaders now provide caching options, but not all loaders do, so it’s best to configure the global caching action yourself.

Before Webpack5, we all used cache-loader, and in Webpack5, there is an official cache option that gives us persistent caching.

// Development environment
module.exports = {
  cache: {
    type: 'memory'  // Default configuration}}// Production environment
module.exports = {
  cache: {
    type: 'filesystem'.buildDependencies: {
      config: [__filename]
    }
  }
}
Copy the code

Analyze the size of the package file

We can use the WebPack-Bundle-Analyzer plug-in to help us analyze the packaged file, presenting the packaged bundle of content as an interactive, intuitive tree to let us know what is really introduced in the package we are building.

Github.com/webpack-con…

# 4.4.2
yarn add webpack-bundle-analyzer -D
Copy the code
// webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');

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

After we pack, WebPack will automatically open a page that shows how much of the package we have packed. The report will give you an intuitive idea of which dependencies are large, so you can make specific changes.

If you don’t want to open the page every time you run it, you can save the data and then run a new command to see it when you need to.

// webpack.config.js
new BundleAnalyzerPlugin({
   analyzerMode: 'disabled'.generateStatsFile: true
 })
Copy the code
// package.json
"scripts": {
  "analyzer": "webpack-bundle-analyzer --port 3000 ./dist/stats.json"
},
Copy the code

A handwritten

Handwritten Loader

Webpack.js.org/contribute/…

On the webpack website, it suggests several principles for loader writing:

  • ** Single principle: ** everyloaderDo one thing only;
  • Chained calls:webpackEach will be called sequentiallyloader;
  • ** Unified principle: ** followwebpackCustom design rules and structure, input and input are both strings, eachloaderFully independent, plug and play.

Webpack also provides us with the Loader API, so we can use this to get the API we need, but because of this, our Loader implementation cannot use the arrow function.

Today, let’s just write a little bit about Sass-Loader, CSS-Loader and style-Loader, and they also have a single function:

  • sass-loader: Used to analyzesassandscssCode;
  • css-loader: Used to analyzecssCode;
  • style-loaderWill:cssCode inserted intojsIn the.

First, we create a myLoders folder and then create three Loader files.

├── myLoaders
│   ├── ou-css-loader.js
│   ├── ou-sass-loader.js
│   └── ou-style-loader.js
Copy the code

Then we need to introduce the resolveLoader option in Webpack, and we need to configure the resolveLoader option, because webpack only searches for Loaders on node_modules by default.

module.exports = {
  ...

  resolveLoader: {
    // Add the loader query path
    modules: ['node_modules'.'./myLoaders']},module: {
    rules: [{
      test: /\.(scss|sass)$/.// Use your own loader
      use: ['ou-style-loader'.'ou-css-loader'.'ou-sass-loader']}}};Copy the code

First let’s implement ou-sass-Loader.

Loader is essentially a function, and we can get the code of the corresponding file in the first argument of the function, we can print it to see.

// ou-sass-loader.js
module.exports = function(source) {
  console.log(source);
}
Copy the code

Then, after the packaging is executed, we can see the code in our SCSS file.

Therefore, we can parse SCSS code using the Sass plugin. Sass has a render function to parse SCSS code.

// ou-sass-loader.js
const sass = require('sass');

module.exports = function(source) {
  // Use the render function to parse SCSS code
  sass.render({data: source},  (err, result) = > {
    console.log(result);
  });
}
Copy the code

When we do the packaging, we find that result is an object, and the CSS inside is all we need, so we need to return it out.

Here CSS is Buffer and we need to parse it, but parsing it is a csS-Loader job, not a Sass-Loader job.

{
  css: <Buffer 62 6f 64 79 20 7b 0a 20 20 62 61 63 6b 67 72 6f 75 6e 64 3a 20 23 32 32 32 3b 0a 7d 0a 62 6f 64 79 20 64 69 76 20 7b 0a 20 20 63 6f 6c 6f 72 3a 20 23.6 more bytes>,
  map: null,
  stats: {
    entry: 'data',
    start: 1628131813793,
    end: 1628131813830,
    duration: 37,
    includedFiles: [ [Symbol($ti)]: [Rti] ]
  }
}
Copy the code

This.async, which is a function in itself, returns a callback() so that we can return the asynchronous result.

// ou-sass-loader.js
const sass = require('sass');

module.exports = function(source) {
  // Get the callback function
  const callback = this.async();
  sass.render({data: source},  (err, result) = > {
    // Return the result
    if (err) return callback(err);
    callback(null, result.css);
  });
}
Copy the code

In this case, we implement ou-sass-loader. Next, we implement ou-csS-loader.

It simply parses the CSS returned by the OU-sass-Loader into strings.

// ou-css-loader.js
module.exports = function(source) {
    return JSON.stringify(source)
}
Copy the code

Finally, there is the OU-style-Loader, which creates a style tag, inserts the data returned by the OU-csS-Loader, and places the style tag in the head tag.

// ou-style-loader.js
module.exports = function(sources) {
    return `
        const tag = document.createElement("style");
        tag.innerHTML = ${sources};
        document.head.appendChild(tag)
    `
}
Copy the code

With our simple versions of Sass-Loader, CSS-laoder and style-laoder implemented, we can execute the packaging command to verify that the page has the appropriate style effect.

Write a Plugin

Webpack.js.org/contribute/…

While WebPack is running, there is a lifecycle in which WebPack broadcasts a lot of things that can be listened for in the Plugin, so the Plugin can implement some actions at the right time using the apis provided by WebPack.

Normally, a plugin is a class that has an Apply function, which receives a compiler parameter that contains all the configuration information about the WebPack environment.

module.exports = class MyPlugin {
  apply (compiler) {}
}
Copy the code

Many lifecycle hook functions are exposed in the Compiler, which can be viewed in the documentation. We can access the hook function in the following ways.

compiler.hooks.someHook.tap(...)
Copy the code

In the tap method, you receive two arguments, the name of the plugin and the callback function, which in turn receives a Compilation parameter.

module.exports = class MyPlugin {
  apply (compiler) {
    compiler.hooks.compile.tap("MyPlugin".(compilation) = > {
      console.log(compilation)
    })
  }
}
Copy the code

The Compilation object contains the current module resources, compiled and generated resources, and changed files. When running the WebPack development environment middleware, every time a file change is detected, a new Compilation is created to generate a new set of compilation resources. The Compilation object also provides a number of time-critical callbacks that plug-ins can choose to use when doing custom processing.

Compliation also exposes a number of hooks, which you can check out in the documentation.

Next, simply implement a plugin that generates a TXT file that prints out the size of each bundle.

module.exports = class MyPlugin {
  apply(compiler) {
    // Generate resources before the output directory
    compiler.hooks.emit.tap("MyPlugin".(compilation) = > {
      let str = ' '
      for (let filename in compilation.assets) {
        // Get the name and size of the file
        str += `${filename} -> ${compilation.assets[filename]['size'(a) /1000}KB\n`
      }

      / / new fileSize. TXT
      compilation.assets['fileSize.txt'] = {
        / / content
        source: function () {
          return str
        }
      }
    })
  }
}
Copy the code

Next, we introduce it into webpack.config.js and create an instance in our plugins.

const MyPlugin = require("./myPlugins/my-plugin")

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

Then, once packaged, a filesize.txt file is generated in the dist file.

TTF -> assets/ image-F3F2ec. PNG -> 207.392KB index.html -> 0.364KB Assets /index-41f0e2.css -> 0.177KB Assets/index-Acc2F5.js -> 1.298KBCopy the code

Handwritten Webpack

Code: github.com/OUDUIDUI/mi…

If you like it, you can click Star

Initialize the

First let’s initialize our project file.

Create a SRC path and create three js files — index.js, a.js, and B.JS.

// index.js
import {msg} from "./a.js";

console.log(msg);



// a.js
import {something} from "./b.js";

export const msg = `Hello ${something}`;


// b.js
export const something = 'World';
Copy the code

Then we can install WebPack and test the features of the bundle.

So I’m not going to go into this, I’m going to go into the bundle file (default configuration, mode is development)

After packaging, we can see that the bundle file has a lot of content, but it also has a lot of comments.

In fact, we only need to look at two places, one is the __webpack_modules__ variable. We can see that it is an object, the key is the module path, and the value is the function that executes the Module code.

var __webpack_modules__ = ({
  "./src/a.js": (() = > eval( ... )),
  "./src/b.js": (() = > eval( ... )),
  "./src/index.js": (() = > eval(...). )})Copy the code

Second, we can see a function called __webpack_require__ that takes a moduleId argument. However, we can see at the end of the call that moduleId is the key of __webpack_modules__, which is the path to the module.

var __webpack_exports__ = __webpack_require__("./src/index.js");
Copy the code

At this point, we have a good idea of the logic of WebPack packaging.

  • webpackIs to get it directlyjsThe code of the file, which is a string. Then througheval()Function execution code;
  • webpackIt starts in the entry file, recurses through the incoming modules, and keeps it in an object,keyA value ofmoduleIdIs the module path, andvalueIs the code associated with the module.
  • webpackWill convert the code tocommonJS, that is, usingrequireTo introduce the module, and it will encapsulate one itselfrequireFunction to execute the entry file code.

Without further ado, let’s start writing code by hand.

First we can initialize the WebPack configuration file, webpack.config.js.

const path = require("path");

module.exports = {
  entry: './src/index.js'.output: {
    path: path.resolve(__dirname, "./dist"),
    filename: 'index.js'}}Copy the code

Next, we create a new lib folder and create a webpack.js to hand-write our mini-WebPack.

We can initialize Webpack, which is a class, and then the constructor will accept the configuration file, and then there will be a run function, which is the run function of Webpack.

module.exports = class Webpack {
  /** * constructor to get webPack configuration *@param {*} options* /
  constructor(options) {}

  /** * WebPack run function */
  run() {
    console.log('Start Webpack! ')}}Copy the code

Then we need an execution file, which is to create a debugger.js in the root path.

const webpack = require('./lib/webpack');
const options = require('./webpack.config');

new webpack(options).run();
Copy the code

Next we execute the file.

node debugger.js
Copy the code

The command line will print out start Webpack! .

We can start writing mini-Webpack by hand.

Module parse

First, in the constructor, we need to save the configuration information.

constructor(options) {
  const {entry, output} = options;
  this.entry = entry;  // Import file
  this.output = output;  // Export the configuration
}
Copy the code

In the first step of execution, we need to parse the entry file, so we use a parseModules to do this.

module.exports = class Webpack {
    constructor(options){... }run() {
        // Parse the module
        this.parseModules(this.entry);
    }

    /** *@param {*} file* /
    parseModules(file){}}Copy the code

In parseModules, we need to do two things: analyze the module information and recursively walk through the incoming module.

Let’s do it step by step. First, encapsulate a getModuleInfo function to analyze the module information.

parseModules(file) {
  // Analysis module
  this.getModuleInfo(file);
}

 /** * Analysis module *@param {*} file
 *  @returns Object* /
getModuleInfo(file) {}
Copy the code

First, the file we receive is actually the relative path to the entry file, i.e./ SRC /index.js. Therefore, we can first read the contents of the file using node’s built-in FS module.

getModuleInfo(file) {
  // Read the file
  const body = fs.readFileSync(file, "utf-8");
}
Copy the code

Once the contents are read, we need to analyze the contents of the file, which is where the AST syntax tree comes in.

Abstract Syntax Tree (AST) is an Abstract representation of the Syntax structure of source code. It represents the syntactic structure of a programming language in the form of a tree, with each node in the tree representing a structure in the source code.

Demo address: astexplorer.net/

Here we use Babel’s Parse plugin, which converts JavaScript to an AST.

# 7.14.8
yarn add @babel/parser -D
Copy the code
const fs = require("fs");
const parser = require("@babel/parser");

module.exports = class Webpack {...getModuleInfo(file) {
      // Read the file
      const body = fs.readFileSync(file, "utf-8");

      // Convert to an AST syntax tree
      const ast = parser.parse(body, {
        sourceType: 'module'  // indicates that we are parsing the ES module}}})Copy the code

Next, we need to use @babel/traverse to traverse the AST to identify if other modules are introduced into the file and record them if so.

# 7.14.8
yarn add @babel/traverse -D
Copy the code
const traverse = require("@babel/traverse").default;
Copy the code

Traverse takes two arguments, the first one is an AST syntax tree and the second one is an object on which we can set the observer function and for specific node types in the syntax tree.

For example, we only need to find the statement that imported the module this time, and the corresponding node type is ImportDeclaration, we can set the corresponding ImportDeclaration function, and get the node information in the parameter value.

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;


module.exports = class Webpack {...getModuleInfo(file) {
      // Read the file
      const body = fs.readFileSync(file, "utf-8");

      // Convert to an AST syntax tree
      const ast = parser.parse(body, {
        sourceType: 'module'  // indicates that we are parsing the ES module
      })
      
      traverse(ast, {
        / / the visitor function
        ImportDeclaration({node}) {
          console.log(node); }}}})Copy the code

Import {MSG} from “./a.js”;

Therefore, we need to collect its path.

// Dependency collection
const deps = {};
traverse(ast, {
  / / the visitor function
  ImportDeclaration({node}) {
    // Enter the file path
    const dirname = path.dirname(file);
    // Import the file path
    const absPath = ". /"+ path.join(dirname, node.source.value); deps[node.source.value] = absPath; }})Copy the code

{‘./a.js’: ‘./ SRC /a.js’}; the deps is {‘./a.js’: ‘./ SRC /a.js’}.

After collecting the dependencies, we need to turn the AST back into JavaScript code and turn it into ES5 syntax. So this is where we use @babel/core and @babel/preset-env.

#@babel/core -> 7.14.8, @babel/preset- > 7.14.8
yarn add @babel/core @babel/preset-env -D
Copy the code
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

module.exports = class Webpack {...getModuleInfo(file) {
      // Read the file
      const body = fs.readFileSync(file, "utf-8");

      // Convert to an AST syntax tree
      const ast = parser.parse(body, {
        sourceType: 'module'  // indicates that we are parsing the ES module
      })
      
      // Dependency collection
      const deps = {};
      traverse(ast, {
        / / the visitor function
        ImportDeclaration({node}) {
          // Enter the file path
          const dirname = path.dirname(file);
          // Import the file path
          const absPath = ". /"+ path.join(dirname, node.source.value); deps[node.source.value] = absPath; }})// Convert from ES6 to ES5
      const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"],})}}Copy the code

At this point we can print the code and see that it is no longer an ESModule import, but uses CommonJS import.

"use strict";

var _a = require("./a.js");

console.log(_a.msg);
Copy the code

Finally, getModuleInfo returns an object containing the path to the parsing file, its dependencies, and the file code.

parseModules(file) {
  // Analysis module
  const entry = this.getModuleInfo(file);
}

getModuleInfo(file){...return {
    file,   // File path
    deps,  // Dependent objects
    code   / / code
  };
}
Copy the code

But once we’ve analyzed the entry file, we need to do a recursive traversal to analyze the incoming module.

First, we need to create a new array to save all the analysis results. Next, let’s implement the getDeps function to recursively traverse the introduced module.

parseModules(file) {
  // Analysis module
  const entry = this.getModuleInfo(file);
  const temp = [entry];

  // Iterate recursively to get the incoming module code
  this.getDeps(temp, entry)
}


/** * Get dependencies *@param {*} temp
 *  @param {*} module* /
getDeps(temp, {deps}) {}
Copy the code

In getDeps, we can get the dependent object by the second parameter. Then, we can iterate over the object and execute the getModuleInfo function one by one to get the resolved content of each dependent module and save it to temp.

Finally, we call getDeps again, passing in the contents of the imported module, and continue the recursive traversal.

getDeps(temp, {deps}) {
  // Iterate over dependencies
  Object.keys(deps).forEach(key= > {
    // Get the dependency module code
    const child = this.getModuleInfo(deps[key]);
    temp.push(child);
    // Iterate recursively
    this.getDeps(temp, child); })}Copy the code

For example, if b.js is introduced in multiple files, the temp array will hold the contents of multiple B. js objects, so we can recheck first. If the temp object does not have the module, we can perform the following operations.

getDeps(temp, {deps}) {
  Object.keys(deps).forEach(key= > {
    / / to heavy
    if(! temp.some(m= > m.file === deps[key])) {
      const child = this.getModuleInfo(deps[key]);
      temp.push(child);
      this.getDeps(temp, child); }})}Copy the code

At this point, our module parsing operation is almost complete.

Finally, we need to convert the Temp array into an object, similar to __webpack_modules__, with the path as the key name and the value as the corresponding content information.

parseModules(file) {
  const entry = this.getModuleInfo(file);
  const temp = [entry];

  this.getDeps(temp, entry)

  // Turn temp into an object
  const depsGraph = {};
  temp.forEach(moduleInfo= > {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code
    }
  })

  return depsGraph;
}
Copy the code

At this point, we save the parsing results in the run() function, and we are done.

run() {
  // Parse the module
  this.depsGraph = this.parseModules(this.entry);
}
Copy the code

packaging

The next step is to wrap the bundle function.

run() {
  // Parse the module
  this.depsGraph = this.parseModules(this.entry);

  / / packaging
  this.bundle()
}

/** * generate the bundle file */
bundle(){}Copy the code

First, let’s do the easy part, which is to generate the package.

We will use the FS module to identify if the package path exists, create a new directory if it does not exist, and then write the bundle file.

bundle() {
  const content = `console.log('Hello World')`;

  // Generate the bundle file! fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
  const filePath = path.join(this.output.path, this.output.filename);
  fs.writeFileSync(filePath, content);
}
Copy the code

Run the package command, and the project will have a dist folder with an index.js inside.

console.log('Hello World')
Copy the code

Now we have to implement the contents of the bundle file.

First it is an anonymous function that only executes, and then it receives a parameter __webpack_modules__, which is the result of the file we parsed earlier.

(function(__webpack_modules__){
  ...
})(this.depsGraph)
Copy the code

Second, we need to implement the __webpack_require__ function, which takes a moduleId parameter, the path parameter.

Then we need to call __webpack_require__ and pass in the path to the entry file.

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {... } __webpack_require__(this.entry)  
})(this.depsGraph)
Copy the code

As we saw earlier, Babel escapes code to commonJS, so we need to implement the require function because JavaScript doesn’t have it.

The essence of the require function is to return the contents of the imported file.

In addition, we need to create an exports object so that we can save the module’s exported content in it and then return it.

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {
    // Implement the require method
    function require(relPath) {
      return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
    }
    // Save the exported module
    var exports = {};
    
    return exports
  }
  __webpack_require__(this.entry)  
})(this.depsGraph)
Copy the code

Finally, all you need to do is execute the code for the entry file.

Again, we use an anonymous function and call it from there.

(function(__webpack_modules__){
  function __webpack_require__(moduleId) {
    // Implement the require method
    function require(relPath) {
      return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
    }
    // Save the exported module
    var exports = {};
    
    // Call the function
    (function (require.exports,code) {
      eval(code)
    })(require.exports,__webpack_modules__[moduleId].code)
    
    return exports
  }
  __webpack_require__(this.entry)  
})(this.depsGraph)
Copy the code

At this point, we’re going to put this code in the content variable.

bundle() {
  const content = `
    (function (__webpack_modules__) {
      function __webpack_require__(moduleId) {
        function require(relPath) {
          return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
          }
          var exports = {};
          (function (require,exports,code) {
            eval(code)
          })(require,exports,__webpack_modules__[moduleId].code)
          return exports
        }
        __webpack_require__('The ${this.entry}')
    })(The ${JSON.stringify(this.depsGraph)})
  `;

  // Generate the bundle file! fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
  const filePath = path.join(this.output.path, this.output.filename);
  fs.writeFileSync(filePath, content);
}
Copy the code

Then perform the packaging, and you can see the complete package.

(function (__webpack_modules__) {
    function __webpack_require__(moduleId) {
        function require(relPath) {
            return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
        }
        var exports = {};
        (function (require.exports,code) {
            eval(code)
        })(require.exports,__webpack_modules__[moduleId].code)
        return exports
    }
    __webpack_require__('./src/index.js') ({})"./src/index.js": {"deps": {"./a.js":"./src/a.js"},"code":"\"use strict\"; \n\nvar _a = require(\"./a.js\"); \n\nconsole.log(_a.msg);"},"./src/a.js": {"deps": {"./b.js":"./src/b.js"},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports.msg = void 0; \n\nvar _b = require(\"./b.js\"); \n\nvar msg = \"Hello \".concat(_b.something); \nexports.msg = msg;"},"./src/b.js": {"deps": {},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports.something = void 0; \nvar something = 'World'; \nexports.something = something;"}})

Copy the code

And finally, let’s go ahead and see if we can print Hello World.

node ./dist/index.js
Copy the code