Today, I would like to introduce a streaming configuration scheme of Webpack — Webpack-chain in the community. This scheme has been implemented in my current team and has brought some positive benefits. Now, I would like to introduce the background, core concepts and daily use of this scheme to you.

Why webpack-chain?

I believe everyone is familiar with the well-known build tool Webpack in the industry. As the most stable build packaging tool and the most widely used in production environment, it certainly has many advantages, such as:

  • Ecologically rich. There are plenty of loaders and plugins in the community, and you can find almost anything you want.
  • Pluggable plug-in mechanism. Extensible architecture based on Tapable implementation.
  • The document is mature. There is a Chinese version and it is always updated and maintained.
  • High stability. Now the formal front-end project production environment is basically built with Webpack, after so many years of verification in the industry, the pit is almost stepped on.

But after all this talk of advantages, people are probably still not happy with the product, because the most important thing that can’t be ignored is the development experience. Building packaging is an extremely complex engineering process that requires input of a large amount of configuration information to ensure that the packaging results are as expected. In Webpack, there was no alternative but to manually configure a huge JavaScript object in which all of the configuration information was stored. This primitive approach was a terrible experience for several reasons:

  1. Objects are too large and visually dazzling, and although they can encapsulate some logic, they can’t avoid deep nested configurations;

  2. Difficult to modify dynamically. For example, if a script is used to dynamically modify some configuration information, such as deleting a plugin of Babel-Loader, it is necessary to find the location of babel-Loader step by step from the top-level configuration object, and then traverse the plug-in list. This manual searching and traversing process is quite tedious.

  3. Difficult to share configuration. If you try to share webPack configuration objects across projects, subsequent changes can be messy because you need to change the original configuration dynamically.

Some in the community have also identified these pain points, and a streaming configuration solution for Webpack has emerged – Webpack-chain.

Core concepts of Webpack-chain

In fact, to really learn Webpack-chain, I think the first step is not to learn the specific configuration method of each attribute, but to understand the two core objects of Webpack-chain – ChainedMap and ChainedSet.

What is a ChainMap?

For example, if I now configure the path alias:

config.resolve.alias
  .set(key, value)
  .set(key, value)
  .delete(key)
  .clear()
Copy the code

Now, the ALIS object is a ChainMap. If a property is marked as a ChainMap in webpack-chain, it has some additional methods and allows these chain calls (as shown in the example above).

Here’s a look at each one:

// Clears all attributes of the current Map
clear()
// Remove a single configuration from the Map by key.
delete(key)
// Whether there is a specific key configured for a value in the Map, return true or false
has(key)
// Returns an array of all values stored in the Map
values()
// Provide an object whose properties and values will be mapped into the Map. The second argument is an array indicating which properties to ignore
merge(obj, omit)
// handler: ChainedMap => ChainedMap
// a function that takes a ChainedMap instance as a single argument
batch(handler)
// condition: Boolean
// whenTruthy: ChainMap -> any, when condition is true
// whenFalsy: ChainSet -> any
when(condition, whenTruthy, whenFalsy)
// Get the corresponding key value in the Map
get(key)
// Call get first, and return the result returned by fn if no corresponding value is found
getOrCompute(key, fn)
// Configure key-value pairs
set(key, value)
Copy the code

The return object of these methods is also a ChainMap, which enables chain calls to simplify operations. In Webpack, most of the objects are chainmaps, you can go to the source code to see, implementation is not complicated.

ChainMap is a very important data structure in Webpack-chain. It encapsulates the methods of ChainMap invocation, so that all subsequent configurations of ChainMap type can directly reuse the methods of ChainMap itself.

What is a ChainSet?

Like ChainMap, it encapsulates its own set of apis:

// Add a value to the end
add(value)
// Add a value at the start position
prepend(value)
// Empty the set
clear()
// Delete a value
delete(value)
// Determine if there is a value
has(value)
// Returns a list of values
values()
// Merge the given array to the end of the Set.
merge(arr)
// handler: ChainSet => ChainSet
// a function that takes a ChainSet instance as a single argument
batch(handler)
// condition: Boolean
// whenTruthy: ChainSet -> any
// whenFalsy: ChainSet -> any
when(condition, whenTruthy, whenFalsy)
Copy the code

ChainSet is similar to ChainMap in that it encapsulates the underlying chain call API. When you need to manipulate attributes of array types in Webpack configuration, you can call the ChainSet method.

Shorthand method

For ChainMap, there is a simplified shorthand, which the website calls shorthand:

devServer.hot(true);

// The above method is equivalent to:
devServer.set('hot'.true);
Copy the code

Therefore, direct is often seen in an actual Webpack-chain configuration. Doesn’t it feel clever to call the property () this way? The implementation of the source code is very simple:

extend(methods) {
  this.shorthands = methods;
  methods.forEach(method= > {
    this[method] = value= > this.set(method, value);
  });
  return this;
}
Copy the code

When ChainMap is initialized, the extend method is called, the attribute list is passed directly as a methods argument, and the set method is called indirectly with the following line:

this[method] = value= > this.set(method, value);
Copy the code

This kind of design is also worth learning.

Configuration Webpack

First, we need to create a new configuration object:

const Config = require('webpack-chain');

const config = new Config();

// after a series of chain operations
// Get the final Webpack object
console.log(config.toConfig())
Copy the code

Then configure resolve, Entry, Output, Module, plugins, optimization objects in sequence. In this article, we will introduce how to use each configuration in detail.

Entry and output

Here is an example of a common configuration, because Webpack has too many attributes in the entry and output, we refer to the official Webpack document to match the following way.

