按需加载原理分析

简介

了解 Babel 插件基本知识,理解按需加载的内部原理,再也不怕面试官问我按需加载的实现原理了。


import { Button } from 'element-ui'

怎么就变成了

var Button = require('element-ui/lib/button.js')
require('element-ui/lib/theme-chalk/button.css')

为了找到答案,分两步来进行,这也是自己学习的过程:

  1. babel 插件入门,编写 babel-plugin-lyn 插件

  2. 解读 babel-plugin-component 源码,从源码中找到答案

babel 插件入门

这一步我们去编写一个babel-plugin-lyn插件,这一步要达到的目的是:

  • 理解babel插件做了什么

  • 学会分析AST语法树

  • 学会使用基本的API

  • 能编写一个简单的插件,做基本的代码转换

有了以上基础我们就可以尝试去阅读babel-plugin-component源码,从源码中找到我们想要的答案

简单介绍

Babel是一个JavaScript编译器,是一个从源码到源码的转换编译器,你为Babel提供一些JavaScript代码,Babel按照要求更改这些代码,然后返回给你新生成的代码。

代码转换(更改)的过程中是借助AST (抽象语法树)来完成的,通过改变AST节点信息来达到转换代码的目的,到这里其实也就可以简单回答出我们在目标中提到的代码转化是怎么完成的 ?,其实就是Babel读取我们的源代码,将其转换为AST,分析AST,更改AST的某些节点信息,然后生成新的代码,就完成了转换过程,而具体是怎么更改节点信息,就需要去babel-plugin-component源码中找答案了

Babel的世界中,我们要更改某个节点的时候,就需要去访问(拦截)该节点,这里采用了访问者模式访问者是一个用于AST遍历的跨语言的模式,加单的说就是定义了一个对象,用于在树状结构获取具体节点的的方法,这些节点其实就是AST节点,可以在 AST Explorer 中查看代码的AST信息,这个我们在编写代码的时候会多次用到

babel-plugin-lyn

接下来编写一个自己的插件

初始化项目目录

mkdir babel-plugin && cd babel-plugin && npm init -y

新建插件目录

在项目的node_modules目录新建一个文件夹,作为自己的插件目录

mkdir -p node_modules/babel-plugin-lyn

在插件目录新建 index.js

touch index.js

创建需要被处理的 JS 代码

在项目根目录下创建 index.js,编写如下代码

let a = 1
let b = 1

很简单吧,我们需要将其转换为:

const aa = 1
const bb = 1

接下来进行插件编写

babel-plugin-lyn/index.js

基本结构
// 函数会有一个 babelTypes 参数,我们结构出里面的 types
// 代码中需要用到它的一些方法,方法具体什么意思可以参考 
// //babeljs.io/docs/en/next/babel-types.html
module.exports = function ({ types: bts }) {
  // 返回一个有 visitor 的对象,这是规定,然后在 visitor 中编写获取各个节点的方法
  return {
    visitor: {
        ...
    }
  }
}

分析源代码

有了插件的基本结构之后,接下来我们需要分析我们的代码,它在AST中长什么样

AST Explorer

如下图所示:

用鼠标点击需要更改的地方,比如我们要改变量名,则点击以后会看到右侧的AST tree展开并高亮了一部分,高亮的这部分就是我们要改的变量aAST节点,我们知道它是一个Identifier类型的节点,所以我们就在visitor中编写一个Identifier方法

module.exports = function ({ types: bts }) {
    return {
        visitor: {
            /**
             * 负责处理所有节点类型为 Identifier 的 AST 节点
             * @param {*} path AST 节点的路径信息,可以简单理解为里面放了 AST 节点的各种信息
             * @param {*} state 有一个很重要的 state.opts,是 .babelrc 中的配置项
            */
            Identifier (path, state) {
                // 节点信息
                const node = path.node
                // 从节点信息中拿到 name 属性,即 a 和 b
                const name = node.name
                // 如果配置项中存在 name 属性,则将 path.node.name 的值替换为配置项中的值
                if (state.opts[name]) {
                    path.node.name = state.opts[name]
                }
            }
        }
    }
}

这里我们用到了插件的配置信息,接下来我们在.babelrc中编写插件的配置信息

