简介
理解 Babel
插件基本知识,了解按需加载的外部原理,再也不怕面试官问我按需加载的实现原理了。
import {Button} from 'element-ui'
怎么就变成了
var Button = require('element-ui/lib/button.js')
require('element-ui/lib/theme-chalk/button.css')
为了找到答案,分两步来进行,这也是本人学习的过程:
- babel 插件入门,编写 babel-plugin-lyn 插件
- 解读
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
开展并高亮了一部分,高亮的这部分就是咱们要改的变量 a
的AST
节点,咱们晓得它是一个 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
高亮局部变了,能够看到 let
的AST
节点类型为 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
,外面定义了imported
和local
的 Identifier
,而咱们的变量名称(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
关键字就是 callee
,require
函数中的参数就是 arguments
数组
以上就是 按需加载原理剖析 的所有内容。
链接
- 组件库专栏
- AST Explorer
- @babel/types
- @babel/helper-module-imports
感激各位的:点赞 、 珍藏 和评论,咱们下期见。
当学习成为了习惯,常识也就变成了常识,扫码关注微信公众号,独特学习、提高。文章已收录到 github,欢送 Watch 和 Star。