takeaway

The previous article (Webpack5 tutorial – introduction) focused on the history of modular specifications and automated build tools, as well as their strengths and weaknesses. Today we are going to systematically explain some of the core features of Webpack5. Later in this article, we will also take you to build a Vue3 project. Related source code has been uploaded to Github: Webpack-Basic.

The relevant versions of Webpack are as follows:

  • Webpack: 5.62.1
  • Webpack – cli: 4.9.1

Let’s do it together

Webpack Usage Guide

Core concept: Build dependency diagrams

In the last article, we spent a lot of time talking about the development of automated build tools and the background of their emergence. These automated build tools have one thing in common: they take source code and run it through a series of operations to get host-environment aware code.

The host environment can recognize the code: the host environment, such as the browser platform, only recognizes HTML/CSS /js/ IMG and other resources, such as SASS/JSX /vue, do not recognize, requiring special processing.

This process can be broken down into tasks or a “pipeline flow” mechanism, but Webpack is similar to a “stream” mechanism in that it can do the same work as an automated build tool and get object code through its Loader mechanism. But the reason it’s not called an automated build tool is that Webpack sees everything as a module (JS/CSS /img/…). , starting from the entrance, find other dependency modules through modularization, build in turn, and finally get the target product. This process is the process of building dependency graph.

Entry/Output

In this section, we will start by creating an empty project called Webpack-Basic and installing webPack and webpack-CLI:

mkdir webpack-basic
cd webpack-basic
yarn init -y Or NPM init -y
yarn add webpack webpack-cli -D NPM I webpack webpack-cli -d
Copy the code

Next we create the following directory structure:

├ ─ ─ package. Json ├ ─ ─ the SRC | ├ ─ ─ index. The js | └ ─ ─ js | └ ─ ─ createTitle. Js └ ─ ─ yarn. The lockCopy the code

The createtitle.js code is as follows:

export default (content) => {
  const h2 = document.createElement('h2')
  h2.innerText = content
  return h2
}
Copy the code

Index.js import createTitle.js module:

import createTitle from './js/createTitle'

const h2 = createTitle('hello webpack')

document.body.appendChild(h2)
Copy the code

As you can see from the dependency diagram above, WebPack will start with the entry, build the dependency diagram, and then process it to get a target product. By default, if we don’t do any configuration, WebPack will find SRC /index.js and output it to dist/main.js. We can use YARN Webpack (or NPX webPack) for testing.

As can be seen from the figure above, packing is normal without any configuration. However, the console will have a prompt telling us that mode is not configured and is set to Production mode by default. This is a new prompt added to Webpackage 5, which we will cover later.

What if we need to customize the entry file, as well as the output directory name?

We can create webpack.config.js in the project root directory, or you can customize the name and configure the config parameter on the command line, which we’ll cover later.

const path = require('path')
module.exports = {
  entry: {
    main: './src/main.js',},output: {
    filename: 'js/[name].[fullhash:8].bundle.js'.path: path.resolve('output'),}}Copy the code

Entry can also be configured as a string to represent single entry packaging, and in the above configuration it is configured as an object. Key is the name of the file we want to package, value is a relative path to the directory of process.cwd(), This is the directory where we run the webpack command, although arrays can be used if the key is not configured.

Filename specifies the name of the output file. A/can be used to add a directory. Path is the directory where the output file resides. It is usually an absolute path.

Fullhash :8 indicates that the hash number of the output file is 8 bits, which is hash in previous versions. Contenthash and chunkhash values can also be configured for better caching.

Create an index. HTML file in the public directory under the root directory and import the package file to see the result (I used VSCode Live Server plug-in here, you can also use the serve tool to preview the effect).

<! 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>
    <script src=".. /output/js/main.0b48a4fc.bundle.js"></script>
  </body>
</html>
Copy the code

Of course, our actual project, of course, not only JS files, but also styles, pictures, TS, Vue and other modules. We also need to process high version JS into low version JS code with good compatibility. At this time, we need to use Webpack to provide a core function: Loader.

It is essentially a function that accepts string /buffer data and returns JS string /buffer after processing. We will explain loader systematically in the following sections.

Style to deal with

Most of our actual development uses CSS pre-processing and post-processing tools such as Sass /less/stylus/postcss, and building our code using these tools requires loader processing. First we’ll create a styles directory to hold the style file:

├ ─ ─ package. Json ├ ─ ─ public | └ ─ ─ index. The HTML ├ ─ ─ the SRC | ├ ─ ─ js | | └ ─ ─ createTitle. Js | ├ ─ ─ the main, js+ | └ ─ ─ styles
+ | ├ ─ ─ global. CSS
+ | ├ ─ ─ the title. The CSS
+ | └ ─ ─ title. Less├ ─ ─ webpack. Config. Js └ ─ ─ yarn. The lockCopy the code

title.less

@fontColor: #1a5f0611.h2 {
  font-size: 20px;
  color: @fontColor;
}
Copy the code

title.css

h2 {
  display: grid;
  transition: all 0.2 s;
}
Copy the code

global.css

@import './title.css';

body {
  background: orange;
}
Copy the code

Next, install the required dependency packages:

yarn add style-loader css-loader postcss-loader less-loader less postcss -D
Copy the code

Note: less-loader can handle less files, but compilation requires less, and postCSs-loader also relies on postCSS. Here is the loader configuration:

    const path = require('path')
    module.exports = {
      entry: {
        main: './src/main.js',
      },
      output: {
        filename: 'js/[name].bundle.js',
        path: path.resolve(__dirname, 'output'),
      },
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader', 'postcss-loader'],
+},
+ {
+ test: /\.less$/,
+ use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'],
+},
+],
+},
    }
Copy the code

Note:

  • allloaderAll inmodule.rulesFor configuration, the option is an array of objects.
  • Rule configuration uses regular expressions to match files.
  • useIt can be a string, an array, an object, used for pairingloaderConfigure.
  • loaderThe rules apply from right to left.

After the configuration is complete, package the page and find that the style can be applied normally, indicating that loader is successfully applied:

Although the style compiles successfully, postCSS does not seem to work. Why?

Let me introduce the working process of the above configuration file:

  • We import less and CSS files in main.js, webpack doesn’t know these modules, and then go to loader to process them.

  • Matches less-loader, processes less files, internally uses less compilation style, and finally outputs JS strings to the next loader for processing.

  • Match postCSs-loader, process the compiled style, use postCSS internally and look up the plug-in, find that the plug-in is not configured, do not process, return JS string, pass to the next loader for processing.

  • Match csS-loader, parse @import and URL () in the file, and return JS string after processing.

  • Match style-loader and create a style tag to add the style to it.

You can see that when we apply PostCSS, there are no plug-ins added, so we need to install them. If we want to add compatible CSS prefixes and want to deal with the new CSS syntax of octet hexadecimal colors (which many browsers don’t recognize), postCSS-preset -env can be used to do this.