.babelrc
{
  "plugins": [
    [
      "lyn",
      {
        "a": "aa",
        "b": "bb"
      }
    ]
  ]
}

这个配置项是不是很熟悉?和babel-plugin-component的及其相似,lyn表示 babel 插件的名称,后面的对象就是我们的配置项

输出结果
首先安装 babel-cli

这里有一点需要注意,在安装 babel-cli 之前,把我们编写的插件备份,不然执行下面的安装时,我们的插件目录会被删除,原因没有深究,应该是我们的插件不是一个有效的 npm 包,所以会被清除掉

npm i babel-cli -D
编译
npx babel index.js

得到如下输出:

let aa = 1;
let bb = 1;

说明我们的插件已经生效,且刚才的思路是没问题的,转译代码其实就是通过更改 AST 节点的信息即可

let -> const

我们刚才已经完成了变量的转译,接下来再把let关键字变成const

按照刚才的方法,我们需要更改关键字let,将光标移动到let上,发现AST Tree高亮部分变了,可以看到letAST节点类型为VariableDeclaration,且我们要改的就是kind属性,好了,开始写代码

module.exports = function ({ types: bts }) {
    return {
        visitor: {
            Identifier (path, state) {
                ...
            },
            // 处理变量声明关键字
            VariableDeclaration (path, state) {
                // 这次就没从配置文件读了,来个简单的,直接改
                path.node.kind = 'const'
            }
        }
    }
}
编译
npx babel index.js

得到如下输出:

const aa = 1;
const bb = 1;

到这里我们第一阶段的入门就结束了,是不是感觉很简单??是的,这个入门示例真的很简单,但是真的编写一个可用于业务Babel插件以及其中的涉及到的AST编译原理是非常复杂的。但是这个入门示例已经可以支持我们去分析babel-plugin-component插件的源码原理了。

完整代码
// 函数会有一个 babelTypes 参数,我们结构出里面的 types
// 代码中需要用到它的一些方法,方法具体什么意思可以参考 
// //babeljs.io/docs/en/next/babel-types.html
module.exports = function ({ types: bts }) {
  // 返回一个有 visitor 的对象,这是规定,然后在 visitor 中编写获取各个节点的方法
  return {
    visitor: {
      /**
       * 负责处理所有节点类型为 Identifier 的 AST 节点
       * @param {*} path AST 节点的路径信息,可以简单理解为里面放了 AST 节点的各种信息
       * @param {*} state 有一个很重要的 state.opts,是 .babelrc 中的配置项
       */
      Identifier (path, state) {
        // 节点信息
        const node = path.node
        // 从节点信息中拿到 name 属性,即 a 和 b
        const name = node.name
        // 如果配置项中存在 name 属性,则将 path.node.name 的值替换为配置项中的值
        if (state.opts[name]) {
          path.node.name = state.opts[name]
        }
      },
      // 处理变量声明关键字
      VariableDeclaration (path, state) {
        // 这次就没从配置文件读了,来个简单的,直接改
        path.node.kind = 'const'
      }
    }
  }
}

babel-plugin-component 源码分析

目标分析

在进行源码阅读之前我们先分析一下我们的目标,带着目标去阅读,效果会更好

源代码

// 全局引入
import ElementUI from 'element-ui'
Vue.use(ElementUI)
// 按需引入
import { Button, Checkbox } from 'element-ui'
Vue.use(Button)
Vue.component(Checkbox.name, Checkbox)

上面就是我们使用element-ui组件库的两种方式,全局引入和按需引入

目标代码

// 全局引入
var ElementUI = require('element-ui/lib')
require('element-ui/lib/theme-chalk/index.css')
Vue.use(ElementUI)
// 按需引入
var Button = require('element-ui/lib/button.js')
require('element-ui/lib/theme-chalk/button.css')
var Checkbox = require('element-ui/lib/checkbox.js')
require('element-ui/lib/theme-chalk/checkbox.css')
Vue.use(Button)
Vue.component(Checkbox.name, Checkbox)

以上就是源代码和转译后的目标代码,我们可以将他们分别复制到 AST Explorer 中查看 AST Tree的信息,进行分析

全局引入

从上图中可以看出,这两条语句总共是由两种类型的节点组成,import对应的ImportDeclaration的节点,Vue.use(ElementUI)对应于ExpressionStatement类型的节点

