浅析babel-plugin-component

10,244 阅读4分钟

vue + element-ui 使用 babel-plugin-component 来实现按需加载组件及样式。

一、官网按需引入示例

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

首先,安装 babel-plugin-component:

npm install babel-plugin-component -D

然后,将 .babelrc 修改为:

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

接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

import Vue from 'vue'
import { Button, Select } from 'element-ui'
import App from './App.vue'

Vue.component(Button.name, Button)
Vue.component(Select.name, Select)
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
})

二、babel 编译过程概述

使用 @babel/cli 与 @babel/core 编译文件的主要过程

  • 加载配置文件

    • 尝试加载 babel.config.js、babel.config.cjs、babel.config.mjs、babel.config.json文件
    • 加载 package.json 文件
    • 尝试加载 .bebelrc、.babelrc.js、.babelrc.cjs、.babelrc.mjs、.babelrc.json 文件
    • 合并参数
  • 加载 plugins 与 presets,分别遍历他们(plugins 每一项会调用它的回调,返回包含 visitor 的对象)

  • 解析部分

    • 以 utf8 格式读入口文件得到代码
    • 之后解析生成 ast
  • 遍历与转换部分

    • 遍历插件数组,生成最后的访问者(visitor)对象
    • 开始遍历节点,碰到感兴趣的节点就调用回调
  • 生成部分

    • 遍历 ast,将得到的代码保存在数组中,最后拼接起来

三、babel-plugin-component

通过一个简单的例子看一下 babel-plugin-component 在编译过程中做了哪些事情

1.项目结构

目录结构

├── index.js
├── .babelrc
├── package-lock.json
└── package.json

package.json

{
  
  ...
  
  "scripts": {
    "build": "babel ./index.js --out-dir lib"
  },
  
  ...
  
  "dependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "babel-plugin-component": "^1.1.1"
  }
}

.babelrc 文件

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

index.js 文件

import Vue from 'vue'
import { Button } from 'element-ui'

Vue.component(Button.name, Button)
Vue.component(Select.name, Select)

编译生成文件

import _Button2 from "element-ui/lib/theme-chalk/button.css";
import "element-ui/lib/theme-chalk/base.css";
import _Button from "element-ui/lib/button";
import Vue from 'vue';
Vue.component(_Button.name, _Button);
Vue.component(Select.name, Select);

2.插件做了什么

在 babel 遍历 ast 的时候,这个插件主要关注了 ImportDeclaration 与 CallExpression 与 Program

				Program: function Program() {// 在这里做一些初始化操作,用来保存信息
					specified = Object.create(null);
          libraryObjs = Object.create(null);
          selectedMethods = Object.create(null);
          moduleArr = Object.create(null);
        },       
				ImportDeclaration: function ImportDeclaration(path, _ref2) {
          var opts = _ref2.opts;// 获得配置文件中的配置 {libraryName: "element-ui, "styleLibraryName: "theme-chalk"}
          
          var node = path.node;// 获得当前的节点
          
          var value = node.source.value;// 当前节点的值(import 引入 vue,这里就是 vue,引入 element-ui 这里就是element-ui)
          
          ...

          var libraryName = result.libraryName || opts.libraryName || defaultLibraryName;// 拿到名称 一般是 element-ui

          if (value === libraryName) {// 如果是 element-ui
            node.specifiers.forEach(function (spec) {
              if (types.isImportSpecifier(spec)) {
                specified[spec.local.name] = spec.imported.name;// 存起来 { Button: "Button" }
                moduleArr[spec.imported.name] = value;// { Button: "element-ui" }
              } else {
                libraryObjs[spec.local.name] = value;
              }
            });

            if (!importAll[value]) {
              path.remove();// 移除节点
            }
          }
        },
          
        CallExpression: function CallExpression(path, state) {
          var node = path.node;
          var file = path && path.hub && path.hub.file || state && state.file;
          var name = node.callee.name;

          if (types.isIdentifier(node.callee)) {
            if (specified[name]) {
              node.callee = importMethod(specified[name], file, state.opts);
            }
          } else {
            node.arguments = node.arguments.map(function (arg) {
              var argName = arg.name;

              if (specified[argName]) {// 找到上面存起来的 Button
                return importMethod(specified[argName], file, state.opts);
              } else if (libraryObjs[argName]) {
                return importMethod(argName, file, state.opts);
              }

              return arg;
            });
          }
        },