First installation:

yarn add postcss-preset-env -D
Copy the code

Then configure in Webpack:

    const path = require('path')
    module.exports = {
      entry: {
        main: './src/main.js',
      },
      output: {
        filename: 'js/[name].bundle.js',
        path: path.resolve(__dirname, 'output'),
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader',
- 'postcss - loader,
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ plugins: [require('postcss-preset-env')],
+},
+},
+},
            ],
          },
          {
            test: /\.less$/,
            use: [
              'style-loader',
              'css-loader',
- 'postcss - loader,
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ plugins: [require('postcss-preset-env')],
+},
+},
+},
              'less-loader',
            ],
          },
        ],
      },
    }
Copy the code

Color: rgba(26,95,6,0.06667); .

Of course, I prefer to separate the configuration into a file to better manage the project:

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-preset-env')]}Copy the code

However, we can see from the compiled result that the relevant CSS prefix is not added:

Why is that?

We now know that PostCSS can handle compatibility with plug-ins, but which platforms should it work with? This can be configured separately, but I’d like to introduce Browserslist. Browserslist tells PostCSS which platforms to support. It also provides a platform compatibility reference for Babel, so any build tool that needs to support a platform compatibility can use this file.

Also, Webpack installs the browerslist package at the same time it is installed, so we just need to configure it. Create.browserslistrc under the root directory:

< span style = "max-width: 100%; clear: both; min-height: 1emCopy the code

Browerslist uses the Can I Use data to filter some browsers. Here I set the usage to 0.01% to test compatibility. This is because most browsers are already very compatible, but it is not recommended for real projects, resulting in performance overhead. If you want to see more configuration usage, check out browerslist’s official documentation.

We can use Yarn Browserslist to see which browsers are matched.

When you pack it again, you should see the effect, but it doesn’t. Why is that? Let’s go back to global.css:

@import './title.css';

body {
  background: orange;
}
Copy the code

The @import file is used to import the CSS module, and this rule will be resolved in CSS-loader, and our postCSS-loader has already handled this rule, that is, CSS-Loader can not go back to the “way”, what to do?

The solution is also very simple, configure csS-loader:

    const path = require('path')
    module.exports = {
      // ... entry/ouput
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
- 'css-loader',
+ {
+ loader: 'css-loader',
+ options: {
+ importLoaders: 1,
+},
+},
              'postcss-loader',
            ],
          },
          {
            test: /\.less$/,
            use: [
              'style-loader',
- 'css-loader',
+ {
+ loader: 'css-loader',
+ options: {
+ importLoaders: 1,
+},
+},
              'postcss-loader',
              'less-loader',
            ],
          },
        ],
      },
    }
Copy the code

The importLoaders value is set to 1, which means that a loader is selected for processing. If the loader is selected for processing, the value is set to 2. At this point, our styling is successful, and here’s what it looks like:

Image/font processing

In practice, we use images in two common scenarios:

  • imgTags bring in images.
  • cssBackground image.

In Webpack 4.x, we usually use url-loader and file-loader to process images:

  • File-loader: Sends the file to the output folder and returns the (relative) URL.
  • Url-loader: Works like a file loader, but returns a data URL if the file is smaller than the limit.
module.exports = {
  module: {
    rules: [{test: /\.(png|svg|gif|jpe? g)$/,
        use: [
          {
            loader: 'url-loader'.// File loader is used internally
            options: {
              name: 'img/[name].[fullhash:6].[ext]'.// Customize the file output name
              limit: 4 * 1024.// Images smaller than 4KB are converted to base64},},],}, {test: /\.(ttf|woff2?) $/,
        use: 'file-loader'},],}},Copy the code

In Webpack 5, we don’t need to install both loaders anymore, it provides a new feature called Type that allows you to configure resource module types and has four values:

  • asset/resourcealternativefile-loader, send a separate file and export the URL.
  • asset/inlinealternativeurl-loaderDerived,data URL
  • asset/sourcealternativeraw-loader, export source code.
  • assetInstead ofurl-loader, can be output depending on the file sizedata URLAgain, send a separate file.

The above configuration can be modified as follows:


module.exports = {
  module: {
    rules: [{test: /\.(png|svg|gif|jpe? g)$/,
        type: 'asset'.generator: {
          filename: 'img/[name].[fullhash:4][ext]',},parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024,},},}, {test: /\.(ttf|woff2?) $/,
        type: 'asset/resource'.generator: {
          filename: 'font/[name].[fullhash:4][ext]',},},],},}Copy the code

Custom resource name can also be output in the output. The assetModuleFilename configured, but personal advice or separately to each resources configuration.

Now let’s modify the code so that the project supports image manipulation.

Directory structure:

├ ─ ─ package. Json ├ ─ ─ postcss. Config. Js ├ ─ ─ public | └ ─ ─ index. The HTML ├ ─ ─ the SRC+ | ├ ─ ─ img
+ | | ├ ─ ─ bg. PNG # > 4 KB
+ | | └ ─ ─ vue. PNG # < 4 KB| ├ ─ ─ js+ | | ├ ─ ─ createImg. Js| | └ ─ ─ createTitle. Js | ├ ─ ─ the main, js | └ ─ ─ styles | ├ ─ ─ global. The CSS | ├ ─ ─ the title. The CSS | └ ─ ─ the title. The less ├ ─ ─ Webpack. Config. Js └ ─ ─ yarn. The lockCopy the code

createImg.js

export default (content) => {
  const img = document.createElement('img')
  img.src = require('.. /img/vue.png')
  return img
}
Copy the code

global.css

    @import './title.css';

    body {
      background: orange;
+ background-image: url('.. /img/bg.png');
    }
Copy the code

main.js

+ import createImg from './js/createImg'
    import createTitle from './js/createTitle'
    import './styles/global.css'
    import './styles/title.less'

    const h2 = createTitle('hello webpack')
+ const img = createImg()

    document.body.appendChild(h2)
+ document.body.appendChild(img)
Copy the code

Now pack again and the image is ready to work:

  • Images larger than 4KB are exported directly to the directory.
  • Image output less than 4KBdata URL.

Note: Make sure that webpack and CSS-loader are installed in the latest version, otherwise there will be an object object problem. This is because in some 5.x versions, images introduced by require are exported by ESM by default. In some versions of CSS-Loader, images imported from the URL are also exported from the ESM. In this case, you can perform the following operations:

  • throughrequireThe imported image can be changed toimportImport, or inrequireAfter adddefault
// Import logo from '.. /img/vue.png'
export default (content) => {
  const img = document.createElement('img')
  // img. SRC = logo
  img.src = require('.. /img/vue.png').default
  return img
}
Copy the code
  • incssthroughurlImport background image, need to configurecss-loader :
    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  importLoaders: 1,
+ esModule: false
                },
              },
              'postcss-loader',
            ],
          },
          {
            test: /\.less$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  importLoaders: 1,
+ esModule: false
                },
              },
              'postcss-loader',
              'less-loader',
            ],
          },
        ],
      },
    }