可以看到import ElementUI from 'element-ui'对应到AST中,from后面的element-ui对应于source.value,且节点类型为StringLiteral

import ElementUI from 'element-ui'中的ElementUI对应于ImportDefaultSpecifier类型的节点,是个默认导入,变量对应于Indentifier节点的name属性

6

Vue.use(ElementUI)是个声明式的语句,对应于ExpressionStatement的节点,可以看到参数ElementUI放到了arguments部分

按需引入

可以看到body有三个子节点,一个ImportDeclaration,两个ExpressionStatement,和我们的代码一一对应

import语句中对于from后面的部分上面的全局是一样的,都是在source中,是个Literal类型的节点

可以看到import后面的内容变了,上面的全局引入是一个ImportDefaultDeclaration类型的节点,这里的按需加载是一个ImportDeclaration节点,且引入的内容放在specifiers对象中,每个组件(Button、Checkbox)是一个ImportSpecifier,里面定义了importedlocalIdentifier,而我们的变量名称(Button、Checkbox)放在name属性上

剩下的Vue.use(Button)Vue.component(Checkbox.name, Checkbox)和上面全局引入类似,有一点区别是Vue.component(Checkbox.name, Checkbox)arguments有两个元素

经过刚开始的基础入门以及上面对于AST的一通分析,我们其实已经大概可以猜出来从源代码目标代码这个转换过程中发生了些什么,其实就是在visitor对象上设置响应的方法(节点类型),然后去处理符合要求的节点,将节点上对应的属性更改为目标代码上响应的值,把源代码目标代码都复制到 AST Explorer 中查看,就会发现,相应节点之间的差异(改动)就是babel-plugin-component做的事情,接下来我们进入源码寻找答案。

源码分析

直接在刚才的项目中执行

npm i babel-plugin-component -D

安装 babel-plugin-component,安装完成,在node_modules目录找babel-plugin-component目录

image-20220207175041085

看代码是随时对照AST Explorer和打log确认

.babelrc

