contexts
When we use Vite for packaging, we often encounter this problem: as the business unfolds, version iteration, more and more pages, more and more third-party dependencies, the package is getting bigger and bigger. If the pages are dynamically imported, all the files shared by several pages will be unpacked independently, resulting in a large number of chunk fragments. Many of these chunks are very small in size, such as 1k, 2k, 3k, which significantly increases resource requests from the browser.
While it is possible to passCustomizing the sub-packaging strategy, but the dependencies between files are intricate, and a little carelessness in the sub-packaging configuration will either lead to the initial package size being too large, or lead to cyclic dependency errors, so the burden on the mind is heavy. So is there an automated sub-packaging mechanism to completely solve the problem of packaging fragmentation?
Two Pitfalls of Unpacking and Merging
As mentioned earlier, the use ofThere are two pitfalls of the customized subcontracting strategy, which are expanded on here:
1. Resulting in excessive initial package size
As shown in the figure, file A originally depends only on file C, but according to the configuration of the package shown in the figure, Chunk1 and Chunk2 must be downloaded before using file A. In a slightly larger project, due to the complexity of the dependencies between the files, this dependency will spread quickly as a large number of small files are merged, resulting in the initial package size being too large.
2. Causing circular dependency errors
As shown in the figure, the interdependencies between the files result in a circular dependency error in the packaged Chunk1 and Chunk2. Then in complex projects, it is even more common to have interdependencies between businesses.
The solution: the modularization system
Because subcontracted configurations lead to both of these pitfalls, they are often difficult to step through, and it is hard to have an easy-to-use configuration rule that can be followed. This is because the subcontracting configuration is closely related to the current state of the business. Once the business changes, the subcontracting configuration needs to change accordingly.
In order to solve this challenge, I introduced a modularization system in the project. That is, the code of the project is split according to the business characteristics to form a combination of several modules. Each module can contain pages, components, configurations, languages, tools and other resources. Then a module is a natural unpacking boundary , in the build build , automatically packaged into an independent asynchronous chunk , say goodbye to Vite configuration troubles , while effectively avoiding the fragmentation of the build product . Especially in large business systems, this advantage is especially obvious. Of course, the modularization system is also conducive to decoupling the code and facilitating the division of labor.
Since a module is an unpacking boundary, we can control the size and number of product chunks by controlling the content and number of modules. And the module division is based on business characteristics with realistic business meaning, compared to theCustomization, apparently, has a low mental burden.
file structure
As the project continues to iterate and evolve, the business modules created will also expand. For some business scenarios, it often requires the cooperation of multiple modules to realize. Therefore, I also introduced the concept of kits in the project, a kit is a combination of a set of business modules. In this way, a project is a combination of several kits and several modules. The following is the file structure of a project:
project
├── src
│ ├── module
│ ├── module-vendor
│ ├── suite
│ │ ├── a-demo
│ │ └── a-home
│ │ ├── modules
│ │ │ ├── home-base
│ │ │ ├── home-icon
│ │ │ ├── home-index
│ │ │ └── home-layout
│ └── suite-vendor
name (of a thing) | clarification |
---|---|
src/module | Stand-alone module (not part of a kit) |
src/module-vendor | Stand-alone modules (from third parties) |
src/suite | suite |
src/suite-vendor | Kits (from third parties) |
name (of a thing) | clarification |
---|---|
a-demo | Test suite: put the test code into a suite so that it can be easily disabled at any time |
a-home | Business suite: includes 4 business modules |
Packing effect
Here's a look at the actual packing results:
modularhome-baseFor example, the left side of the figure shows the code of the module, the right side of the figure shows that the module is packaged file size 12K, after compression is 3K. to achieve this effect of sub-packaging, do not need to do any configuration.
As another example, we can also centralize the layout components into the modulehome-layoutManage it. The module is packaged into separate Chunks that are 29K in size and 6K when compressed.
source code analysis
1. Dynamic import module
Since the project's module directory structure is regular, we can extract the list of all modules before starting the project and generate a js file to centralize the dynamic import of modules:
const modules = {};
...
modules['home-base'] = { resource: () => import('home-base')};
modules['home-layout'] = { resource: () => import('home-layout')};
...
export const modulesMeta = { modules };
Since all modules are dynamically imported via the import method, they are automatically split into separate chunks when packaging with Vite.
2. Unpacking configuration
We also need to passCustomized unpacking configurations so as to ensure that code within modules is uniformly packaged together to avoid fragmented files.
const __ModuleLibs = [
/src\/module\/([^\/]*?)\//,
/src\/module-vendor\/([^\/]*?)\//,
/src\/suite\/.*\/modules\/([^\/]*?)\//,
/src\/suite-vendor\/.*\/modules\/([^\/]*?)\//,
];
const build = {
rollupOptions: {
output: {
manualChunks: (id) => {
return customManualChunk(id);
},
},
},
};
function customManualChunk(id: string) {
for (const moduleLib of __ModuleLibs) {
const matched = (moduleLib);
if (matched) return matched[1];
}
return null;
}
Match each file path with a regular expression, and use the corresponding module name as the chunk name if the match is successful.
The solution to two hidden dangers
If the modules depend on each other, then there is also a possibility of the two pitfalls mentioned earlier, as shown in the figure:
In order to prevent both hidden situations, we can implement a more fine-grained mechanism for dynamic loading and resource location. Simply put, when we add a resource to theModule 1interim reportModule 2When you want to find the resources of module 2, you first load module 2 dynamically, then find the resources of module 2 and return them to the user.
For example, there is a Vue component in Module 2Card
In module 1, there is a page componentFirstPage
We need to add the page componentFirstPage
hit the nail on the headCard
component. So, here's what we need to do:
// Dynamically loaded modules
export async function loadModule(moduleName: string) {
const moduleRepo = [moduleName];
return await ();
}
// Generating Asynchronous Components
export function createDynamicComponent(moduleName: string, name: string) {
return defineAsyncComponent(() => {
return new Promise((resolve) => {
// Dynamically loaded modules
loadModule(moduleName).then((moduleResource) => {
// Returns the components in the module
resolve([name]);
});
});
});
}
const ZCard = createDynamicComponent('module (in software)2', 'Card');
export class RenderFirstPage {
render() {
return (
<div>
<ZCard />
</div>
);
}
}
Advanced import mechanism
Although the use ofcreateDynamicComponent
This accomplishes the desired goal, but the code is not clean enough to take full advantage of the auto-import mechanism provided by Typescript. We would like to still use components in the usual way:
import { ZCard } from 'module (in software)2';
export class RenderFirstPage {
render() {
return (
<div>
<ZCard />
</div>
);
}
}
Code like this, which is a form of static import, results in aModule 1cap (a poem)Module 2Strong interdependence. So, is there a way to have the best of both worlds? Yes, there is. We can develop a Babel plugin that parses the AST tree and automatically changes the ZCard import to a dynamic import. In this way, our code is not only simple and intuitive, but also can realize dynamic import, avoiding the two hidden dangers of subcontracting. In order to avoid the topic distraction, how to develop Babel plugin will not be expanded here, if you are interested, you can directly refer to the source code:babel-plugin-zova-component
concluding remarks
This paper analyzes the causes of packaging fragmentation in Vite, and proposes a modular system to simplify the configuration of sub-packaging, and at the same time adopts the dynamic loading mechanism, which perfectly avoids the occurrence of the two hidden dangers when sub-packaging.
Of course, to achieve a complete modular system, there are many more details to consider, if you want to experience the effect of out-of-the-box, you can visit my open source framework:/cabloy/zovaThe following is a list of the most important things you can do to help you get the most out of the program You can add my WeChat, into the group exchange: yangjian2025