Copy the code

Script file processing

Using TypeScript and ES6+ in projects is a common requirement, so we can use Babel to help us do it all at once. Install the following packages:

  • @babel/core: Babel core library.
  • @babel/preset-env: A collection of common syntax conversion plug-ins.
  • @babel/preset-typescript: typescript conversion plug-in.
  • Babel – loader.
  • Typescript: Supports TS type verification.

Note: it is also possible to use ts-loader, but it is slower, because after using TS-Loader, Babel may need to compile again, and the process becomes TS > TS compiler > JS > Babel > JS (again). Using @babel/preset-typescript requires only one compiler to manage.

yarn add  @babel/core @babel/preset-env @babel/preset-typescript babel-loader typescript -D
Copy the code

Loader configuration:

    module.exports = {
      mode: 'development'.// Enable packaging in development mode, so that you can view the packaged code later.
      devtool: false.// Turn off the default eval code block.
      resolve: {
        extensions: ['.js'.'.jsx'.'.json'.'.ts'.'.tsx'],},By default, only '. Js' and '. Json 'files can be found.
      module: {
        rules: [{test: /\.(js|ts)x? $/,
            exclude: /node_modules/.// disable node_modules detection
            use: [
              {
                loader: 'babel-loader'.options: {
                  cacheDirectory: true.// Enable directory caching},},],},},}Copy the code

There are some additional configurations not mentioned earlier, mainly to explain the packaged code later, you can look at the comments.

Integrating TS plugins with Babel does not provide type checking, so you need to configure a separate check command to do type checking before packaging (you can also take advantage of ESLint + VSCode’s powerful checking capabilities, described later).

  • Initialize thetsconfig.json.
yarn tsc --init
Copy the code

Modify tsconfig.json configuration:

{
    "compilerOptions": {
        // No output file
        "noEmit":true}}Copy the code

Configure the script:

{
    "scripts": {
        "check-type": "tsc"}}Copy the code

With that in mind, do the following to test:

  • willsrcAll under the directoryjsSuffix file changed tots, includingmain.tsAdd one byPromiseencapsulatedsleepFunction:
import createImg from './js/createImg'
import createTitle from './js/createTitle'
import './styles/global.css'
import './styles/title.less'

function sleep(time = 1) {
  return new Promise((resolve) = > {
    setTimeout(() = > {
      resolve('done')
    }, time * 1000); })}const h2 = createTitle('hello webpack')
const img = createImg()

document.body.appendChild(h2)
document.body.appendChild(img)
sleep().then(console.log)

Copy the code
  • webpack.config.jsIn theentrytomain.ts.
  • Create it in the root directorybabel.config.jsImport the default plug-in.
module.exports = {
  presets: [['@babel/preset-env'], ['@babel/preset-typescript']],}Copy the code

When we open the compiled source file and search for Promise, it still uses the native API. This is because some new features, such as Promise, Map, Set, Generator, etc., cannot be implemented by default. This is done with polyfill. Prior to Babel 7.4.0, polyfill was added by default, but this increased the size of the package and required additional configuration. Polyfill imports two packages if it needs to use these apis in certain files:

  • Core-js 3: Polyfill to implement some new features.
  • Regenerator-runtime: Implementation of the Generator API.

Import the module in use:

import "core-js/stable";
import "regenerator-runtime/runtime";
Copy the code

Of course, we can also configure it in babel.config.js and implement it according to the target browser in.browserslist. Here we just need to install the latest version of core-js, which will automatically import the above two packages.

module.exports = {
  presets: [['@babel/preset-env',
      {
        useBuiltIns: 'usage'.corejs: 3,}], ['@babel/preset-typescript']],}Copy the code

UseBuiltIns has three values:

  • Usage: Find the source code to use the latest ES new features, and then according tobroswserslistThe target platform of the configuration is populated.
  • False: default value. Do nothing.
  • Entry: findbroswserslistAll target platforms are populated.

At this point we’ll package again and see that the built code Promise is fulfilled. If it doesn’t work, check to see if.browserslist market share is configured to > 0.01% (this is low for testing purposes).

Plugin is the most powerful function of Webpack, it is also the core of Webpack, I will focus on the following article, the use of plug-in is mainly introduced to you.

HTML – webpack – the plugin

The htML-webpack-plugin is a good solution to this problem. It can automatically inject scripts into HTML files. We can also customize a template, which supports EJS syntax.

public/index.html

<! DOCTYPEhtml>
<html lang="">
  <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><%= htmlWebpackPlugin.options.title %></title>
  </head>
</html>
Copy the code

Installation:

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

Configure the plugins property in Webpack:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // Other configuration...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'hello webpack'.template: path.resolve(__dirname, './public/index.html'),})],}Copy the code

At this point we can see that the output directory has an index. HTML file, and the script is automatically injected.

The clean – webpack – the plugin to use

We need to manually clear dist every time we pack it, which is a hassle (the hash configuration is different every time). This plugin usually comes with the above plug-in, which deletes the previous files before packing the output directory each time.

Installation and use:

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

Configuration:

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

module.exports = {
    // Other configuration...
    plugins: [new CleanWebpackPlugin()],
}
Copy the code

Open devServer

In addition, we also need to solve cross-domain problems locally, so we can use webpack-dev-server to solve these problems.

Install webpack dev – server:

yarn add webpack-dev-server -D
Copy the code

webpack.config.js

const path = require('path')
module.exports = {
  // Other configuration...
  devServer: {
    static: path.resolve(__dirname, 'public'), // Set the static server directory
    hot: 'only'.// Prevent error from causing the entire page to refresh
    compress: true.// Enable gzip compression on the local server
    historyApiFallback: true.// Prevent the history route from being blank after refreshing
    // Configure the interface proxy
    proxy: {
      '/api': {
        target: 'https://api.github.com'.pathRewrite: { '^/api': ' ' },
        changeOrigin: true,},},},}Copy the code

Note: In some webpack V5.x versions, the static directory configuration provided by the development server is contentBase and hot: ‘only’ is hotOnly: True.

Then configure the script:

package.json

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

Then add a request to main.ts:

    import createImg from './js/createImg'
    import createTitle from './js/createTitle'
    import './styles/global.css'
    import './styles/title.less'

    function sleep(time = 1) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve('done')
        }, time * 1000);
      })
    }

+ async function fetchData(url: string) {
+ return (await fetch(url)).json()
+}

    const h2 = createTitle('hello webpack')
    const img = createImg()

    document.body.appendChild(h2)
    document.body.appendChild(img)
    sleep().then(console.log)

+ fetchData('/api/users').then(console.log).catch(console.log)
Copy the code

In this case, we only need to use YARN Dev. If the printed data can be seen on the browser console, the agent configuration is successful.

Note: after webpack-dev-server v4, HMR (HotModuleReplacement HotModuleReplacement) is enabled by default, and we configured hot above: ‘Only’ is mainly to prevent situations where errors can cause the browser to refresh.

source-map

Source-map is an essential feature of development (provided by Google Browser) to help locate source code errors. Configuration in WebPack can be done using devtool, which has the following categories:

  • Use the eval:evalPackage module code through inevalAdd to the end of the package module//# sourceURLTo find the original code location, not generated.mapThe file is located throughbabel-loaderProcessed code.
  • The source – the map: withoutloaderProcess the source code (complete row information), produced.mapFile, and at the end of the packaged file//# sourceMappingURL=main.bundle.js.mapTo introducemapFile.
  • Being the source – the map: afterloaderAfter processing the source code.
  • Cheap-module-source-map: source code not processed by loader, with only lines of information.
  • The inline – being – the source – the map: will.mapEmbedded as DataURL and not generated separately.mapFile, pastloaderAfter processing the source code, only line of information.
  • The inline – being – the module – the source – the map: will.mapEmbedded as DataURL and not generated separately.mapFile, not passedloaderAfter processing the source code, only line of information.

Behind there will be other types, but generally look down is just [inline – | hidden – | eval -] [nosources -] [being – [module -]] the combination of the source – map mode, detailed information can refer to the document, customized according to demand.

Patterns and environment variables

Earlier we mentioned the mode option, which is used to differentiate between the packaging modes in WebPack, and different optimizations are made for different modes. There are three main values:

  • Development: willDefinePlugin 中 process.env.NODE_ENVIs set todevelopment. Enable valid names for modules and chunks.
  • Production: it willDefinePlugin 中 process.env.NODE_ENVIs set toproduction. Enable deterministic obfuscated names for modules and chunks, and set up optimized plugins such asTerserPlugin 。
  • None: No optimization options are used.

For details, see the WebPack Chinese document.

This is why we can often use process.env.node_env directly in webpack-built projects. We can also inject environment variables ourselves, such as the BASE_URL variable in the HTML file favicon.ico:

<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
Copy the code

It can be configured like this:

  plugins: [
    new HtmlWebpackPlugin({
      title: 'hello webpack'.template: path.resolve(__dirname, './public/index.html'),}),new DefinePlugin({
      BASE_URL: JSON.stringify(' '),})],Copy the code

Note: The value of the injected variable must be a JS string.

Differentiate packaging environment

With the schema and environment variables, we can differentiate the packaging environment. We want to:

  • The development environment provides development server, HMR, development and debugging functions.
  • The production environment provides features such as optimization.

Next we will split the configuration files and merge them using the webpack-merge package (no installation demo).

Root directory Create the following directory structure:

├ ─ ─ the config | ├ ─ ─ utils. Some general methods js # | ├ ─ ─ webpack.com mon. # js common configuration | ├ ─ ─ webpack. Dev. | js # development environment configuration └ ─ ─ webpack. Prod. Js # Production Environment ConfigurationCopy the code

Let’s first pull out some common paths:

Utils. Js:

const path = require('path')

// Working directory
const WORK_PATH = process.cwd()

// Parse the path
function resolvePath(target) {
  return path.join(WORK_PATH, target)
}

module.exports = {
  SRC_PATH: resolvePath('src'),
  OUTPUT_PATH: resolvePath('dist'),
  PUBLIC_PATH: resolvePath('public'),
  WORK_PATH,
  resolvePath,
}
Copy the code

webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
const { OUTPUT_PATH, PUBLIC_PATH } = require('./utils')

module.exports = {
  entry: {
    main: './src/main.ts',},output: {
    filename: 'js/[name].bundle.js'.path: OUTPUT_PATH,
  },
  resolve: {
    extensions: ['.js'.'.json'.'.ts'.'.vue'],},module: {
    rules: [{test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader'.options: {
              importLoaders: 1,}},'postcss-loader',]}, {test: /\.less$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader'.options: {
              importLoaders: 1,}},'postcss-loader'.'less-loader',]}, {test: /\.(png|svg|gif|jpe? g)$/,
        type: 'asset'.generator: {
          filename: 'img/[name].[fullhash:4][ext]',},parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024,},},}, {test: /\.(ttf|woff2?) $/,
        type: 'asset/resource'.generator: {
          filename: 'font/[name].[fullhash:4][ext]',}}, {test: /\.(js|ts)x? $/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'.options: {
              cacheDirectory: true,},},],},plugins: [
    new HtmlWebpackPlugin({
      title: 'hello webpack'.template: path.join(PUBLIC_PATH, 'index.html'),}),new DefinePlugin({
      BASE_URL: JSON.stringify(' '),})],}Copy the code

