preface

On-demand loading is used when using UI component libraries such as Vant, Element-UI, and ant-Design, The [babel-plugin-import](https://github.com/ant-design/babel-plugin-import) plug-in can be quickly configured to automatically load components on demand, You can also manually import the corresponding component and style files. At the same time, lazy loading techniques are often used when building projects using Webpack during development. The dynamic loading of component libraries described in this article is different from lazy loading of WebPack building projects. This article will mainly explain the implementation principle of component library on demand loading scheme with the babel-plugin-import plug-in.

Compare webpack lazy loading

Component libraries are loaded on demand:Component library to component as the basic unit output JS, CSS, LESS files, with the help of plug-in or part of the introduction of writing, so that the project code or Babel compiled code only contains the components of JS, CSS, LESS and so on. Webpack lazy loading:Webpack compiles the files introduced by import and require in source code and then splits the larger code into smaller chunks based on the configuration of dynamic loading syntax (usually based on page routing). The application does not load and execute the corresponding chunk code until the corresponding business logic is executed at runtime. Lazy loading of webpack mainly occurs when JS is split into different chunks as shown in the figure below.Visible,The difference between the two mainly lies in:

  1. The execution timing is different. Component library loading on demand is in the source code compilation stage or Babel js compilation stage, while Webpack lazy loading is in the construction and generation of packaged products, component library loading on demand is before webpack lazy loading;
  2. The principle is different. Component library loading on demand removes irrelevant code at the source stage, whereas WebPack lazy loading splits the tree-shaking optimized large file packages and loads them on demand at the appropriate runtime.

Why do you need component libraries loaded on demand

The main purpose of the component library loading on demand is to reduce the size of the project build package, improve the first screen rendering speed on the project line, reduce the white screen time, and reduce the traffic consumption.

Generic component libraries provide a way to import all components and CSS files, for example:

import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';

Vue.use(Vant);
Copy the code

After webpack, this method will introduce and package the ant. Min. js and index. CSS produced by the component library into the build product. Index.css Contains the CSS section of all components. Therefore, this results in a larger build package.

Component library dynamic loading usage

The official Vant documentation recommends using the following two methods to enable the Vant component library to be loaded on demand.

Method 1: Manually load the file

Manually import components that need to be used and their corresponding style files. Other unimported files in the component library are not packaged when webpack components.

import Button from 'vant/lib/button';
import 'vant/lib/button/style';
Copy the code

Method 2: Automatic loading

Install the babel-plugin-import plug-in

npm i babel-plugin-import -D
Copy the code

Modify the Babel plug-in configuration

module.exports = {
  plugins: [['import', {
      libraryName: 'vant'.libraryDirectory: 'es'.style: true
    }, 'vant']]};Copy the code

Import the required components in the project code

import { Button } from 'vant';
Vue.use(Button);
Copy the code

The on-demand nature of component libraries

Loading on demand means importing components on demand, which means manually importing components into your code. The component contains js, CSS and less files, so you need to manually import the corresponding style file vant/lib/button/style.

That is, essentially transforming the source code as follows

import { Button } from 'vant';
Copy the code

convert

import "vant/es/button/style";
import _Button from "vant/es/button";
Copy the code

As you can imagine, it would be very troublesome to manually import JS, CSS or less files every time you need to use a new component like this, so in order to avoid the complexity of writing the introduction, there are two solutions: import all components and use plug-ins to automatically import. Automatic import is the plug-in that converts the imported component to the manual loading method described above.

Babel – plugin – import plug-in

Having described the nature of automatic on-demand loading using plug-ins, let’s take a closer look at how this plug-in is implemented underneath.

Core principles

The core logic of the plug-in can be summarized as shown belowKey processes include: lexical analysis, syntax analysis, AST transformation, code generation.

1. Lexical and grammatical analysis

@babel/core (after v7 version) reads the source content of the project, conducts lexical and grammatical analysis, and then obtains the abstract syntax tree AST. The lexical and grammatical analysis process is not described here. The abstract syntax tree AST can be simply understood as a tree-like structure of data used to describe the source content. The following figure shows the AST structure corresponding to an import statement. An AST consists of multiple nodes. The basic node structure is similar to the following:

{
    "type": "ImportDeclaration"."start": 0."end": 29. }Copy the code

The mapping between source code and AST is shown below:

2. The AST

As mentioned above, the essence of component loading on demand is to transform the source code import notation, as is the babel-plugin-import plugin. This process is performed at the AST level rather than directly manipulating the source code.

Take the code introduced by the following component for example:

import { Button } from 'vant'
Copy the code

The corresponding AST is (Click for the full AST)After conversion, the code is:

import "vant/es/button/style";
import _Button from "vant/es/button";
Copy the code

Corresponding AST is (Click for the full AST)Changes were made to the AST during the transformation: new import declarations for style files were added – the firstImportDeclarationNode, for the second oneImportDeclarationVariable descriptor of a nodeIdentifierA name change and so on.

3. Code generation

After transforming the AST tree, code is generated based on the transformed AST, which is done by @babel/ Generator. The process is to depth-first traverse the AST and build a string that represents the transformed code.

Build the following code from this AST:

import "vant/es/button/style";
import _Button from "vant/es/button";
Copy the code

The above use case demo can be found here github.com/johniexu/de… Look at it.

Brief Analysis of plug-in principle

The plug-in configuration

The plugins in the babelrc configuration file tell Babel to use the babel-plugin-import plugin when compiling the js file. The plugins also specify the parameters of the plug-in, including:

{
  "libraryName": "vant".// Component library name, corresponding to the package name in import syntax
  "libraryDirectory": "lib".// The name of the folder where each component unit resides after compilation
  "style": true.// Whether to import the component's style file. You can also pass less to import the less file
  "styleLibraryDirectory": "".// The name of the folder where the component style files are introduced after compilation
  "camel2DashComponentName": false.// Whether to convert the camel-named import variable to the filename of the corresponding horizontal connection name
  "customName": (name, file) = > { return `vant/lib/${name}` }, // Custom component name introduced after compilation
  "customStyleName": (name, file) = > { return `vant/lib/css/${name}` }, // Customize the name of the style file introduced after compilation
}
Copy the code

For more configuration parameters, see github.com/ant-design/…

Plug-in entry

A Babel plug-in is simply a function that returns a Visitor object, roughly structured as follows

export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state){}}}; };Copy the code

