preface
Visual building platform has been relatively popular, the specific principle of this paper will not be repeated, here and you talk about an important step of building a platform: the generation process of THE C-side page. In fact, there are two mainstream ways
-
Dynamic solution: The C side maintains a unified entry page that dynamically pulls the required configuration items and component code. The advantages of this approach are rapid release, batch update and repair of code, but the corresponding need to maintain a set of back-end services to deal with the storage and parsing of configuration items, component versioning, page component relational mapping, etc.
-
Statically solidified solution: Each time a page is generated, an entry file containing components and configuration information is generated based on the configuration data for a specific activity, and then the complete front-end compilation process is completed. The advantage is that there is no need for back-end services, no need to worry about the high concurrency impact of big push activities, and the page from page to page is completely isolated, more controllable, and the C-side page experience is better than the dynamic solution.
The following is a description of the optimization we made in compiling speed for the static cure solution.
Webpack based component source code compilation scheme
The use method of building class tools is relatively simple. Usually, operation students need to create an activity in the system, add the required components and carry out the corresponding configuration, and then publish the activity. Our page compilation and publishing service can be roughly divided into the following phases:The generated entry file is roughly as follows. As you can see, the entry file is no different from a normal front-end project.
import componentA from './components/componentA'
import componentB from './components/componentB'
import componentConfig from './componentConfig'
const componentMap = { componentA, componentB }
new Vue({
render(h) {
return h('div',
componentConfig.map(
conf= > h(componentMap[conf.componentName], { props: conf.config }
)
)
}
}).$mount('#root')
Copy the code
In this process, it usually takes 30 to 60 seconds to publish a page, which may vary depending on the complexity of the configuration page. With the increasing proportion of building platforms in the company’s operation activities, especially in the wide use of promotion, the shortening of release time can greatly improve the use experience of operation students. In fact, the time of the entire compilation process is mainly concentrated in the WebPack compilation step, and the time of the other three steps is negligible. There are two reasons why compilation takes so long:
- The component source code is compiled, that is, the imported component in the entry file points to the SRC directory without any processing, which also means that the component needs to go through the WebPack Loader every time the page is published, which is time-consuming.
- Webpack itself is slow. It is known to all that WebPack is a powerful packaging tool that can meet almost any customization requirements through loader and plug-in system. However, its powerful function also makes its architecture relatively complex, and its built-in compression tool Terser is also slow. And the limitations of the JS dynamic language, which makes Webpack unlikely to be fast.
In fact, if the number of pages built by the company is small and the build service calls are at a low level, this option can be chosen, and the advantages are obvious: components can be developed and published, and services can be built without additional processing of components.
Webpack-based component prepacking scheme
A large part of the slow packaging mentioned above is due to component source compilation, so it is natural to think that you can pack components in advance and refer to the packaged code.The important thing to watch out for here is the common dependencies of components, and the repeated packaging of common resources should be avoided. We can set externals solution during component prepackaging
// webpack.config.js
externals: {
vue: 'commonjs vue'.xxx: 'commonjs xxx'. }Copy the code
However, it is difficult to enumerate all possible external dependencies, and the Babel translation of component source code introduces many potential dependencies such as core-JS. Ideally, the package of components contains only the business code of the component itself, and all component dependencies are introduced when the page is packaged. This avoids repackaging dependencies. We can use the externals custom function to exclude all external dependencies
// webpack.config.js
externals: function(context, request, callback) {
if (isExternalsPath(request)) {
return callback(null.`commonjs ${request}`)
}
callback()
}
Copy the code
How do I implement isExternalsPath? The simple and crude approach is to disallow the use of aliases within components and treat all modules imported from non-relative paths as external dependencies.
Another problem with component prepackaging is that module path information is erased and all modules are exported from the DIST file. But this information is actually useful for setting up scenarios. Imagine having a component that provides two styles, style-a and style-b
|- src
- style-a.vue
- style-b.vue
Copy the code
When choosing style – a style – b.v ue in fact can be considered to be dead code, we can use NormalModuleReplacementPlugin style – the module b.v ue to replace. Because the building page to meet a variety of page effects, components often provide a variety of styles, using the above method can be very good packaging products thin, but with components pre-packaged, similar optimization is difficult to achieve.
If we can accept the above problems, the real reason for us to give up this solution is that the packaging speed is not as fast as we expected (within 2s). Although we have temporarily dealt with the problem of re-compiling components, the limitations of WebPack itself are difficult to solve.
Esbuild-based component precompilation scheme
As a new generation of build tools, ESBuild can build 10 to 100 times faster than traditional build tools
Therefore, migrating the compilation link to the ESbuild ecosystem can theoretically make a qualitative leap in compilation speed. We know that for front-end projects, a complete build tool needs to support transform, Bundle, and Minify, but for esBuilds, bundles and Minify are the things they do best. Vite, UMI and others are already using Esbuild for packaging and compression. Although vue, SCSS and other esbuild-related plug-ins already exist in the community, their capabilities as Transformer are not rich enough and mature. Meanwhile, as mentioned above, transforming component code during page compilation is actually a repetitive work. Therefore, we need to precompile the component, and the compiled component code needs to meet the requirements
- Vue single-file components are split into JS files and CSS files
- Js, TS, and TSX files are converted to ES5 JS files
- The SCSS file is converted to the CSS file
- Retain module path information, that is, output code according to the original file structure
The current compilation process:
The core process for component precompilation is as follows
const compile = (filePath, compiledPaths = []) = > {
// Prevent circular references
if (compiledPaths.includes(filePath)) return
compiledPaths.push(filePath)
/ / js file
if (isScript(filePath)) {
const deps = analysisImport(filePath)
// Compile the imported file
deps.forEach(depFilePath= > {
compile(depFilePath, compiledPaths)
})
const { code } = babel.transformFileSync(filePath, babelConfig)
outputFileSync(outputFilePath, code)
// Single file component
} else if (isSfc(filePath)) {
const source = fs.readFileSync(filePath, 'utf-8')
// Single file component split
const { templage, scripts, styles } = parse({
source,
compiler,
filename: filePath
})
const runtimeCode = genRuntimeCode({ templage, scripts, styles })
outputFileSync(outputFilePath, runtimeCode)
/ / CSS files
} else if (isStyle(filePath)) {
let content = readFileSync(filePath, 'utf-8')
// scss -> css
content = preprocess(content)
// postcss
content = postprocess(content)
outputFileSync(outputFilePath, content)
// Copy other file types directly
} else {
copyFileSync(filePath, outputFilePath)
}
}
Copy the code
After the above steps, esBuild only needs to package and compress the code.
On the other hand, in a typical front-end project, we would put the node_modules dependencies into a common chunk for long-term caching, but this is not very suitable for building pages because the life cycle of a single page is very short. We prefer to cache the common dependencies of all pages in a single chunk. SplitChunks similar to WebPack is not provided in esBuild. We achieve similar caching capability by exposing common dependencies as global variables and packaging them into a separate file. When introducing dependencies in the corresponding component code, we need to redirect to global variables. This step can be achieved through custom plugins
// Public dependency module
const commonChunks = ['vue'.'xxx']
await esbuild.build({
...
plugins: [{name: 'global'.setup(build) {
// Common chunks cache
build.onResolve({ filter: new RegExp(` ^ (${commonChunks.join('|')}) $`)},args= > {
return {
path: args.path,
namespace: 'global-ns'
}
})
commonChunks.forEach(path= > {
build.onLoad({ filter: new RegExp(` ^${path}$`), namespace: 'global-ns' }, () = > {
return {
contents: `var mod = window['${path}']. export default mod; `}})})}}]})Copy the code
After the build is completed, we need to write the build product path into the HTML file, and then push to the CDN.
The last
The above are some of our attempts to optimize the construction speed in the rotating construction system. After the optimization, the page publishing speed can be controlled within 2s, which also meets our expectations. Thank you for your reading and welcome to comment.