Webpack. Dev. Js:

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const { PUBLIC_PATH } = require('./utils')

module.exports = merge(baseConfig, {
  mode: 'development'.devtool: 'cheap-module-source-map'.devServer: {
    static: PUBLIC_PATH,
    hot: 'only'.// Prevent error from causing the entire page to refresh
    compress: true.// Enable gzip compression on the local server
    historyApiFallback: true.// Prevent the history route from being blank after refreshing
    // Configure the interface proxy
    proxy: {
      '/api': {
        target: 'https://api.github.com'.pathRewrite: { '^/api': ' ' },
        changeOrigin: true,},},},})Copy the code

Webpack. Prod. Js:

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = merge(baseConfig, {
  mode: 'production'.plugins: [new CleanWebpackPlugin()],
})
Copy the code

In addition, if we need to pass OS level environment variables to Webpack we can use the cross-env package to handle this for us, which differs from DefinePlugin in that one is used at run time and the other is used at compile time.

Installation:

yarn add cross-env -D
Copy the code

After the configuration is complete, you also need to change the script because the configuration file is no longer in the root directory:

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js"."build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",}}Copy the code

With this configuration in place, it is now easy to configure different environments.

Resolution of the code

Whatever modules above are finally packaged into a file, which seems to reduce THE NUMBER of HTTP requests, but the file will become very large, and there will be a certain delay in network transmission, so the operation of code splitting is needed.

Use multiple entry

The easiest way to split code is to configure multiple entries, and WebPack packs a separate file for each entry.

webpack.common.js

module.exports = {
  entry: {
    main: './src/main.ts'.main2: './src/main.ts',}}Copy the code

You can now see that the output directory has an additional main2.bundle.js file. On this basis, if both entries rely on third-party modules and we want the third-party dependencies to be packaged into other files, we can configure them as follows:

webpack.common.js

