按需加载原理分析

当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github,欢迎 Watch 和 Star。

简介

了解 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属性

6

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

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值