In the development of projects such as ICE and Rac, we are more or less exposed to the use of build-scripts. build-scripts is a unified build scaffolding solution jointly built by the group that supports flexible plug-in mechanisms for developers to extend build configurations in addition to the basic start, build and test commands.
This article tries to explain the architectural evolution process of build-scripts from simple to complex through the way of scene evolution, note that the evolution process described below is intended to clarify the design principle of build-scripts and the role of related methods, and does not represent the evolution process of build-scripts when actually designing.
Let’s first build such a business scenario:
Let’s say we have a front-end project project-a on our team that uses webpacks to build and package.
Project project-a
project-a/src/say.js
project-a/src/index.js
project-a/scripts/build.js
project-a/package.json
After some time, due to business needs, we created a new front-end project, project-b. Since the project type is the same, the project project-b wants to reuse the webpack build configuration of the project project-a, what should I do at this time?
In order to quickly launch the project, we can first copy a webpack build configuration directly from the project project-a to the project project-b, and then configure the build command in package.json, and the project project-b can be “perfectly reused”.
Project project-b
project-b/package.json
Let’s take a look at our scenario:
After the project project-b was launched for a period of time, the team implemented the project TS, and we first made the following transformation of the project project-a:
Project project-a
project-a/scripts/build.js
project-a/package.json
Since the project project-b also needs to be TS-made, we have to repeat the modification in the project project-a according to the project-a. At this point, the problem of reusing configurations between projects through copying is exposed: when building configuration updates, manual modifications need to be synchronized between projects, and configuration maintenance costs are high and there is a risk of inconsistent modifications.
In general, copying only solves the problem on a temporary basis and is not a long-term solution. If the build configuration needs to be reused across multiple projects, we might consider encapsulating it as an npm package for independent maintenance. Let’s create a new npm package build-scripts to do this:
npm package build-scripts
build-scripts/bin/build-scripts.js
build-scripts/src/commands/build.ts
build-scripts/package.json
We extract the construction configuration of the project into the npm package build-scripts for unified maintenance, and provide project calls in the form of scaffolding to reduce the access cost of the project. The project project-a and the project project-b simply do the following:
Project project-a
project-a/package.json
Project project-b Renovation is the same as project project-a
After the renovation is completed, the project project-a and project project-b no longer need to maintain the build configuration independently in the project, but instead call the build command of build-scripts to build and package it by means of unified scaffolding. When the configuration is updated in the future, each project only needs to upgrade the npm package build-scripts version, avoiding the modification and maintenance problems caused by the previous manual copy.
Let’s evolve our scenario again:
Due to business requirements, we have created a new front-end project, project-c. The project project-c wants to access build-scripts for build packaging, but its packaging entry is not the default src/index, and the build directory is not /dist.
In general, different projects will have certain customization requirements for build configurations, so we need to open some common configurations to the project for settings, such as entry, outputDir, etc. For this purpose, let’s make the following changes to build-scripts:
Let’s start by adding a new user profile build.json for the project project-c.
Project project-c
project-c/build.json
Then let’s modify the execution logic in build-scritps, so that when build-scripts execute the build command, first read the user configuration build.json under the current project, and then use the user configuration to override the default build configuration.
build-scripts/src/commands/build.ts
Through the above transformation, we can basically implement the custom requirements of the project project-c for the build configuration.
But after careful observation, we can find that there are some problems with the above transformation method:
Based on the above questions, let’s make the following changes to build-scripts:
npm package build-scripts
We first extract the default build configuration to a separate file configs/build.ts for maintenance.
build-scripts/src/configs/build.ts
Then we add a ConfigManager class for the management of build configurations, which is responsible for merging user configurations and default build configurations.
build-scripts/src/core/ConfigManager.ts
Then modify the build command execution logic to manage the build configuration by initializing the ConfigManager instance.
build-scripts/src/commands/build.ts
Through the above transformation, we decouple the overlay logic of user configuration and the default build configuration, and at the same time aggregate the validation, override, and other logic of user configuration through the registerUserConfig method of the ConfigManager class for management.
After the transformation is completed, the overall execution process is as follows:
Let’s evolve our scenario again:
Due to business requirements, the project project-c needs to handle xml files, so the project needs to add xml file processing loaders to the build configuration, but build-scripts does not support the extension of config.module.rules, what should I do in this case?
Our previous new user configuration solution is only suitable for some simple configuration overrides, and if the project involves complex build configuration customization operations, there is nothing that can be done.
A common practice in the community is to eject the build configuration into the project and modify it by the user, such as react-scripts. However, the eject operation is irreversible, and if the subsequent build configuration is updated, the project cannot be updated directly by upgrading the npm package, and the extension of the build configuration by a single project cannot be reused between multiple projects.
The ideal approach is to design a plug-in mechanism that allows users to plug-in extend the build configuration and reuse these plug-ins between projects. For this purpose, let’s make the following changes to build-scripts:
A new plugins field has been added in User Configuration build.json to configure a custom plugin list.
project-c/build.json
Then let’s revamp the execution logic in ConfigManager, so that after the ConfigManager has completed the merge of the user configuration and the default configuration, it executes the list of plugins defined in the project build.json in turn, and passes the merged configuration into the plugin as a parameter.
build-scripts/core/ConfigManager.ts
With the build configuration passed in when the plugin executes, we can complete the extension of the build configuration for xml-loader directly inside the plugin.
build-plugin-xml/index.js
Based on the above plug-in mechanism, the project can implement any custom extension of the build configuration, and the plug-in can also be reused between multiple projects in the form of npm packages.
After the transformation is completed, the overall execution process is as follows:
Let’s evolve our scenario again:
Due to build performance issues (scenario assumptions only), the plugin build-plugin-xml needs to adjust the xml-loader’s matching rules before the ts-loader’s matching rules, so we have made the following modifications to the plugin build-plugin-xml:
After the transformation is complete, the plugin build-plugin-xml does a total of four things for the xml-loader extension:
Looking at the above modifications, we can see that although our build configuration is not complicated, it is still cumbersome to modify and extend it. This is mainly because the webpack build configuration is maintained in the form of a JavaScript object, the configuration object in the general project is often very large, and there are layers of nesting between the internal properties, and the modification and extension of the configuration object will involve various operations such as null determination, traversal, branch processing, etc., so the logic will be more complicated.
To solve the problem of complex build configuration modification and extension logic in the plugin, we can introduce webpack-chain in the project:
webpack-chain is a webpack streaming configuration scheme that manipulates configuration objects through chain calls. Its core is ChainedMap and ChainedSet two object types, with the help of ChainedMap and ChainedSet to provide the operation methods, we can easily modify and extend the configuration object, can avoid the previous manual operation of JavaScript objects brought about cumbersome. Without further ado, interested students can check the official documentation.
Let’s first modify the default build configuration to webpack-chain.
build-scripts/src/configs/build.ts
Then we switched the place in ConfigManager that involves building the configuration to the webpack-chain approach.
src/core/ConfigManager.ts
At the same time, the place involved in the user configuration to build the configuration is also switched to the webpack-chain method.
src/commands/build.ts
With webpack-chain, the extension logic of the plugin build-plugin-xml for xml-loader can be simplified to:
Compared with the previous complex null value judgment and object traversal logic, webpack-chain greatly simplifies the modification and extension of configuration objects inside the plug-in, whether it is code quality or development experience, compared with the previous has a lot of improvement.
Let’s evolve our scenario again:
Assuming that the projects that access build-scripts are now react projects, due to the adjustment of business direction, the technical stack of the subsequent team will switch to rax, and the new rax projects want to continue to use build-scripts for the reuse of inter-project build configurations.
Since the default build configuration in build-scripts is based on react, so the rax project cannot be extended directly based on the plugin, is it necessary to create a new build-scritps project based on the rax build configuration? This obviously does not achieve core logic reuse. Let’s think about it another way, since the plugin can modify the build configuration, can we also put the initialization of the build configuration in the plugin? This enables the decoupling of build configurations and build-scripts, and build-scripts can be managed and extended for build-scripts based on build-scripts.
For this purpose, let’s make the following changes to build-scripts:
Let’s first make a tweak to the logic in ConfigManager, adding a new setConfig method to the plug-in for the initialization of the build configuration, since the plug-in also assumes the responsibility of modifying and extending the build configuration, and the call to this part of the logic is after the initial configuration and user configuration are combined, so we register the callback function through the onGetWebpackConfig method to perform this part of the logic.
src/core/ConfigManager.ts
Then we extract the logic related to the default configuration in build-scripts.
npm package build-scripts
Since the user configuration is generally the same as the default build configuration, we also pull it out.
src/commands/build.ts
We will encapsulate the logic related to the default build configuration of the extract into the plugin build-plugin-base.
build-plugin-base/index.js
At the same time, we also need to adjust the logic in build-plugin-xml to call the logic of building configuration extensions through the onGetWebpackConfig method instead of calling back functions.
build-plugin-xml/index.js
Through the above transformation, we have realized the decoupling of the default build configuration and build-scripts, and theoretically any type of project can be based on build-scripts to achieve inter-project reuse and extension of the build configuration.
After the transformation is completed, the overall execution process is as follows:
Finally, let’s expand on the scene:
Suppose there is more than one build artifact of a single project, for example, a Rax project needs to be packaged and built into two types: H5 and Mini Program, and the two types correspond to different build configurations, but build-scripts only support one build configuration, what should I do then?
Webpack actually supports multi-build configuration execution by default, we only need to pass an array to the compiler instance of webpack:
Based on webpack’s multi-configuration execution capabilities, we can consider designing a multitasking mechanism for build-scripts. For this purpose, let’s make the following changes to build-scripts:
First of all, let’s adjust the logic in ConfigManager, change the default configuration of webapck to an array form, add a registerTask method to register the default configuration of webpack, and adjust the relevant logic of the webpack default configuration reference.
build-scripts/src/commands/ConfigManager.ts
The build configuration fetching when the build command is executed also needs to be changed to the form of an array.
build-scripts/src/commands/build.ts
The plugin build-plugin-base also needs to adjust how the default build configuration is registered.
build-plugin-base/index.js
The plugin build-plugin-xml also needs to be added on the corresponding webpack task name parameter.
build-plugin-xml/index.js
Through the above transformation, we have added a multi-task execution mechanism for build-scripts, which can realize multi-build task execution under a single project.
After the transformation is completed, the overall execution process is as follows:
In the above way, we explain the core design principles and related methods of build-scripts through the way of scene evolution. Through the above analysis, we can see that build-scripts is essentially a configuration management scheme with a flexible plug-in mechanism, not only limited to webpack configuration, any cross-project configuration reuse and extension scenarios, you can use build-scripts design ideas.
Note: The sample code involved in this article can be viewed through the repository _build-scripts-demo_[2], and the relevant methods not introduced in build-scripts can also be read by interested students through the repository _build-scripts_[3].
Official Documentation: https://github.com/neutrinojs/webpack-chain
_ build-scripts-demo_: https://github.com/CavsZhouyou/build-scripts-demo
build-scripts: https://github.com/ice-lab/build-scripts