module.exports = {
  entry: {
    // Depends on the shared module
    main: { import: './src/main.ts'.dependOn: 'shared' },
    // Depends on the shared module
    main2: { import: './src/main.ts'.dependOn: 'shared' },
    shared: ['lodash-es',}}Copy the code

You need to install lodash-es @types/lodash-es packages and import them in the entry file to test them.

At this point, you can see that the packaged directory looks like this:

The shared.bundle.js package was extracted from The Lodash-es package and a LICENSE file was generated. This is the version description for using the open source library. If you do not need to perform the following configuration:

webpack.prod.js

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const Terser = require('terser-webpack-plugin') Webpack V5 is installed automatically

module.exports = merge(baseConfig, {
  mode: 'production'.optimization: {
    minimizer: [
      new Terser({
        extractComments: false,}),],},})Copy the code

Note: configurations related to optimization are centralized in the Optimization configuration.

Configuration splitChunks

This approach is not common, but splitChunks are particularly common for unpacking and dividing chunks. We refer to VueCLI package output directory for configuration, VueCLI output directory will have three types of chunk:

  • Third party dependencies, chunk-vendors.
  • The main entry chunk.
  • The route lazily loads chunk.

Of course, there may be some public modules, this is also more common, let’s explain the configuration in detail.

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')

module.exports = merge(baseConfig, {
  optimization: {
    splitChunks: {
      chunks: 'all'.// Support synchronous/asynchronous import module, with three values: initial, async, all
      minSize: 20000.// Generates the minimum chunk size, in bytes
      minChunks: 1.// This module is imported at least once before being subcontracted
      cacheGroups: {
        // Group 1: Inherits the previous configuration for third-party dependencies
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10.// Indicates the priority set when two packets are matched. A larger value indicates a higher priority
          filename: 'js/chunk-vendors.[fullhash:8].js',},// Group 1: for public modules, the previous configuration is inherited
        default: {
          minChunks: 2.// The module is imported twice for subcontracting
          priority: -20.filename: 'js/[name]-common.[fullhash:8].js',}},// Extract the chunk group}},})Copy the code

A more detailed configuration can be found in the documentation, Portal -> Optimization.splitChunks.

import() + webpackChunkName

In SPA applications, it is common to configure route lazy loading, so that js files will be loaded only when jumping to the corresponding route. This function can be realized by using the import() function.

We split the main. Ts request into js/fetch. Ts and changed it to dynamic import:

; (async() = > {try {
  await sleep(2)
  const { default: fetchData } = await import('./js/fetch')
  const data = await fetchData('/api/users')
  console.log(data);

} catch (error) {
  console.log(error);
}
})()
Copy the code

We package again and see that a 335.bundle.js file is generated. The name is a bit odd. The number 335 was not configured before. This brings us to the chunkIds attribute, which is the algorithm that determines chunk’s selection of output filenames. Common values include:

  • deterministic: The default value, a short numeric ID that does not change between compilations. Good for long-term caching.
  • natural: Produce natural numbers, 1, 2, 3… .
  • named: Generates meaningful strings according to the chunk source file directory.

For example, we configure named:

webpack.prod.js

module.exports = merge(baseConfig, {
  mode: 'production'.optimization: {
    chunkIds: 'named',})Copy the code

A meaningful name is generated:

We can also configure a custom chunk name through output.chunkfilename:

webpack.common.js

module.exports = {
  output: {
    chunkFilename: 'js/chunk-[name].[fullhash:8].js',}}Copy the code

The placeholder name is still generated using the algorithm mentioned earlier. Of course, if you think this is still not user-friendly, you can customize it with magic comments inside import() :

main.ts

; (async() = > {try {
  await sleep(2)
  const { default: fetchData } = await import(/* webpackChunkName: 'fetch' */'./js/fetch')
  const data = await fetchData('/api/users')
  console.log(data);

} catch (error) {
  console.log(error);
}
})()
Copy the code

At this point Webpack will generate the chunkName we need from output.chunkfilename and the name of the magic comment.

Open runtimeChunk

RuntimeChunk can separate webPack module loading code (Webpack has implemented its own module loading method for compatibility with multiple modular specifications), which can enhance the browser cache capability, but the disadvantages are one more request.

webpack.prod.js

module.exports = merge(baseConfig, {
  mode: 'production'.optimization: {
    runtimeChunk: true,}}Copy the code

Build optimization

Extract CSS to a separate file

In the previous example, we created the style tag with the style-loader and inline the style in the HTML. The advantage of this is to reduce the request, but it also affects the performance if the file becomes larger. We can use the mini-CSS-extract-plugin to extract styles to separate files. In addition, we want to use style-loader during development and only extract files in generation mode. We can use the environment variables passed by cross-env mentioned above to judge.

webpack.common.js

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

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  module: {
    rules: [{test: /\.css$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader'.options: {
              importLoaders: 1,}},'postcss-loader',]}, {test: /\.less$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader'.options: {
              importLoaders: 1,}},'postcss-loader'.'less-loader',],},],},}Copy the code

webpack.prod.js

const { merge } = require('webpack-merge')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = merge(baseConfig, {
  mode: 'production'.plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[fullhash:6].css',})]})Copy the code

The CSS code is not compressed. We can use the csS-minimizer-webpack-plugin to do this.

webpack.prod.js

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const Terser = require('terser-webpack-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')

module.exports = merge(baseConfig, {
  mode: 'production'.optimization: {
    runtimeChunk: true.// This option specifically configures code compression
    minimizer: [
      Js / / compression
      new Terser({
        extractComments: false,})./ / compress CSS
      new CssMinimizerWebpackPlugin(),
    ],
  },
})
Copy the code

Preload /prefetch

  • Prefetch: A resource that may be needed for some future navigation. It is downloaded when the browser is idle. It is insensitive to the user and recommended.
  • Preload: Resources may be required in the current navigation. Resources may be downloaded in parallel with other chunks. If the chunk size is large, performance may be affected.

We used the import() function to dynamically load chunk. Let’s look at this code:

; (async() = > {try {
  await sleep(2)
  const { default: fetchData } = await import(/* webpackChunkName: 'fetch' */'./js/fetch')
  const data = await fetchData('/api/users')
  console.log(data);

} catch (error) {
  console.log(error);
}
})()
Copy the code

The chunk will wait 2 seconds before requesting js files for lazy loading. However, we can use the feature of Prefetch to load this resource in advance, because this chunk may be used in the future. It will be loaded when the browser is idle, without affecting the user experience.

Open the prefetch:

; (async() = > {try {
  await sleep(2)
  const { default: fetchData } = await import(
    /* webpackChunkName: 'fetch' */
    /* webpackPrefetch: true */
    './js/fetch')
  const data = await fetchData('/api/users')
  console.log(data);

} catch (error) {
  console.log(error);
}
})()
Copy the code

When packaged, a link tag is added to the browser to indicate that the resource will be loaded when the browser is idle:

<link rel="prefetch" as="script" href="Http://127.0.0.1:5501/webpack-basic/dist/js/.. /js/chunk-fetch.56cdf1d3.js">
Copy the code

TreeShaking

TreeShaking:

You can think of an application as a tree. Green represents the actual source code and library used, the living leaves on the tree. Gray indicates unreferenced code and is the withered leaves on a tree in autumn. To get rid of dead leaves, you must shake the tree to make it fall.

Rollup is a means of optimizing code through the ES Module static Module resolution feature, originally introduced by the Rollup tool. CommonJs TreeShaking is already supported in WebPack V5, and TreeShaking is started by default in Mode: ‘Production’. To demonstrate that this feature can be configured in development mode, there are two important configurations involved:

  • UsedExports: mark unreferenced code -> find withered leaves; Remove unreferenced code with code compression enabled -> Give the tree a good kick and let the withered leaves fall.
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const Terser = require('terser-webpack-plugin')

module.exports = merge(baseConfig, {
  mode: 'development'.// Play Treeshaking manually
  devtool: false.// Remove eval wrapped code for easy viewing
  optimization: {
    usedExports: true.// Mark unused members
    minimize: true.// "shake" the unused members and compress the code using the plug-in provided below
    minimizer: [
      new Terser({
        extractComments: false,}),],},},},})Copy the code
  • SideEffects: marks code for sideEffects, andusedExportsNon-conflicting, mainly used to safely delete code. It andusedExportsThe difference is, if there issideEffectsCode marked as having side effects will not shake off “withered” leaves. Can be found inpackage.jsonTo configure:
{
    "sideEffects": ["./src/some-side-effectful-file.js"."*.css"]}Copy the code

