The author is Daniel Ant Financial · Data Experience Technology team
It’s been a while since Webpack 4 was released. The version of Webpack has come to 4.12.x. However, because Webpack has not completed the migration guide officially, there is still a lack of documentation, and most people still have no idea how to upgrade Webpack.
But the Webpack development team has written a few scattered articles, and the new configuration documentation is available on the Webpack website. Some of the developers in the community have successfully tested the waters, upgrading to Webpack 4 and summarizing into blogs. So I finally got to see what Webpack 4 was all about. Here are some of my experiences with moving to Webpack 4.
This paper focuses on:
- What configuration benefits does Webpack 4 bring? What configuration files need to be modified to migrate?
- Do the previous Webpack configuration best practices still apply in Webpack 4?
Webpack best practices prior to Webpack 4
Using Vue’s official Webpack template vuejs-Templates/Webpack as an example, here is how the mature Webpack configuration files were organized in the community prior to Webpack 4.
Distinguish between development and production environments
The general directory structure looks like this:
+ build
+ config
+ src
Copy the code
There are four webpack configurations in the Build directory. Respectively is:
- webpack.base.conf.js
- webpack.dev.conf.js
- webpack.prod.conf.js
- webpack.test.conf.js
This corresponds to the configuration of the development, production, and test environments respectively. Where webpack.base.conf.js are some common configuration items. We use Webpack-Merge to merge these common configuration items with environment-specific configuration items into a complete configuration item. For example, in webpack.dev.conf.js:
'use strict'
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const devWebpackConfig = merge(baseWebpackConfig, {
...
})
Copy the code
Not only are some of the three environments configured differently, but more importantly, each configuration injects the NODE\_ENV environment variable into the code using webpack.definePlugin.
This variable has different values in different environments, such as development in dev. The values of these environment variables are defined in the configuration file under the Config folder. Webpack first reads this value from the configuration file and then injects it. Like this:
build/webpack.dev.js
plugins: [
new webpack.DefinePlugin({
'process.env': require('.. /config/dev.env.js')}),]Copy the code
config/dev.env.js
module.exports ={
NODE_ENV: '"development"'
}
Copy the code
As for the specific values of environment variables in different environments, for example, the development environment is development, and the production environment is production, it is generally accepted.
Frameworks, library authors, or even our business code have some code that executes different logic depending on the context, such as this:
if(process.env.NODE_ENV ! = ='production') {
console.warn("error!")}Copy the code
This code is preexecuted once during code compression, and if the conditional expression is true, the true branch is removed. This is a compile-time dead-code optimization. This practice of distinguishing between different environments and setting different values for environment variables opens up the possibility of optimizing code for environment at compile time.
Code Splitting && Long-term caching
Code Splitting generally involves doing these things:
- Package separately for Vendor (Vendor refers to third-party libraries or public base components, since Vendor changes less, packaging separately is good for caching)
- Separate package for the Manifest (Webpack Runtime code)
- Packaging common business code for different entries (again, for caching and loading speed)
- Type a common package for asynchronously loaded code
Code Splitting is usually done by configuring the CommonsChunkPlugin. A typical configuration is as follows, with CommonsChunkPlugin configured for Vendor, MANIFEST, and Vendor-Async respectively.
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '.. /node_modules'= = =))0)}}),new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'.minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app'.async: 'vendor-async'.children: true.minChunks: 3
}),
Copy the code
The CommonsChunkPlugin is often difficult to configure, and people’s configurations are often copied from each other. This code is basically boilerplate code. If the requirements of Code Splitting are simple, it would be difficult to configure if there are special requirements, such as sending different packages from vendors of different portals. In general, configuring Code Splitting is a pain in the neck.
The long-term caching strategy works like this: give static files a Long cache expiration time, such as one year. Then add a hash to the file name, which will change every time the file content changes. The browser identifies the file based on the filename, so when the hash changes, the browser reloads the file.
The Output option of Webpack can be configured to hash the file name, for example:
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')},Copy the code
Best practices in Webpack 4
Webpack 4 changes and changes
There are some breaking changes in the Webpack 4 API, but that doesn’t mean it’s completely different. There are only a few points of change. And if you read up on these changes, you’ll be sure to clap your hands.
Migrating to Webpack 4 is just a matter of checking the Checklist to see if these points are covered.
Differentiation between development and production environments
Webpack 4 introduced the mode option. The value of this option can be development or production.
Setting mode also sets process.env.node \_ENV to development or production. Then in Production mode, UglifyJsPlugin and a bunch of other plug-ins are enabled by default.
Webpack 4 supports zero configuration. You can specify the entry location from the command line, or SRC /index.js if you don’t. The mode argument can also be passed in from the command line argument. Some of the usual production environment packaging optimizations can be enabled directly.
Note that there is a limit to Webpack 4’s zero configuration. If you want to add plug-ins or multiple entries, you still need a configuration file.
Nonetheless, Webpack 4 goes all out to make zero configuration more possible. This built-in optimization allows us to focus on business development at the beginning of the project, and then write configuration files as the business gets more complex.
Before Webpack 4 introduced mode, the only way to create different build options for different development environments was to create multiple Webpack configurations with different environment variable values. This is also best practice in the community.
The Mode option introduced in Webpack 4 is an adaptation of best practices in the community. I quite agree with this line of thinking. Open source projects come from the community, grow in the community, absorb from the community, and give back to the community. It’s a virtuous circle. I’ve seen a similar trend in many front-end projects lately. There are several other Webpack 4 features that are also dependent on community feedback.
Will the use of multiple Webpack configurations and manual environment variable injection not work with Webpack 4? It’s not. With Webpack 4, we still need several different configuration files for a serious project. If we do something special with packaging for the test environment, we also need to manually inject the values of NODE\_ENV (such as test) in that configuration file using webpack.definePlugin.
If a test environment is required in Webpack 4, the mode of the test environment is also development. Since there are only two modes of development and production, the test environment should belong to the development phase.
Third-party library build options
In the Webpack 3 era, we need to set the alias for the third party library in the Webpack configuration of the production environment, and set the path of the library to the production build file. This introduces production version dependencies.
Like this:
resolve: {
extensions: [".js".".vue".".json"].alias: {
vue$: "vue/dist/vue.runtime.min.js"}},Copy the code
With the introduction of Mode in Webpack 4, we can do without configuring aliases for partial dependencies, such as React. The React entry file looks like this:
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
Copy the code
This enables the 0 configuration to automatically select the build for production.
But most third libraries do not make environmental judgments about this entry. So in this case we still need to manually configure aliases.
Code Splitting
Another big change for Webpack 4 is the scrapping of the CommonsChunkPlugin and the introduction of optimization.splitchunks.
Optimization.splitchunks is not set by default. If mode is production, Code Splitting is turned on in Webpack 4.
By default Webpack 4 only splits code loaded on demand. We can set splitchunks. chunks to ‘all’ if we want to configure the initial loaded code to be included in the code split.
The biggest features of Code Splitting in Webpack 4 are the ease of configuration (starting with 0) and the automatic Splitting of __ based on built-in rules. The built-in code sharding rules look like this:
- The new bundle is referenced by two or more modules, or from node_modules
- New bundle larger than 30KB (before compression)
- Asynchronous loading A maximum of five bundles can be concurrently loaded
- The number of bundles initially loaded cannot be greater than 3
Simply put, Webpack automatically pulls out the public modules from the code into a package, provided the package is larger than 30KB, otherwise Webpack will not pull out the public code, because the cost of adding a request is not to be ignored.
For specific business scenarios and split logic, see SplitChunksPlugin documentation and WebPack 4: Code Splitting, Chunk Graph and the splitChunks Optimization blog. These two articles basically outline all possible scenarios.
For normal applications, the rules built into Webpack 4 are sufficient.
For specific requirements, the Optimization.splitChunks API of Webpack 4 can also be used.
SplitChunks has a parameter called cacheGroups, which is similar to the previous CommonChunks instance. Each object in a cacheGroups is a user-defined chunk.
As we mentioned earlier, Webpack 4 has a code splitting rule built in, so users can also customize cacheGroups, aka chunks. Which chunk should the module go to? This is controlled by the extraction scope of cacheGroups. Each cacheGroups can define its own scope for extracting modules, that is, which files’ common code will be extracted into its chunk. The priority attribute can be used to control the priority of modules whose scope overlapped between different cacheGroups. Webpack 4 has the lowest extraction priority by default, so modules are extracted into the user’s custom chunk first.
SplitChunksPlugin provides two ways to control the scope of the chunk extraction module. One is the test property. This attribute can be passed in as a string, a re, or a function. All modules will match the criteria passed in by test, and if they do, they will be included in the chunk’s alternative modules. If we pass in a condition like a string or a re, the matching process looks like this: first match the path of the Module, then match the name of the chunk before the Module.
For example, we want to pack all modules introduced in node_modules into one module:
vendors1: {
test: /[\\/]node_modules[\\/]/.name: 'vendor'.chunks: 'all',}Copy the code
Since dependencies loaded from node_modules have node_modules in their path, this re matches all dependencies loaded from node_modules.
The test attribute can control the extraction scope of chunk by module, which is a fine-grained way. SplitChunksPlugin’s second way of controlling the scope of the extraction module is the Chunks property. Chunks can be a string, such as’ all ‘|’ async ‘|’ initial ‘, representing all the chunk, on-demand loaded the chunk chunk and the initial load. Chunks can also be a function where we get chunk.name. This gives us the ability to split code by entry. This is a fine-grained approach, measured in chunks.
For example, we have entrances A, B and C. We want the common code for A and B to be packaged separately as common. In other words, c code does not participate in the segmentation of common code.
We can define a cacheGroups and then set the Chunks property as a function that filters which chunks are included in the cacheGroups. Example code is as follows:
optimization: {
splitChunks: {
cacheGroups: {
common: {
chunks(chunk) {
returnchunk.name ! = ='c';
},
name: 'common',
minChunks: 2,
},
},
},
},
Copy the code
We want to bundle the common code in entry A and B into a single chunk named Common. Using chunk.name, we can easily accomplish this requirement.
In the above case, we know that the Chunks attribute can be used to slice several sets of common code by entry. Now let’s look at a slightly more complicated situation: grouping dependencies in node_modules introduced in different grouping entries.
For example, we have entrances A, B, C and D. We expect the dependencies of A and B to be packaged as Vendor1 and those of C and D to be packaged as Vendor2.
This requirement requires us to filter both entries and modules, so we need to use the test attribute in a less granular way. The idea is to write two cachegroups, one of which is determined by: If the module is introduced on chunk A or CHUNK B, and the module path contains node\_modules, then the module should be packaged in Vendors1. Vendors2 similarly.
vendors1: {
test: module= > {
for (const chunk of module.chunksIterable) {
if (chunk.name && /(a|b)/.test(chunk.name)) {
if (module.nameForCondition() && /[\\/]node_modules[\\/]/.test(module.nameForCondition())) {
return true; }}}return false;
},
minChunks: 2.name: 'vendors1'.chunks: 'all',},vendors2: {
test: module= > {
for (const chunk of module.chunksIterable) {
if (chunk.name && /(c|d)/.test(chunk.name)) {
if (module.nameForCondition() && /[\\/]node_modules[\\/]/.test(module.nameForCondition())) {
return true; }}}return false;
},
minChunks: 2.name: 'vendors2'.chunks: 'all',}};Copy the code
Long-term caching
For long-term Caching, the basic operation is the same as for Webpack 3. However, there is a small problem in the operation of long-term caching for Webpack 3. This problem is about the inconsistency between chunk content and hash changes:
The hash of the Vendor changes when entry, external dependencies, or asynchronous modules are added while the contents of the common code Vendor remain unchanged.
Predictable Long Term Caching with Webpack was featured in the official Webpack column. A solution is given.
At the heart of the solution, Webpack maintains a self-increasing ID, one for each chunk. Therefore, when an entry or other type of chunk is added, the ID of the chunk is changed. As a result, the ID of the chunk is also changed.
We plan is, use webpack NamedChunksPlugin laid a string chunk id identifier, the characters are generally relative path of the module. In this way, the chunk ID of the module can be stabilized.
Vendors1 is the chunk ID
HashedModuleIdsPlugin works in the same way as NamedChunksPlugin, except HashedModuleIdsPlugin uses the hash generated based on the relative path of the module as the chunk ID. So the chunk ID will be shorter. Therefore, HashedModuleIdsPlugin is recommended in production.
Says the article also talked about, webpack NamedChunksPlugin can only work on ordinary webpack module, asynchronous module, external module will not work.
Asynchronous modules can comment chunkName with import, such as import(/* webpackChunkName: “lodash” */ ‘lodash’).then() to have a Name
So we need to use another plugin: name-all-modules-plugin
Some old apis are used in this plugin, Webpack 4 will warn that there is a new version of this PR, but the author may not merge. We can copy the plugin code directly into our Webpack configuration.
After doing these things, the ChunkId of our Vendor will never have any changes that should not happen.
conclusion
The changes in Webpack 4 are primarily an incorporation of best practices in the community. Webpack 4 greatly enhances the Code Splitting experience with a new API. However, the problem of Vendor hash in long-term caching is still unresolved and needs to be configured manually. This article focuses on the similarities and differences between Webpack configuration best practices in the context of Webpack 3.x and 4.x. Hopefully it will help readers organize configuration files for Webpack 4 projects.
Also, survivejs-Webpack is recommended for this online tutorial. This tutorial summarizes Webpack practice in actual development and updates the material to the latest Webpack 4.
If you are interested in our team, you can follow our column, follow Github or send your resume to ‘tao.qit####alibaba-inc.com’.replace(‘####’, ‘@’)
Original address: github.com/ProtoTeam/b…