简介

理解 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 = 1let b = 1

很简略吧,咱们须要将其转换为:

const aa = 1const bb = 1

接下来进行插件编写

babel-plugin-lyn/index.js

根本构造
// 函数会有一个 babelTypes 参数,咱们构造出外面的 types// 代码中须要用到它的一些办法,办法具体什么意思能够参考 // https://babeljs.io/docs/en/next/babel-types.htmlmodule.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.htmlmodule.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 2var 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。