After this introduction, we will do a little test. We will extract all functions from main.ts into js/utils.ts:

export async function fetchData(url: string) {
  return (await fetch(url)).json()
}

export function createTitle(content: string) {
  const h2 = document.createElement('h2')
  h2.innerText = content
  return h2
}

export function createImg(src: string) {
  const img = document.createElement('img')
  img.src = src
  return img
}

export function sleep(time = 1) {
  return new Promise((resolve) = > {
    setTimeout(() = > {
      resolve('done')
    }, time * 1000); })}Copy the code

Then import only sleep in main.ts:

import { sleep, fetchData } from './js/utils'

sleep(2)
Copy the code

To see usedExports effects, we set minimize to false and then pack. At this point we can see some special comments:

Unexported members are tagged by webPack: unused Harmony exports… In the future, it will be taken out. It’s worth noting that even if we import fetchData, if we don’t use it it will still be marked as unreferenced code. This is a very important point, please understand this sentence.

But the truth is, we won’t use this import variables directly, but we need to make this module performs some code, the code may change state, such as timer inside executive function, change is a global state, such as the style code, this case, we must use sideEffects to show that this module is to have side effects, We don’t want to shake this code off, that’s the difference between sideEffects and usedExports, they complement each other to make the code more streamlined.

Configure the CDN

In a word: CDN (Content Delivery Network) is a technology that allows users to access resources on the nearest network node to improve network transmission rates.

Most of our projects will use Vue, VUe-Router, Axios and other third-party modules, which will be packaged together with chunk-vendors (Vue is set to host chunks of third-party dependencies). If there are too many production dependencies used, the initial page will be slow to load. So we can separate these packages into CDNS and not participate in the packaging. Take lodash-es as an example and configure externals.

webpack.common.js

module.exports = {
  externals: {
    // Key is the external dependency name and value is the name of your import
    'lodash-es': '_',}}Copy the code

Packaging Dll library

Dll concept was first introduced by Microsoft, is a “dynamic link library”. It is similar to a CDN, but the files are generally stored locally, and it is a way to speed up the build by pulling out some infrequently changing code and third-party modules and not participating in packaging. If we do not want to introduce loDash-es through CDN, we can extract it into Dll library and introduce it directly through script locally without involvement in packaging.

The following is the process of using Dll:

  • inconfigcreatewebpack.dll.js.
const { resolvePath, WORK_PATH } = require('./utils')
const webpack = require('webpack')