It is called Visitor because of the Visitor pattern, where all the member methods, member objects, configured on the Visitor object are the hook functions that Babel uses to process each node. The hook functions used in the babel-plugin-import plug-in are shown below, followed by comments indicating the timing of each hook.

const methods = [
  'ImportDeclaration'.// import import declaration
  'CallExpression'.// Function call
  'MemberExpression'.'Property'.'VariableDeclarator'.'ArrayExpression'.'LogicalExpression'.'ConditionalExpression'.'IfStatement'.'ExpressionStatement'.'ReturnStatement'.'ExportDefaultDeclaration'.'BinaryExpression'.'NewExpression'.'ClassDeclaration'.'SwitchStatement'.'SwitchCase',];Copy the code

The babel-plugin-import Visitor object also has the Program hook configured as follows:

const Program = {
  enter(path, options) {
  	// ...
  },
  exit() {
  	// ...}}Copy the code

The Program hook is executed when Babel handles a separate file (or module, as the Node specification defines a file as a module). Here, enter and exit are another way of writing hook functions, corresponding to enter and exit hooks respectively. Each hook function can specify the specified hook function when entering and exit respectively. Otherwise, the default hook is Enter.

The logic that Program executes when entering is as follows:

  1. Initialize the plug-in Plugin array based on the configuration parameters received by the plug-in
  2. Iterate through the plug-in Plugin array, executing the initialization methods for each plug-in in turnProgramEnter

Babel-plugin-import is a class that defines the execution logic of hook functions and interconnects with the Babel Visitor object. The difference here is that the Plugin encapsulates some reusable utility methods. The core method is as follows:

Transform import syntax

The transformation import syntax needs to recognize the default import, partial import and whole import syntax of ES6 module specification. The main logic includes identifying whether it is a partial import, only partial import means importing specific components, and converting imported variable names.

import { Button } from 'vant'
console.log(Button) // 1. Partial import

import Vant from 'vant'
console.log(Vant.Dialog, Vant.Toast, Vant.Cell) // 2. Import by default

import * as V from 'vant'
console.log(V.Dialog, V.Toast) // 3. Import all
Copy the code

Import statements such as default import and whole import are removed and converted into partial imports of corresponding components, for example: import Vant from ‘Vant’.

import "vant/es/log/style";
import _log from "vant/es/log";
import "vant/es/cell/style";
import _Cell from "vant/es/cell";
import "vant/es/toast/style";
import _Toast from "vant/es/toast";
import "vant/es/dialog/style";
import _Dialog from "vant/es/dialog";
import "vant/es/button/style";
import _Button from "vant/es/button";
console.log(_Button); // 1. Partial import

console.log(_Dialog, _Toast, _Cell); // 2. Import by default

console.log(_Dialog, _Toast); // 3. Import all
Copy the code

The processing logic is as follows:

  1. ImportDeclarationThe hook records the partial import, default import, and global import statements to the plug-in global state object, and the path object of the node to the plug-in global state object.
  2. The path object stored on the plug-in global state object will be stored in theProgramIterate over execution on exitremoveMethod to remove all original import statements;
  3. inMemberExpression,CallExpression,buildExpressionHandler,buildDeclaratorHandlerWait for the hook function to executeimportMethodFunctions;
  4. importMethodAccording to the configuration parameters of the plug-in, the function calculates the real file import path, whether to import the style file, the style file name, whether to convert the default import configuration, and so onaddSideEffectMethod to add the corresponding partial import statement.

Add style imports

In step 4 above, the importMethod method adds an import to the style file based on the calculated parameter configuration. Its source code implementation logic is as follows:

// plugin.js importMethod part of the method logic
if (this.customStyleName) {
  const stylePath = winPath(this.customStyleName(transformedMethodName));
  addSideEffect(file.path, `${stylePath}`);
} else if (this.styleLibraryDirectory) {
  const stylePath = winPath(
    join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName),
  );
  addSideEffect(file.path, `${stylePath}`);
} else if (style === true) {
  addSideEffect(file.path, `${path}/style`);
} else if (style === 'css') {
  addSideEffect(file.path, `${path}/style/css`);
} else if (typeof style === 'function') {
  const stylePath = style(path, file);
  if(stylePath) { addSideEffect(file.path, stylePath); }}Copy the code

The core uses the addSideEffect method provided by @babel/helper-module-imports to add the import of the style file.

Conversion variable reference

In the above transformation import syntax step, regardless of whether or not you configure transformToDefaultImport to handle Export Default, the variable descriptor Identifier of the imported module is renamed so that the variables corresponding to the referenced import component are transformed.

The main hooks involved in identifiers are:

Property
VariableDeclarator
ArrayExpression
LogicalExpression
ConditionalExpression
IfStatement
ExpressionStatement
ReturnStatement
ExportDefaultDeclaration
BinaryExpression
NewExpression
SwitchStatement
SwitchCase
ClassDeclaration
Copy the code

Each of the above hook functions has a slightly different attribute for Path.node, but ultimately all need to translate the Identifier name attribute.

Therefore, the main logic of the conversion variable reference is:

  1. The hook function is used as the entry to find the variable related attributes of different nodes according to different node types.
  2. Check variablenameExists in the plug-in global statespecfied, that is, whether the variable is pointed to by the importing component;
  3. throughpath.scope.hasBinding,path.scope.getBindingExclude variables from other scopes;
  4. With the help ofimportMethodMethod to calculate the variable name corresponding to the converted module and then modify the variable name corresponding to the node.

conclusion

After the above explanation, we have understood the ways and differences of introducing component libraries when using common UI component libraries. We have also learned how to configure the loading of component libraries on demand. Finally, we have analyzed the implementation principle of the babel-plugin-import plug-in. Knowing that the so-called component library loading on demand is a code transformation during the Babel JS compilation phase.

The introduction to the Babel plugin and the AST abstract syntax tree is not sufficient. Since these are not the focus of this article, it is highly recommended to read the article in Resources for more details.

The resources

  1. View AST tool: astexplorer.net/
  2. In-depth Babel, this one is enough juejin.cn/post/684490…
  3. A (very long) breath to understand Babel juejin.cn/post/684490…
  4. Babel plugin manual github.com/jamiebuilds…
  5. ECMAScript 6 Getting started – Syntax for Module es6.ruanyifeng.com/#docs/modul…

Write in the last

Now that I see this, I might as well click a “like” to encourage the author

Author’s blog: blog.lessing.online/

Public account: [Dino notes]

Language finch: www.yuque.com/johniexu

Github:github.com/johniexu