{
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

入口,index.js

// 默认就是用于element-ui组件库的按需加载插件
module.exports = require('./core')('element-ui');

核心,core.js

源码阅读提示

  • 清楚读源码的目的是什么,为了解决什么样的问题
  • 一定要有相关的基础知识,比如上面的 babel 入门,知道入口位置在 visitor,以及在 visitor 中找那些方法去读
  • 读过程中一定要勤动手,写注释,打 log,这样有助于提高思路
  • 阅读这篇源码,一定要会用 AST Explorer 分析和对比我们的源代码 和 目标代码
  • 下面的源代码几乎每行都加了注释,大家按照步骤自己下一套源码,可以对比着看,一遍看不懂,看两遍,书读三遍其义自现,真的,当然,读的过程中有不懂的地方需要查一查
/**
 * 判断 obj 的类型
 * @param {*} obj 
 */
function _typeof(obj) { 
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { 
    _typeof = function _typeof(obj) { 
      return typeof obj; 
    }; 
  } else { 
    _typeof = function _typeof(obj) { 
      return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 
    }; 
  } 
  return _typeof(obj); 
}

// 提供了一些方法,负责生成 import 节点
var _require = require('@babel/helper-module-imports'),
  addSideEffect = _require.addSideEffect,
  addDefault = _require.addDefault;

// node.js 的内置模块,处理 路径信息
var resolve = require('path').resolve;

// node.js 内置模块,判断文件是否存在
var isExist = require('fs').existsSync;

// 缓存变量, cache[libraryName] = 1 or 2
var cache = {};
// 缓存样式库的样式路径,cachePath[libraryName] = ''
var cachePath = {};
// importAll['element-ui/lib'] = true,说明存在默认导入
var importAll = {};

module.exports = function core(defaultLibraryName) {
  return function (_ref) {
    // babelTypes,提供了一系列方法供使用,官网地址://babeljs.io/docs/en/next/babel-types.html
    var types = _ref.types;
    // 存储所有的 ImportSpecifier,即按需引入的组件,specified = { Button: 'Button', Checkbox: 'Checkbox' }
    var specified;
    // 存储所有全局引入的库,libraryObjs = { ElementUI: 'element-ui' }
    var libraryObjs;
    // 存储已经引入(处理)的方法(组件),
    // selectedMethods = {
    //   ElementUI: { type: 'Identifier', name: '_ElementUI' },
    //   Button: { type: 'Identifier', name: '_Button' },
    //   Checkbox: { type: 'Identifier', name: '_Checkbox' }
    // }
    var selectedMethods;
    // 引入的模块和库之间的对应关系,moduleArr = { Button: 'element-ui', Checkbox: 'element-ui' }
    var moduleArr;

    // 将驼峰命名转换为连字符命名
    function parseName(_str, camel2Dash) {
      if (!camel2Dash) {
        return _str;
      }

      var str = _str[0].toLowerCase() + _str.substr(1);

      return str.replace(/([A-Z])/g, function ($1) {
        return "-".concat($1.toLowerCase());
      });
    }

    /**
     * 该方法负责生成一些 AST 节点,这些节点的信息是根据一堆配置项来的,这对配置项就是在告诉 AST 节点每个组件的路径信息,
     * 比如 'element-ui/lib/button.js' 和 'element-ui/lib/theme-chalk/button.css'
     * @param {*} methodName Button、element-ui
     * @param {*} file 一拖不想看的对象信息
     * @param {*} opts .babelrc 配置项
     */
    function importMethod(methodName, file, opts) {
      // 如果 selectedMethods 中没有 Butotn、element-ui 则进入 if ,否则直接 return selectedMethods[methodName],说明该方法(组件)已经被处理过了
      if (!selectedMethods[methodName]) {
        var options;
        var path;

        // 不用管
        if (Array.isArray(opts)) {
          options = opts.find(function (option) {
            return moduleArr[methodName] === option.libraryName || libraryObjs[methodName] === option.libraryName;
          }); // eslint-disable-line
        }

        /**
         * 以下是一堆配置项
         */
        // 传递进来的配置
        options = options || opts;
        var _options = options,
          // 配置的 libDir
          _options$libDir = _options.libDir,
          // 没有配置,就默认为 lib, /element-ui/lib/button.js 中的 lib 就是这么来的
          libDir = _options$libDir === void 0 ? 'lib' : _options$libDir,
          // 组件库,element-ui
          _options$libraryName = _options.libraryName,
          // 组件库名称
          libraryName = _options$libraryName === void 0 ? defaultLibraryName : _options$libraryName,
          // 样式,boolean 类型,这里是 undefined
          _options$style = _options.style,
          // style 默认是 true,也可以由用户提供,在用户没有提供 styleLibraryName 选项是起作用
          style = _options$style === void 0 ? true : _options$style,
          // undefiend
          styleLibrary = _options.styleLibrary,
          // undefined
          _options$root = _options.root,
          // ''
          root = _options$root === void 0 ? '' : _options$root,
          _options$camel2Dash = _options.camel2Dash,
          camel2Dash = _options$camel2Dash === void 0 ? true : _options$camel2Dash;
        // 配置项中的,'theme-chalk'
        var styleLibraryName = options.styleLibraryName;
        // ''
        var _root = root;
        var isBaseStyle = true;
        var modulePathTpl;
        var styleRoot;
        var mixin = false;
        // 后缀 xx.css
        var ext = options.ext || '.css';

        if (root) {
          _root = "/".concat(root);
        }

        if (libraryObjs[methodName]) {
          // 默认导入 ElementUI, path = 'element-ui/lib'
          path = "".concat(libraryName, "/").concat(libDir).concat(_root);

          if (!_root) {
            // 默认导入的情况下,记录在 importAll 中标记 path 为 true
            importAll[path] = true;
          }
        } else {
          // 按需引入,path = 'element-ui/lib/button'
          path = "".concat(libraryName, "/").concat(libDir, "/").concat(parseName(methodName, camel2Dash));
        }

        // 'element-ui/lib/button'
        var _path = path;
        /**
         * selectedMethods['Button'] = { type: Identifier, name: '_Button' }
         * addDefault 就负责添加刚才在 visitor.CallExpreesion 那说的那堆东西,
         * 这里主要负责 var Button = require('element-ui/lib/button.js'),
         * 这是猜的,主要是没找到这方面的文档介绍
         */
        selectedMethods[methodName] = addDefault(file.path, path, {
          nameHint: methodName
        });

        /**
         * 接下来是处理样式
         */
        if (styleLibrary && _typeof(styleLibrary) === 'object') {
          styleLibraryName = styleLibrary.name;
          isBaseStyle = styleLibrary.base;
          modulePathTpl = styleLibrary.path;
          mixin = styleLibrary.mixin;
          styleRoot = styleLibrary.root;
        }

        // styleLibraryName = 'theme-chalk',如果配置该选项,就采用默认的方式,进入 else 查看
        if (styleLibraryName) {
          // 缓存样式库路径
          if (!cachePath[libraryName]) {
            var themeName = styleLibraryName.replace(/^~/, '');
            // cachePath['element-ui'] = 'element-ui/lib/theme-chalk'
            cachePath[libraryName] = styleLibraryName.indexOf('~') === 0 ? resolve(process.cwd(), themeName) : "".concat(libraryName, "/").concat(libDir, "/").concat(themeName);
          }

          if (libraryObjs[methodName]) {
            // 默认导入
            /* istanbul ingore next */
            if (cache[libraryName] === 2) {
              // 提示信息,意思是说如果你项目既存在默认导入,又存在按需加载,则要保证默认导入在按需加载的前面
              throw Error('[babel-plugin-component] If you are using both' + 'on-demand and importing all, make sure to invoke the' + ' importing all first.');
            }

            // 默认导出的样式库路径:path = 'element-ui/lib/theme-chalk/index.css'
            if (styleRoot) {
              path = "".concat(cachePath[libraryName]).concat(styleRoot).concat(ext);
            } else {
              path = "".concat(cachePath[libraryName]).concat(_root || '/index').concat(ext);
            }

            cache[libraryName] = 1;
          } else {
            // 按需引入,这里不等于 1 就是存在默认导入 + 按需引入的情况,基本上没人会这么用
            if (cache[libraryName] !== 1) {
              /* if set styleLibrary.path(format: [module]/module.css) */
              var parsedMethodName = parseName(methodName, camel2Dash);

              if (modulePathTpl) {
                var modulePath = modulePathTpl.replace(/\[module]/ig, parsedMethodName);
                path = "".concat(cachePath[libraryName], "/").concat(modulePath);
              } else {
                path = "".concat(cachePath[libraryName], "/").concat(parsedMethodName).concat(ext);
              }

              if (mixin && !isExist(path)) {
                path = style === true ? "".concat(_path, "/style").concat(ext) : "".concat(_path, "/").concat(style);
              }

              if (isBaseStyle) {
                addSideEffect(file.path, "".concat(cachePath[libraryName], "/base").concat(ext));
              }

              cache[libraryName] = 2;
            }
          }

          // 添加样式导入,require('elememt-ui/lib/theme-chalk/button.css'),这里也是猜的,说实话,addDefault 方法看的有点懵,要是有文档就好了
          addDefault(file.path, path, {
            nameHint: methodName
          });
        } else {
          if (style === true) {
            // '/element-ui/style.css,这里是默认的,ext 可以由用户提供,也是用默认的
            addSideEffect(file.path, "".concat(path, "/style").concat(ext));
          } else if (style) {
            // 'element-ui/xxx,这里的 style 是用户提供的 
            addSideEffect(file.path, "".concat(path, "/").concat(style));
          }
        }
      }

      return selectedMethods[methodName];
    }

    function buildExpressionHandler(node, props, path, state) {
      var file = path && path.hub && path.hub.file || state && state.file;
      props.forEach(function (prop) {
        if (!types.isIdentifier(node[prop])) return;

        if (specified[node[prop].name]) {
          node[prop] = importMethod(node[prop].name, file, state.opts); // eslint-disable-line
        }
      });
    }

    function buildDeclaratorHandler(node, prop, path, state) {
      var file = path && path.hub && path.hub.file || state && state.file;
      if (!types.isIdentifier(node[prop])) return;

      if (specified[node[prop].name]) {
        node[prop] = importMethod(node[prop].name, file, state.opts); // eslint-disable-line
      }
    }

    return {
      // 程序的整个入口,熟悉的 visitor
      visitor: {
        // 负责处理 AST 中 Program 类型的节点
        Program: function Program() {
          // 将之前定义的几个变量初始化为没有原型链的对象
          specified = Object.create(null);
          libraryObjs = Object.create(null);
          selectedMethods = Object.create(null);
          moduleArr = Object.create(null);
        },
        // 处理 ImportDeclaration 节点
        ImportDeclaration: function ImportDeclaration(path, _ref2) {
          // .babelrc 中的插件配置项
          var opts = _ref2.opts;
          // import xx from 'xx', ImportDeclaration 节点
          var node = path.node;
          // import xx from 'element-ui',这里的 node.source.value 存储的就是 库名称
          var value = node.source.value;
          var result = {};

          // 可以不用管,如果配置项是个数组,从数组中找到该库的配置项
          if (Array.isArray(opts)) {
            result = opts.find(function (option) {
              return option.libraryName === value;
            }) || {};
          }

          // 库名称,比如 element-ui
          var libraryName = result.libraryName || opts.libraryName || defaultLibraryName;

          // 如果当前 import 的库就是我们需要处理的库,则进入
          if (value === libraryName) {
            // 遍历node.specifiers,里面放了多个ImportSpecifier,每个都是我们要引入的组件(方法)
            node.specifiers.forEach(function (spec) {
              // ImportSpecifer 是按需引入,还有另外的一个默认导入,ImportDefaultSpecifier,比如:ElementUI
              if (types.isImportSpecifier(spec)) {
                // 设置按需引入的组件, 比如specfied['Button'] = 'Button'
                specified[spec.local.name] = spec.imported.name;
                // 记录当前组件是从哪个库引入的,比如 moduleArr['Button'] = 'element-ui'
                moduleArr[spec.imported.name] = value;
              } else {
                // 默认导入,libraryObjs['ElementUI'] = 'element-ui'
                libraryObjs[spec.local.name] = value;
              }
            });

            // 不是全局引入就删掉该节点,意思是删掉所有的按需引入,这个会在 importMethod 方法中设置
            if (!importAll[value]) {
              path.remove();
            }
          }
        },
        /**
         * 这里很重要,我们会发现在使用按需加载时,如果你只是import引入,但是没有使用,比如Vue.use(Button),则一样不会打包,所以这里就是来
         * 处理这种情况的,只有你引入的包实际使用了,才会真的import,要不然刚才删了就没有然后了,就不会在 node 上添加各种 arguments 了,比如:
         * {
         *   type: 'CallExpression',
         *   callee: { type: 'Identifier', name: 'require' },
         *   arguments: [ { type: 'StringLiteral', value: 'element-ui/lib' } ]
         * }
         * {
         *   type: 'CallExpression',
         *   callee: { type: 'Identifier', name: 'require' },
         *   arguments: [
         *    {
         *      type: 'StringLiteral',
         *      value: 'element-ui/lib/chalk-theme/index.css'
         *    }
         *   ]
         * }
         * {
         *    type: 'CallExpression',
         *    callee: { type: 'Identifier', name: 'require' },
         *    arguments: [ { type: 'StringLiteral', value: 'element-ui/lib/button' } ]
         * }
         * 以上这些通过打log可以查看,这个格式很重要,因为有了这部分数据,我们就知道:
         * import {Button} from 'element-ui' 为什么能
         * 得到 var Button = require('element-ui/lib/button.js')
         * 以及 require('element-ui/lib/theme-chalk/button.css')
         *
         * @param {*} path 
         * @param {*} state 
         */
        CallExpression: function CallExpression(path, state) {
          // Vue.use(Button),CallExpression 节点
          var node = path.node;
          // 很大的一拖对象,不想看(不用看,费头发)
          var file = path && path.hub && path.hub.file || state && state.file;
          // callee 的 name 属性,我们这里不涉及该属性,类似ElementUI(ok)这种语法会有该属性,node.callee.name 就是 ElementUI
          var name = node.callee.name;

          console.log('import method 处理前的 node:', node)
          // 判断 node.callee 是否属于 Identifier,我们这里不是,我们的是一个 MemberExpression
          if (types.isIdentifier(node.callee)) {
            if (specified[name]) {
              node.callee = importMethod(specified[name], file, state.opts);
            }
          } else {
            // 解析 node.arguments 数组,每个元素都是一个 Identifier,Vue.use或者Vue.component的参数
            node.arguments = node.arguments.map(function (arg) {
              // 参数名称
              var argName = arg.name;

              // 1、这里会生成一个新的 Identifier,并更改 AST节点的属性值
              // 2、按需引入还是默认导入是在 ImportDeclaration 中决定的
              if (specified[argName]) {
                // 按需引入,比如:{ type: "Identifier", name: "_Button" },这是 AST 结构的 JSON 对象表示形式
                return importMethod(specified[argName], file, state.opts);
              } else if (libraryObjs[argName]) {
                // 默认导入,{ type: "Identifier", name: "_ElementUI" }
                return importMethod(argName, file, state.opts);
              }

              return arg;
            });
          }
          console.log('import method 处理后的 node:', node)
        },
        /**
         * 后面几个不用太关注,在这里不涉及,看字面量就可以明白在做什么 
         */
        // 处理 MemberExpression,更改 node.object 对象
        MemberExpression: function MemberExpression(path, state) {
          var node = path.node;
          var file = path && path.hub && path.hub.file || state && state.file;

          if (libraryObjs[node.object.name] || specified[node.object.name]) {
            node.object = importMethod(node.object.name, file, state.opts);
          }
        },
        // 处理赋值表达式
        AssignmentExpression: function AssignmentExpression(path, _ref3) {
          var opts = _ref3.opts;

          if (!path.hub) {
            return;
          }

          var node = path.node;
          var file = path.hub.file;
          if (node.operator !== '=') return;

          if (libraryObjs[node.right.name] || specified[node.right.name]) {
            node.right = importMethod(node.right.name, file, opts);
          }
        },
        // 数组表达式
        ArrayExpression: function ArrayExpression(path, _ref4) {
          var opts = _ref4.opts;

          if (!path.hub) {
            return;
          }

          var elements = path.node.elements;
          var file = path.hub.file;
          elements.forEach(function (item, key) {
            if (item && (libraryObjs[item.name] || specified[item.name])) {
              elements[key] = importMethod(item.name, file, opts);
            }
          });
        },
        // 属性
        Property: function Property(path, state) {
          var node = path.node;
          buildDeclaratorHandler(node, 'value', path, state);
        },
        // 变量声明
        VariableDeclarator: function VariableDeclarator(path, state) {
          var node = path.node;
          buildDeclaratorHandler(node, 'init', path, state);
        },
        // 逻辑表达式
        LogicalExpression: function LogicalExpression(path, state) {
          var node = path.node;
          buildExpressionHandler(node, ['left', 'right'], path, state);
        },
        // 条件表达式
        ConditionalExpression: function ConditionalExpression(path, state) {
          var node = path.node;
          buildExpressionHandler(node, ['test', 'consequent', 'alternate'], path, state);
        },
        // if 语句
        IfStatement: function IfStatement(path, state) {
          var node = path.node;
          buildExpressionHandler(node, ['test'], path, state);
          buildExpressionHandler(node.test, ['left', 'right'], path, state);
        }
      }
    };
  };
};

总结

通过阅读源码以及打log的方式,我们得到了如下信息:

{
    type: 'CallExpression',
    callee: { type: 'Identifier', name: 'require' },
    arguments: [ { type: 'StringLiteral', value: 'element-ui/lib' } ]
}
{
    type: 'CallExpression',
    callee: { type: 'Identifier', name: 'require' },
    arguments: [
        {
          type: 'StringLiteral',
          value: 'element-ui/lib/chalk-theme/index.css'
        }
    ]
}
{
    type: 'CallExpression',
    callee: { type: 'Identifier', name: 'require' },
    arguments: [ { type: 'StringLiteral', value: 'element-ui/lib/button' } ]
}

这其实就是经过变化后的AST的部分信息,通过对比目标代码在AST Tree中的显示会发现,结果是一致的,也就是说通过以上AST信息就可以生成我们需要的目标代码

目标代码中的require关键字就是calleerequire函数中的参数就是arguments数组

以上就是 按需加载原理分析 的所有内容。

链接

感谢各位的:点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识,扫码关注微信公众号,共同学习、进步。文章已收录到 github,欢迎 Watch 和 Star。

微信公众号

Tags: