关于前端:按需加载原理分析

3次阅读

共计 17732 个字符,预计需要花费 45 分钟才能阅读完成。

简介

理解 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
// 代码中须要用到它的一些办法,办法具体什么意思能够参考 
// https://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
// 代码中须要用到它的一些办法,办法具体什么意思能够参考 
// https://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 属性

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 目录

看代码是随时对照 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,提供了一系列办法供应用,官网地址:https://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 数组

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

链接

  • 组件库专栏
  • AST Explorer
  • @babel/types
  • @babel/helper-module-imports

感激各位的:点赞 珍藏 评论,咱们下期见。


当学习成为了习惯,常识也就变成了常识,扫码关注微信公众号,独特学习、提高。文章已收录到 github,欢送 Watch 和 Star。

正文完
 0