module.exports = {
  mode: 'production'.entry: {
    lodash: ['lodash-es'.'lodash'],},output: {
    path: resolvePath('dll'),
    filename: 'dll_[name].js'.library: 'dll_[name]'.// This is an exported global variable that will be used by the browser in the future via 'dll_lodash.foreach'
  },
  plugins: [
    new webpack.DllPlugin({
      name: 'dll_[name]'.// Set to the library value
      path: resolvePath('dll/[name].manifest.json'), // Set the absolute path to the manifest.json output directory. This file can be interpreted as the.map file in source-map, which is used to locate resources.})],}Copy the code
  • Configuration script.
  "scripts": {
    "dll": "webpack --config config/webpack.dll.js",},Copy the code
  • Packaging.
yarn dll
Copy the code

The root directory structure then creates the DLL as follows:

├ ─ ─ DLL | ├ ─ ─ dll_lodash. Js | ├ ─ ─ dll_lodash. Js. LICENSE. TXT | └ ─ ─ lodash. Manifest. JsonCopy the code

The.txt file can be configured to remove the TerserPlugin property in Minimizer, as described above.

We then need to introduce DLL libraries in our project using the DllReferencePlugin for DLL dynamic library lookup and the AddAssetHtmlPlugin for embedding files into HTML files.

config/webpack.common.js

module.exports = {
  plugins: [
    new DllReferencePlugin({
      context: WORK_PATH, // Make sure it is the same directory as package.json, absolute path
      manifest: resolvePath('dll/lodash.manifest.json'), // Specify the absolute path to the manifest file
    }),
    new AddAssetHtmlPlugin({
      outputPath: 'auto'.// The directory to which the output will be placed
      filepath: resolvePath('dll/dll_lodash.js'), // The absolute path to the DLL file}),].// externals: {
  // 'lodash-es': '_',
  // },
}
Copy the code

The VueCLI and CRA scaffolding tools don’t use DLLS anymore. The VueCLI and CRA scaffolding tools don’t use DLLS anymore. Instead, use webPack’s own optimizations. If you have any questions about this don’t go too far, just get it.

Console optimization

In our development, we generally do not need to be compiled as a result, the most important thing is to have prompt an error, so we can use FriendlyErrorsWebpackPlugin and stats option to optimize our console:

webpack.dev.js

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const { PUBLIC_PATH } = require('./utils')
const FriendlyErrorsWebpackPlugin = require('@nuxtjs/friendly-errors-webpack-plugin')

// Get the boot port. Default is 8080
const portArgvIndex = process.argv.indexOf('--port')
letport = portArgvIndex ! = = -1 ? process.argv[portArgvIndex + 1] : 8080

module.exports = merge(baseConfig, {
  mode: 'development'.devtool: 'cheap-module-source-map'.stats: 'errors-only'.devServer: {
    host: '0.0.0.0',
    port,
    static: PUBLIC_PATH,
    hot: 'only'.// Prevent error from causing the entire page to refresh
    compress: true.// Enable gzip compression on the local server
    historyApiFallback: true.// Prevent the history route from being blank after refreshing
    // Configure the interface proxy
    proxy: {
      '/api': {
        target: 'https://api.github.com'.pathRewrite: { '^/api': ' ' },
        changeOrigin: true,}}},plugins: [
    new FriendlyErrorsWebpackPlugin({
      compilationSuccessInfo: {
        // Modify the display of localhost and network access addresses on the terminal after startup
        messages: [
          `App runing at: `.`Local: http://localhost:${port}`.`Network: http://The ${require('ip').address()}:${port}`,],},}),],})Copy the code

Then the dev command line is configured with a –progress parameter, which shows the percentage at compile time. After configuration, we start the local server and see the familiar console:

There is a plugin I recommend: Webpack-Dashboard, which provides a more complete panel. If you are interested, check out the official documentation.

Post-construction analysis

If you want to analyze the size of a packed file after it is built in production mode, you can use the webpack-bundle-Analyzer plug-in, which provides visualization to help you analyze it.

  • The installation
yarn add -D webpack-bundle-analyzer
Copy the code
  • use

webpack.prod.js

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = merge(baseConfig, {
  mode: 'production'.plugins: [
    new BundleAnalyzerPlugin(),
  ],
})
Copy the code

At this point, we use Yarn Build to package. We can see that the plug-in helps us start a service, and we can see the visual page after the package:

Use the copy – webpack – the plugin

In our development, files that do not need to be packaged, such as Favicon. Ico and static resources, can be directly copied to the output directory through copy-webpack-plugin. The reason why we do not need to copy in the development stage is that the file I/O efficiency is low. You can directly leverage the capabilities of the devServer static server to provide resources directly (the devServer.static property configured earlier).

It’s easy to install and use, so here’s the configuration in plugins:

{
  plugins: [
    new CopyWebpackPlugin({
      patterns: [{from: 'public'.globOptions: {
            // Ignore index.html, which is copied by html-webpack-plugin
            ignore: ['**/index.html'],},},],}Copy the code

Packaging Library

If we are library developers or want to separate out a function and distribute it separately, rollup is recommended first because it provides clean source code and webPack supports it. SRC /js/utils.ts is published as lib.

  • First of all inconfigThere’s another one under the directorywebpack.lib.js
const { resolvePath } = require('./utils')
module.exports = {
  mode: 'production'.entry: './src/js/utils.ts'.output: {
    filename: 'utils.js'.path: resolvePath('lib'),
    libraryTarget: 'umd'.// Compatible with AMD, CJS, ESM and other modules
    library: 'utils'.// The name of our package, the global variable
    globalObject: 'this'.// Which global object to use. The default is 'self' which is the window object. This can be set to compatible with Node and other platforms
  },
  resolve: {
    extensions: ['.ts'.'.js'],},module: {
    rules: [{test: /\.(js|ts)x? $/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'.options: {
              cacheDirectory: true,},},],},}Copy the code

Once packaged, there is a lib file in the root directory, which you can use either directly require or through script in the browser, which I won’t demonstrate here.

In actual combat

Previously we have basically covered some of the core Webpack features, we now know how to use loader to handle a variety of resources, using plugin allows us to expand the ability to build the system. The Vue3 / React project to be introduced below is actually very simple, that is, on the basis of the previous, coupled with the relevant loader and plugin can work.

Normalization project

No matter what the project is, code quality control is required, so ESLint and Git commits need to be normalized. Previously, we only support TS syntax, and if you need code prompts, you also need to start the previously configured check-type in real-time via Watch mode, which is very difficult.

ESLint + Prettier integrated function

Install the following dependencies:

  • Eslint: uses its syntax checking function.
  • Prettier: Uses code style detection
  • Eslint-webpack-plugin: Plugin that webpack integrates with ESLint.
  • Eslint-plugin-vue: supports vUE syntax checking.
  • Eslint-plugin-prettier: Integrates the prettier code style function.
  • Eslint-config-prettier: Overwrites code style detection in ESLint, or resolves conflicts between ESLint and Prettier.
  • @typescript-eslint/eslint-plugin: Integrate TS code checking.
  • @typescript-eslint/parser: TS parser.
yarn add eslint prettier eslint-webpack-plugin eslint-plugin-vue eslint-plugin-prettier eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
Copy the code

Create.eslintrc.js from the root directory:

module.exports = {
  parser: 'vue-eslint-parser'.// parse 
      
  env: {
    browser: true.node: true.es2021: true.'vue/setup-compiler-macros': true Vue 3 compiles macros
  },
  extends: [
    'plugin:vue/vue3-strongly-recommended'.'plugin:@typescript-eslint/recommended'.'plugin:prettier/recommended'].parserOptions: {
    parser: '@typescript-eslint/parser'.// Parse script in SFC
    ecmaVersion: 12.sourceType: 'module'.ecmaFeatures: {
      jsx: true}},rules: {
    'prettier/prettier': 'error'.'vue/multi-word-component-names': 'off'}}Copy the code

Creating.eslintigore ignores some file detection (node_modules is not detected by default, remember that the project is in the root directory, otherwise it doesn’t work) :

*.sh
.vscode
.idea
.husky
.local

*.js
/public
/dist
/config
/dll
/lib
Copy the code

Create a prettier. Config. Js:

module.exports = {
  printWidth: 100.tabWidth: 2.useTabs: false.semi: false.vueIndentScriptAndStyle: true.singleQuote: true.quoteProps: 'as-needed'.bracketSpacing: true.trailingComma: 'none'.arrowParens: 'always'.insertPragma: false.requirePragma: false.proseWrap: 'never'.htmlWhitespaceSensitivity: 'strict'.endOfLine: 'lf'
}
Copy the code

You don’t need to remember related rules. Here are some rules configuration documents. You can find them as needed:

  • Vue Rules
  • Prettier Rules
  • ESLint Rules
  • TS Rules

Configure webpack.dev.js to enhance the development of real-time detection capabilities:

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.common')
const ESLintPlugin = require('eslint-webpack-plugin')

module.exports = merge(baseConfig, {
  plugins: [
    new ESLintPlugin({
      extensions: ['js'.'ts'.'jsx'.'tsx'].emitError: true.emitWarning: true.failOnError: true})]})Copy the code

The eslint-webpack-plugin version has to be installed as 2.1.0 or you won’t be able to find errors on the console. I found it in the official issue. The portal is here.

After configuration, remember to restart VSCode and then start the development server. You can see the console is full of errors:

Git Commit Constraints

The configuration above is only the first barrier to the specification, and some people are actually annoyed by it. It even turns ESLint detection off via inline comments.

It doesn’t matter, the code must be uploaded to Git, right? Then we will check it when he submits the code. If he fails to pass the code, he is not allowed to submit the code. This depends on the following tools:

  • husky: Trigger Git Hooks to execute the script.
  • lint-staged: Detects files, only in the staging areaFiles with changesLint can be checked before committing.
  • commitizen: Use normalizedmessageTo submit.
  • commitlintCheck:messageCompliance with specifications.
  • cz-conventional-changelog: Adapter. provideconventional-changelogCriteria (contract submission criteria). Depending on your requirements, you can also use different adapters (for example:cz-customizable).

Installation:

yarn add husky lint-staged commitizen @commitlint/config-conventional @commitlint/cli  -D
Copy the code

Set up the adapter:

# yarn
yarn commitizen init cz-conventional-changelog --yarn --dev --exact --force

# npm
npx commitizen init cz-conventional-changelog --save-dev --save-exact --force
Copy the code

Use the –force parameter to prevent collisions if you have installed it before.

It will configure the adapter in the local project, then go to install the CZ-Conventional – Changelog package, and finally generate the following code in package.json file:

 "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
Copy the code

Next, configure a script for future git commits:

{
  "scripts": {
    "commit": "git cz"}},Copy the code

Then configure commitLint to validate Git commit messages.

echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
Copy the code

Husky can trigger the Git Hook to execute the script. All we need to do is add the verification tool to the script. Here’s how to do it:

We need to define the Npm script to execute when the hook is triggered:

  • Check the code style and syntax of the files in the staging area before submission
  • The submitted information is normalized to verify
{
  "scripts": {
    "lint-staged": "lint-staged"."commitlint": "commitlint --config commitlint.config.js -e -V"."lint": "eslint ./src/**/*.{js,jsx,vue,ts,tsx} --fix"."prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,jsx,vue,ts,tsx}": [
      "yarn lint"."prettier --write"]}}Copy the code

Next, configure husky to execute the script by triggering Git Hook:

Install hooks that project developers will install whenever they pull code
yarn prepare

# set the 'pre-commit' hook to perform validation before commit
yarn husky add .husky/pre-commit "yarn lint-staged"

# set the 'pre-commit' hook to commit message for validation
yarn husky add .husky/commit-msg "yarn commitlint"
Copy the code

Note: Your repository must be a Git repository prior to this.

After the configuration, use git add. && yarn commit to test it.

Support Vue3 projects

We know that the Vue project is mainly SFC (single file component), and the following dependencies need to be installed to support identification:

  • Vue: Production dependency.

  • Vue-router: indicates the production dependency.

  • @vue/ compiler-SFC (the version must be the same as vUE) : used to compile templates in the SFC.

  • Vue-loader v16.x: recognize and parse.vue files, split script/style files and pass them to other loaders (VueLoaderPlugin).

  • The installation

yarn add vue@next vue-router@next
yarn add vue-loader@next @vue/compiler-sfc -D
Copy the code
  • Configure webpack.com mon. Js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  entry: {
    app: './src/entry-vue.ts'
  },
  resolve: {
    extensions: ['.js'.'.json'.'.ts'.'.vue']},module: {
    rules: [{test: /\.vue$/,
        use: ['vue-loader']},]},plugins: [
    new VueLoaderPlugin()
  ]
}
Copy the code

Configure @babel/preset-typescript to recognize TS code in vue files:

babel.config.js

module.exports = {
  presets: [['@babel/preset-env',
      {
        useBuiltIns: 'usage'.corejs: 3.modules: false}], ['@babel/preset-typescript',
      {
        allExtensions: true // All file extensions are supported}}]]Copy the code

AppendTsSuffixTo configuration in the TS-Loader.

  • SRC directory add the following structure:
├ ─ ─ entry - vue. Ts # vue entrance ├ ─ ─ env. Which statement s # ts file └ ─ ─ VueApp ├ ─ ─ App. Vue ├ ─ ─ the router | └ ─ ─ but ts └ ─ ─ views ├ ─ ─ home | ├ ─ 07.088 ─ 07.088Copy the code

Among them we look at a few core files:

  • entry-vue.ts
import { createApp } from 'vue'
import App from './VueApp/App.vue'
import router from './VueApp/router'

createApp(App).use(router).mount('#app-vue')
Copy the code

VueApp/App.vue

Just a navigation jump function, very simple:

<template>
  <button @click="$router.push({ path: '/', query: { id: 1 } })">to home</button>
  <button @click="handleClick">to login</button>
  <router-view></router-view>
</template>

<script setup lang="ts">
  import { useRouter } from 'vue-router'

  const router = useRouter()
  const handleClick = () = > {
    router.push({ path: '/login'.query: { id: 1}})}</script>
Copy the code

VueApp/ Router /index.ts Route configuration:

import { createRouter, RouteRecordRaw, createWebHistory } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/'.component: () = > import('.. /views/home/index.vue')}, {path: '/login'.component: () = > import('.. /views/login/index.vue')}]const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router
Copy the code

Identification of extension. Vue module:

env.d.ts

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}
Copy the code
  • yarn devStart the project and you’re done!

Wait, this warning is annoying. What is it? We are using the vue version of the ESM-Bundler (vue.runtime.esm-bundler.js), which is used by packers such as Webpack.

  • VUE_OPTIONS_API: indicates whether Options API is supported
  • VUE_PROD_DEVTOOLS: specifies whether to enable DevTool in the production environment

Let’s just inject it using DefinePlugin:

webpack.common.js

{
  plugins: [
    new DefinePlugin({
      BASE_URL: JSON.stringify(' '),
      __VUE_OPTIONS_API__: false.// The Options API is not supported
      __VUE_PROD_DEVTOOLS__: false // Production DevTool is not supported}})],Copy the code

At this point, a simple Vue 3 project is set up. HMR may arise if multiple entry doesn’t work, related issue, the solution is: in the development and optimization in the configuration. RuntimeChunk: ‘single’ single entry didn’t find any problem now.

webpack.dev.js

  optimization: {
    runtimeChunk: 'single'
  },
Copy the code

To optimize

A set of process, our project directory becomes very large and fat, in fact some configuration files need to change very little, and there is a new project in the future after may want to build a again, so I provide the idea is to make a similar VueCLI scaffolding tools, and conforms to the scaffolding company business needs, so that it can greatly improve the efficiency, In the future, WHEN I have the time and ability, I will write an article about the process of CLI development.

conclusion

This article mainly describes some core features in WebPack V5 (of course, some new features such as module federation are not mentioned, which will be discussed in a separate article when it comes to micro front-end architecture. It is not meaningful to talk about it in detail now, so you can understand it), and ends with a practical construction of Vue 3. I believe you will have a harvest after reading this article. It took me about 5 days to write this article, which was much slower than I had planned. In the middle of the article, I wrote that I encountered many potholes. You can see many “tips”.

VueCLI/create-React-app is a better choice. However, for our own development, they are a black box, and we can only make more progress if we enter the black box. After we master these skills, we can have a thinking dimension for the whole project. Therefore, it is very rewarding to build a project from 0 to 1 with plenty of time.

Please wait a few days and start writing loader/ Plugin handwriting and principle analysis later. Please look forward to it!

reference

  • TypeScript holding Babel: A beautiful marriage
  • Do you really need the Webpack DllPlugin?
  • Webpack Chinese document