config.entryPoints.clear() // The default entry will be cleared
config.entry('entry1').add('./src/index1.tsx')// Add a new entry
config.entry('entry2').add('./src/index2.tsx')// Add a new entry

config.output
      .path("dist")
      .filename("[name].[chunkhash].js")
      .chunkFilename("chunks/[name].[chunkhash].js")
      .libraryTarget("umd")
Copy the code

alias

The configuration of path aliases, which is an essential part of almost all projects, is as follows:

Resolve. Alias is a ChainMap object
config.resolve.alias
  .set('assets',resolve('src/assets'))
  .set('components',resolve('src/components'))
  .set('static',resolve('src/static'))
  .delete('static') // Delete the specified alias
Copy the code

plugins

The configuration of the plugin is quite important. The configuration of webpack-chain will be a little different from the normal configuration. Let’s take a look.

1. Add a plug-in

// First specify a name (this name is custom), then add the plug-in through use
config
  .plugin(name)
  .use(WebpackPlugin, args)
Copy the code

Here’s an example:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

// Specify a name (this name can be customized), and then add the plugin through use. The second parameter of use is the plugin parameter, which must be an array or not
config.plugin('extract')
  .use(ExtractTextPlugin, [{
    filename: 'build.min.css'.allChunks: true,}])Copy the code

2. Remove the plug-in

Removing a plug-in is easy. Remember when we added plug-ins we specified the name of each plug-in? Now remove by name:

config.plugins.delete('extract')
Copy the code

3. Specify that the plug-in is invoked before or after the XX plug-in

For example, I now need to specify that the html-webpack-plugin should be executed before the extract plugin I just wrote:

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

config.plugin('html')
  .use(htmlWebpackPlugin)
  .before('extract')
Copy the code

With the before method, you simply pass in the name of another plug-in, indicating that it will be executed before another plug-in.

Also, if you need to execute after the extract plug-in, call the after method:

config.plugin('html')
  .use(htmlWebpackPlugin)
  .after('extract')
Copy the code

4. Dynamically modify plug-in parameters

We can also use webpack-chain to dynamically modify the plugin’s pass-through, for example:

// Use the tap method to modify parameters
config
  .plugin(name)
  .tap(args= > newArgs)
Copy the code

Modify the plug-in initialization process

We can also customize the plug-in instantiation process, as follows:

// An instance is returned via init, which replaces the original instantiation process
config
  .plugin(name)
  .init((Plugin, args) = > newPlugin(... args));Copy the code

loader

Loader is an essential configuration in Webpack. Let’s take a look at loader operations.

1. Add a Loader

config.module
  .rule(name)
    .use(name)
      .loader(loader)
      .options(options)
Copy the code

Here’s an example:

config.module
  .rule('ts')
  .test(/\.tsx? /)
  .use('ts-loader')
    .loader('ts-loader')
    .options({
      transpileOnly: true
    })
    .end()
Copy the code

2. Modify Loader parameters

You can use the tap method to modify loader parameters:

config.module
  .rule('ts')
  .test(/\.tsx? /)
  .use('ts-loader')
    .loader('ts-loader')
    .tap(option= > {
      / / a series
      return options;
    })
    .end()
Copy the code

After all the configuration is complete, you can call config.toconfig () to get the final configuration object, which can be used directly as the webPack configuration.

3. Remove a Loader

// Delete according to loader name by using the delete method of uses objects
config.module
  .rule('ts')
  .test(/\.tsx? /)
  .uses.delete('ts-loader')
Copy the code

optimization

Webpack of optimization is a relatively large objects that refer to the official documentation: webpack.js.org/configurati…

Here are splitChunks and minimizer as examples:

config.optimization.splitChunks({
     chunks: "async".minChunks: 1.// Minimum chunk, default 1
     maxAsyncRequests: 5.// Maximum number of asynchronous requests. Default: 5
     maxInitialRequests : 3.// Maximum number of initial requests, default 3
     cacheGroups: {// Start setting the chunks for the cache here
         priority: 0.// Cache group priority
         vendor: { // Key is the entry name defined in entry
             chunks: "initial"./ / must choose three: "initial" | | "all" "async" (the default is async)
             test: /react|vue/.// Verify the regular rule. If it matches, extract chunk
             name: "vendor".// The name of the separated chunk to cache
             minSize: 30000.minChunks: 1,}}});// Add a minimizer
config.optimization
  .minimizer('css')
  .use(OptimizeCSSAssetsPlugin, [{ cssProcessorOptions: {}}])/ / remove the minimizer
config.optimization.minimizers.delete('css')
// Modify the minimizer plugin parameters
config.optimization
  .minimizer('css')
  .tap(args= > [...args, { cssProcessorOptions: { safe: false}}])Copy the code

Conditional allocation

As mentioned earlier, there is a conditional configuration method when for both ChainSet and ChainMap objects, which can replace if-else in many scenarios, keeping the configuration chained and making code more elegant.

config.when(
  process.env.NODE === 'production',
  config.plugin('size').use(SizeLimitPlugin)
)
Copy the code

summary

Webpack-chain, as the streaming configuration scheme of Webpack, operates configuration objects through chain call, thus replacing the previous manual operation of JavaScript objects. It not only facilitates the reuse of configuration, but also makes the code more elegant, both in terms of code quality and development experience. Relative to the previous is a good upgrade, recommend everyone to use.

I was miserable configuring Webpack until I came across this streaming configuration solution

Bytedance IES front-end architecture team is in urgent need of talents (P5 / P6 / P7 has a lot of HC), welcome to communicate with us on wechat sanyuan0704, and also welcome everyone to do things together.