“This is the second day of my participation in the November Gwen Challenge. See details of the event: The last Gwen Challenge 2021”.

The introduction

For those of you who are not familiar with the basics of Babel and plug-in development, check out this article “Front-end Infrastructure” to take you through the world of Babel and supplement your knowledge of the basics

As a front-end developer, I believe you all have a component library of your own, both for business and learning.

Here, from a Tree Shaking perspective, we talk about how to provide on-demand loading for our own component libraries.

What is a Tree Shaking

What is Tree Shaking

The concept of Tree Shaking is familiar: Shaking code away from our code that we don’t use to reduce the size of our packages.

Let’s look at a simple example:

// index.js entry file
import { funcHao } from './math'

funcHao()
Copy the code
// math.js
const funcWang = () = > {
  const obj = {};
  return obj;
};

const funcHao = () = > {
  console.log('hao');
};

export { funcWang, funcHao };


export {
  funcWang,
  funcHao
}
Copy the code
// webpack.config.js
const { resolve } = require('path');

module.exports = {
  mode: 'production'.entry: resolve(__dirname, './src/index.js'),
  module: {
    rules: [{test: /\.js$/,
        use: [
          {
            loader: 'babel-loader'.options: {
              presets: ['@babel/preset-env'],},},],},output: {
    filename: '[name].js',}};Copy the code

Here we create a new project with only two files using Webpack.

  • src/index.js: Import file, importmath.jsIn thefuncHaoMethods.
  • src/math.js: Exports two methodsfuncHaoandfuncWangTwo methods.

Tip: The reason we configure Babel here is not just for presetting arrow functions, but I’ll talk about why a babel-preset-env is configured later.

Let’s run the webpack command to pack our code:

// dist/main.js
(() = >{"use strict";console.log("hao")}) ();Copy the code

We’ll see that the packaged code only exists in console.log(“hao”), and the funcWang content is not packaged.

It’s called Tree Shaking: Dead Code Elimination based on the ES Module specification, dead-Code Elimination statically analyzes the imports and exports between modules during execution, determines the exported values in the ESM Module that are not used by other modules, and then removes them to optimize the package.

In simple terms, this is to optimize the code by removing code that is not used in the project.

Tree Shaking works

Additional notes are needed:

  • Tree ShakingIs based onESMModule base for processing.

Why does Tree Shaking need the ESM module to use? Let’s take a look at the problem.

Briefly speaking, the execution process of a SECTION of JS code needs to go through the following three steps:

  • V8 conducts lexical analysis from source code, which generates AST and execution context.

  • Generate computer-executable bytecode from the AST.

  • Execute the generated bytecode.

In the JS execution process, ES Module can confirm the corresponding dependency at the first step (compilation stage), and can confirm the Module import and export without execution.

ES Modules import and export modules during JS compilation, so we don’t need code execution to determine the rules between modules according to ESM to achieve Tree Shaking. We call it static analysis feature.

Similarly, compared with commonJS module, it depends on the execution of the code, and the dependency of the module can only be confirmed after the completion of the code execution in the third phase.

Tree Shaking is not supported.

Dynamic import is introduced in ES Module, because it is also a dynamic Module relationship that needs TO be confirmed after JS execution. Tree Shaking is not supported.

Why should I configurebabel-preset-env

I intentionally configured @babel/preset-env to handle our code, as those of you who are familiar with it might know.

@babel/preset-env is a modules configuration parameter that exists, and its default is auto.

The implication of the modules configuration is to enable the ES module syntax conversion to another module type in preset-env translation.

You may see it in many tutorials or websites, because Tree Shaking must be based on the Es Module Module.

So if we use babel-preset-env in our project, we need to set its modules to false: equivalent to telling Babel, “Hey, Babel please keep the ESM module specification in my code”.

Yes, there is nothing wrong with you setting it to false, but our configuration above is not configured at all. It is Tree Shaking when the default value is auto.

Have you ever wondered why? In daily work, I believe most students don’t intentionally configure modules:false when preset-env is used in conjunction with their business.

The root cause is in the default parameter auto.

Configured to auto, by default, @babel/preset-env uses caller data to determine whether import() should transform ES modules and module functions (for example).

If we call Babel using babel-loader, modules will be set to False because WebPack supports the ES module.

You can see more details about the Auto parameter in this Commit.

In fact, THE reason FOR setting preset-env is to talk about the meaning of Modules: Auto, and I’m sure there are many people who still have a vague understanding of modules: Auto.

Tree Shaking

After talking about what Tree Shaking is, let’s actually implement a Tree Shaking plugin based on Babel.

Component libraryTree Shakingcourse

First, compiling code into Es Module modules was not supported in older versions of WebPack, so some component library compiled code cannot be handled using Tree Shaking. (Because the code it compiles is not ES Module at all!)

So in older versions of component libraries, such as Element-UI, babel-plugin-component is borrowed, and in older versions of Ant-Design, babel-plugin-import is used to analyze code to achieve the Tree Shaking effect.

The fact that these two plugins are similar is something we’ll focus on later.

If you are aware, Tree Shaking is already fully supported in antD and Elemental-Plus.

That’s right! This is because its code is now packaged with an extra copy of the ES Module specification. In combination with the Module field in package.json, it is perfectly possible to tree-shaking ES Module modules without the help of any plug-ins.

Since the birth of yu, how to speak bright?

Some students may ask, since many build tools now support packaging ES Module specification, so we can directly package the component library into the ES Module specification? Why bother writing such a Babel plugin to use.

In the first place, the reason why I chose to write a Tree Shaking plugin is to give everyone a “inside look” through such a plugin. After understanding the development history of Tree Shaking and component libraries, I learned the development process of Babel plug-in by combining previous industry practices. I personally find this plugin to be the best for getting started and thinking clearly.

Second, it is true that our component libraries can be packaged as Es Modules to support Tree Shaking directly, but inevitably some of the library packages we use in our business generate files that are not based on the Es Module specification.

Now, if we use these libraries. Different ways to store Tree Shaking in separate files, we can use our own Tree Shaking plugin to analyze Tree Shaking at import time.

Let’s take Lodash as an example:

import { cloneDeep } from 'lodash'

/ /... Business code
Copy the code

When you use LoDash this way, the packaged LoDash is not based on the ESM module specification. So we can’t do Tree Shaking.

import cloneDeep from 'lodash/cloneDeep'

/ /... Business code
Copy the code

At this point, the location of the cloneDeep method in LoDash is a separate file — the Lodash /cloneDeep file.

When we do this, we’re just introducing a JS file. You can significantly reduce the amount of imported volume and thus remove unnecessary code.

Of course, loDash already provides the ES standard library, so we’ll just use it as an example to give you a better understanding.

BabelA pluginTree ShakingThe principle of

In fact, the loDash example above is pretty close to what the plug-in is supposed to do.

When importing specific library methods using import (not the default), we parse the corresponding import statements and rewrite the corresponding import statements: importing the corresponding methods to the corresponding individual files Tree Shaking the effect.

Of course, some students may be curious, can I just do this:

import cloneDeep from 'lodash/cloneDeep'
import join from 'lodash/join'
import findLast from 'lodash/findLast'
/ /...

Copy the code

Emm… It does work, but, uh. How can a qualified front-end engineer write such redundant code?

import { cloneDeep,join,findLast } from 'lodash'
Copy the code

In contrast, this is not more refreshing 😂.

Implement the Babel plug-in

It needs to be usedBabelpackage

A detailed Babel configuration guide and basic plug-in developer’s guide are posted at the top link. Here I will briefly mention the babel-related tool package used in plug-in development:

  • @babel/coreCore:babelTranslation package, which is mainly used heretransformMethods.
  • babel/types: babelToolkit, which is used here to generate the correspondingASTNodes and calls correspond to check nodesAPI.
  • babel/handbook: babelPlugin Developer manual,herecoversbabelThe plug-in process andAPI.

Developing a plug-in

Having talked about so many principles, let’s come to the Coding stage.

// index.js
const core = require('@babel/core');
const babelPluginImport = require('./babel-plugin-import');

const sourceCode = ` import { Button, Alert } from 'hy-store'; `;

const parseCode = core.transform(sourceCode, {
  plugins: [
    babelPluginImport({
      libraryName: 'hy-store',})]});console.log(sourceCode, 'Entered Code');
console.log(parseCode.code, 'Output result');
Copy the code

In index.js we first create a test file. To test our plug-in. Import {Button, Alert} from ‘hy-store’; import {Button, Alert} from ‘hy-store’; .

What we want to print is:

Our plug-in also needs to accept a parameter called libraryName. This parameter tells our plug-in that only the import statement for the libraryName will be processed.

With the basic test plug-in code set up, let’s get into the logic inside the plug-in:

The Babel plug-in is essentially an object containing onevisitorProperty to targetvisitorOn the propertieskeyFor depth traversal generatedAST, matches to the correspondingvisitorOn thekeyTriggers the corresponding method to perform the matchingASTThe add, delete, change and check implementation of the node generates newAST-> Generate a new onecode.

Of course, you can export a function that returns this object.

Let’s start with basic plug-in structure development:

const core = require('@babel/core');
const t = require('@babel/types');

function babelPluginImport(options) {
  const { libraryName = 'hy-store' } = options;
  return {
    // The Babel plugin is based on the 'visitor' observer mode
    visitor: {
     // do something}}; }module.exports = babelPluginImport;

Copy the code

Next let’s open –Astexplorer and enter our input code and output code:

Let’s first look at the input (parsed) code:

  • Pay attention to1The location we chose is the compiler@babel/parser.
  • And we’re going to enter our theta on the leftsource Code.
  • The right-hand side will dynamically generate the correspondingAST.

Let’s enter targetCode again for the same operation:

I want you to stop for a moment and look down, and compare the difference between these two trees for yourself. Think in your mind what you need to do if you convert the Tree in source to the Tree in target.


Let’s get this straight:

  • First, when we encounter the ImportDeclaration statement, we need to determine whether its source comes from our libararyName library.

  • When matching is introduced into our corresponding library, we also need to go through whether the default ImportDefaultSpecifier export is included in the specifiers in the current ImportDeclaration node.

  • When both of the above conditions are met, we proceed to our logical processing.

    1. The introduction of (import) is what we pass in to matchlibraryName.
    2. Not included in the import statementimport xx from libraryName(Default export statement).
  • We need to iterate over the Specifiers in the ImportDeclaration on the left, changing each export statement in the SpeciFIERS to the default export statement corresponding to a separate file path.

In a nutshell, a Tree Shaking Babel Pluign goes through these four steps.

const t = require('@babel/types');

function babelPluginImport(options) {
  const { libraryName = 'hy-store' } = options;
  return {
    visitor: {
      // Enter when matching ImportDeclaration
      ImportDeclaration(nodePath) {
        // checked Validity
        if (checkedDefaultImport(nodePath) || checkedLibraryName(nodePath)) {
          return;
        }
        const node = nodePath.node;
        // Get the declaration specifier
        const { specifiers } = node;
        // Iterate over the corresponding declarator
        const importDeclarations = specifiers.map((specifier, index) = > {
          // Get the original imported module
          const moduleName = specifier.imported.name;
          // Get the import renaming
          const localIdentifier = specifier.local;
          return generateImportStatement(moduleName, localIdentifier);
        });
        if (importDeclarations.length === 1) {
          // If there is only one statement
          nodePath.replaceWith(importDeclarations[0]);
        } else {
          // Multiple declaration substitutionsnodePath.replaceWithMultiple(importDeclarations); ,}}}};// Check whether the import is a fixed match library
  function checkedLibraryName(nodePath) {
    const { node } = nodePath;
    returnnode.source.value ! == libraryName; }// Check if the statement has a default import
  function checkedDefaultImport(nodePath) {
    const { node } = nodePath;
    const { specifiers } = node;
    return specifiers.some((specifier) = >
      t.isImportDefaultSpecifier(specifier)
    );
  }

  // Generate export statements to replace each import with a new single path default export statement
  function generateImportStatement(moduleName, localIdentifier) {
    return t.importDeclaration(
      [t.ImportDefaultSpecifier(localIdentifier)],
      t.StringLiteral(`${libraryName}/${moduleName}`)); }}module.exports = babelPluginImport;
Copy the code

At this point, one of our most basic Tree Shaking Babel plugins is implemented.

You’ll notice it’s only 60 lines of code, but it’s got everything. The development process and core ideas of a Babel plug-in I believe that after mastering the development ideas of this plug-in, you can do it easily for other similar needs.

Let’s run our original code:

Done!!

There are many features that can be optimized for this Babel Plugin.

Such as

  • Support for CSS/SCSS/LESS styles in component libraries is introduced.

  • The path in the component library supports dynamic parameter passing.

  • .

These details will be improved in the next commit, and you can see the code address here if you are interested. (github.com/19Qingfeng/…).

Written in the end

That concludes our talk about Tree Shaking with Babel Plguin.

The Plugin example in this article is just an easy to use and simple explanation 🌰, which I personally think is more practical. I hope that when you encounter some difficult problems in business/tools, you can also try to think of different ways to solve the problem from the perspective of customization of Babel Plugin.

In the future code warehouse, I will expand more Babel Learn Feature to summarize and share with every student struggling in the front end.

If you have any questions can also be exchanged in the comments area, I will be the first time to reply 😂~