importMethod 方法

    function importMethod(methodName, file, opts) {
      if (!selectedMethods[methodName]) {
        var options;
        var path;

        ...

        options = options || opts;// 配置文件中的参数 {libraryName: "element-ui", styleLibraryName: "theme-chalk"}
        var _options = options,
            _options$libDir = _options.libDir,//这是组件所在根目录下的路径element-ui/lib/
            libDir = _options$libDir === void 0 ? 'lib' : _options$libDir,
            _options$libraryName = _options.libraryName,//ui库的名字,就是elementui
            libraryName = _options$libraryName === void 0 ? defaultLibraryName : _options$libraryName,
            _options$style = _options.style,
            style = _options$style === void 0 ? true : _options$style,
            styleLibrary = _options.styleLibrary,//这是引入组件时,所需要引入对应组件样式的配置对象
            _options$root = _options.root,
            root = _options$root === void 0 ? '' : _options$root,
            _options$camel2Dash = _options.camel2Dash,
            camel2Dash = _options$camel2Dash === void 0 ? true : _options$camel2Dash;
        var styleLibraryName = options.styleLibraryName;//这是组件所需样式的路径,配置参数拿的,就是 theme-chalk
        var _root = root;
        var isBaseStyle = true;
        var modulePathTpl;
        var styleRoot;
        var mixin = false;
        var ext = options.ext || '.css';//加载样式的后缀,默认css

        ...

        if (libraryObjs[methodName]) {
          
          ...
          
        } else {
          path = "".concat(libraryName, "/").concat(libDir, "/").concat(parseName(methodName, camel2Dash));// 得到文件路径 element-ui/lib/button
        }

        var _path = path;
        selectedMethods[methodName] = addDefault(file.path, path, {
          nameHint: methodName
        });// 这个方法是用来创建 ImportDeclaration 节点的,引入的是 element-ui/lib/button

       ...

        if (styleLibraryName) {
          if (!cachePath[libraryName]) {
            var themeName = styleLibraryName.replace(/^~/, '');
            cachePath[libraryName] = styleLibraryName.indexOf('~') === 0 ? resolve(process.cwd(), themeName) : "".concat(libraryName, "/").concat(libDir, "/").concat(themeName);//得到样式的路径
          }

          if (libraryObjs[methodName]) {

          ...
            
          } else {
            if (cache[libraryName] !== 1) {

              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);// 得到完整的路径 element-ui/lib/theme-chalk/button.css
              }

              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));// 添加 ImportDeclaration 节点引入 base 样式
              }

              cache[libraryName] = 2;
            }
          }

          addDefault(file.path, path, {
            nameHint: methodName
          });// 添加 ImportDeclaration 节点引入组件样式
        } else {
          if (style === true) {
            addSideEffect(file.path, "".concat(path, "/style").concat(ext));
          } else if (style) {
            addSideEffect(file.path, "".concat(path, "/").concat(style));
          }
        }
      }
      return selectedMethods[methodName];
    }

babel-plugin-component 插件在遍历节点的时候做的事情

  • 找到引入 element-ui 的类型为 ImportDeclaration 节点,将感兴趣的值存在对象里(比如引入 button,就存起来),之后移除当前这个节点。
  • 在遍历到 CallExpression 类型节点的时候(假设使用了 button,就判断是否存在了上面的对象里),之后创建新的 ImportDeclaration 节点,用于之后加载对应的 js 与 css 文件。