[Основы фронтенд-инжиниринга — Babel] Простая реализация плагина babel-plugin-import

внешний интерфейс JavaScript

Статья, написанная несколько месяцев назад, до сих пор не опубликована в «Наггетс».Оригинальная ссылка

предисловие

обычно используетсяantd,elementКогда вы ждете библиотеку компонентов, вы будете использоватьBabelПлагин:babel-plugin-import, В этой статье кратко рассказывается о том, что делает этот плагин, на примерах и анализе исходного кода, а также реализуется минимальная используемая версия.

Адрес плагина:GitHub.com/Анта-дизайн/…

Введение в babel-plugin-import

Зачем: зачем вам этот плагин

antdа такжеelementЭти две библиотеки компонентов, посмотрите на их исходный код,index.jsОни следующие:

// antd
export { default as Button } from './button';
export { default as Table } from './table';
// element
import Button from '../packages/button/index.js';
import Table from '../packages/table/index.js';
export default {
  Button,
  Table,
};

antdа такжеelementна всем протяженииES6 Moduleизexportэкспортировать отдельные компоненты с именами.

Итак, мы можем пройтиES6изimport { } fromсинтаксис для импорта однокомпонентногоJSдокумент. Однако нам также необходимо вручную импортировать стили компонентов:

// antd
import 'antd/dist/antd.css';
// element
import 'element-ui/lib/theme-chalk/index.css';

если только одинButtonкомпонента, но вводятся все стили, что явно неразумно.

Конечно, вы сказали, что вы также можете использовать только один компонент, и вы также можете уменьшить размер кода:

import Button from 'antd/lib/button';
import 'antd/lib/button/style';

ПС: похожеantdБиблиотека компонентов предоставляетES ModuleПродукт сборки , напрямую черезimport {} fromтакже может быть в видеtree-shaking, это не в сегодняшней теме, поэтому не буду об этом говорить~

Да, в этом нет ничего плохого. Однако посмотрите, когда нам нужно несколько компонентов:

import { Affix, Avatar, Button, Rate } from 'antd';

import 'antd/lib/affix/style';
import 'antd/lib/avatar/style';
import 'antd/lib/button/style';
import 'antd/lib/rate/style';

Считаете ли вы такой код недостаточно элегантным? Если бы это был я, я бы даже хотел кого-нибудь ударить.

В это время следует подумать о том, как ввестиButtonПри автоматическом импорте файлов стилей.

Что: что делает этот плагин

Проще говоря,babel-plugin-importОн предназначен для решения вышеуказанной проблемы, реализации загрузки отдельных компонентов для библиотеки компонентов по требованию и автоматического введения ее стилей, таких как:

import { Button } from 'antd';

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
require('antd/lib/button/style');

Вам нужно заботиться только о том, какие компоненты нужно импортировать. Мне не нужно заботиться о внутренних стилях. Ничего, если вы поможете мне импортировать их автоматически.

Как: Как использовать этот плагин

Проще говоря, вам нужно заботиться о трех параметрах:

{
  "libraryName": "antd",     // 包名
  "libraryDirectory": "lib", // 目录,默认 lib
  "style": true,             // 是否引入 style
}

Прочее см. документацию:GitHub.com/Анта-дизайн/…

Анализ исходного кода babel-plugin-import

В основном смотреть наbabel-plugin-importкак загрузитьJavaScriptкод и стиль.

В качестве примера возьмем следующий код:

import { Button, Rate } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);

сбор зависимостей первого шага

babel-plubin-importБудет вImportDeclarationЛи будет всеspecifierсобрал.

Первый взглядastБар:

доступно из этогоImportDeclarationИз заявления извлечены несколько ключевых моментов:

  • source.value: antd
  • specifier.local.name: Button
  • specifier.local.name: Rate

Что нужно сделать тоже просто:

  1. importпакет неantd, то есть,libraryName
  2. ПучокButtonа такжеRateсобрал

Посмотрите на код:

ImportDeclaration(path, state) {
  const { node } = path;
  if (!node) return;
  // 代码里 import 的包名
  const { value } = node.source;
  // 配在插件 options 的包名
  const { libraryName } = this;
  // babel-type 工具函数
  const { types } = this;
  // 内部状态
  const pluginState = this.getPluginState(state);
  // 判断是不是需要使用该插件的包
  if (value === libraryName) {
    // node.specifiers 表示 import 了什么
    node.specifiers.forEach(spec => {
      // 判断是不是 ImportSpecifier 类型的节点,也就是是否是大括号的
      if (types.isImportSpecifier(spec)) {
        // 收集依赖
        // 也就是 pluginState.specified.Button = Button
        // local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
        // imported.name 是真实导出的变量名
        pluginState.specified[spec.local.name] = spec.imported.name;
      } else { 
        // ImportDefaultSpecifier 和 ImportNamespaceSpecifier
        pluginState.libraryObjs[spec.local.name] = true;
      }
    });
    pluginState.pathsToRemove.push(path);
  }
}

обращатьсяbabelпрошел всеImportDeclarationПосле того, как узлы типа собраны, собираются зависимости, и следующим шагом является их загрузка.

Второй шаг – определить, следует ли использовать

После сбора зависимостей должен определить, что этоimportИспользуется ли переменная, здесь речь идет о ситуации.

мы знаем,JSXв конце концов статьReact.createElement()реализовано:

ReactDOM.render(<Button>Hello</Button>);

      ↓ ↓ ↓ ↓ ↓ ↓

React.createElement(Button, null, "Hello");

Вот так,createElementПервый параметр — это то, что мы ищем, нам нужно определить, являются ли собранные зависимостиcreateElementиспользовать.

Анализировать эту линию кодаast, этот узел легко найти:

Посмотрите на код:

CallExpression(path, state) {
  const { node } = path;
  const file = (path && path.hub && path.hub.file) || (state && state.file);
  // 方法调用者的 name
  const { name } = node.callee;
  // babel-type 工具函数
  const { types } = this;
  // 内部状态
  const pluginState = this.getPluginState(state);

  // 如果方法调用者是 Identifier 类型
  if (types.isIdentifier(node.callee)) {
    if (pluginState.specified[name]) {
      node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
    }
  }

  // 遍历 arguments 找我们要的 specifier
  node.arguments = node.arguments.map(arg => {
    const { name: argName } = arg;
    if (
      pluginState.specified[argName] &&
      path.scope.hasBinding(argName) &&
      path.scope.getBinding(argName).path.type === 'ImportSpecifier'
    ) {
      // 找到 specifier,调用 importMethod 方法
      return this.importMethod(pluginState.specified[argName], file, pluginState);
    }
    return arg;
  });
}

КромеReact.createElement(Button)Кроме того, естьconst btn = Button / [Button]...и многие другие ситуации будут использоватьсяButton, в исходниках есть соответствующие методы обработки, если интересно, можете сами глянуть:GitHub.com/Анта-дизайн/…, тут особо нечего сказать.

Третий шаг — сгенерировать код импорта (ядро)

Основная работа первого и второго шагов заключается в поиске зависимостей, которые должны обрабатываться плагином, таких как:

import { Button, Rate } from 'antd';
ReactDOM.render(<Button>Hello</Button>);

Buttonиспользуются компоненты,RateНе используется в коде. Так что плагин должен автоматически импортироватьButtonкод и стиль.

Давайте сначала подведем итоги, когда мыimportПри создании компонента вы хотите, чтобы он:

import { Button } from 'antd';

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
require('antd/lib/button/style');

И снова вспомнить конфигурацию плагинаoptions, просто изменитеlibraryDirectoryтак же какstyleПросто подождите, пока конфигурация будет использована.

Дети, у вас есть несколько вопросительных знаков? Как сделать здесьbabelЧтобы изменить код и сгенерировать новыйimportи стильimportЧто ж, не паникуйте, просто посмотрите на код, и вы поймете:

import { addSideEffect, addDefault, addNamed } from '@babel/helper-module-imports';

importMethod(methodName, file, pluginState) {
  if (!pluginState.selectedMethods[methodName]) {
    // libraryDirectory:目录,默认 lib
    // style:是否引入样式
    const { style, libraryDirectory } = this;
    
    // 组件名转换规则
    // 优先级最高的是配了 camel2UnderlineComponentName:是否使用下划线作为连接符
    // camel2DashComponentName 为 true,会转换成小写字母,并且使用 - 作为连接符
    const transformedMethodName = this.camel2UnderlineComponentName
      ? transCamel(methodName, '_')
      : this.camel2DashComponentName
      ? transCamel(methodName, '-')
      : methodName;
    // 兼容 windows 路径
    // path.join('antd/lib/button') == 'antd/lib/button'
    const path = winPath(
      this.customName
        ? this.customName(transformedMethodName, file)
        : join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName),
    );
    // 根据是否有导出 default 来判断使用哪种方法来生成 import 语句,默认为 true
    // addDefault(path, 'antd/lib/button', { nameHint: 'button' })
    // addNamed(path, 'button', 'antd/lib/button')
    pluginState.selectedMethods[methodName] = this.transformToDefaultImport
      ? addDefault(file.path, path, { nameHint: methodName })
      : addNamed(file.path, methodName, path);
    // 根据不同配置 import 样式
    if (this.customStyleName) {
      const stylePath = winPath(this.customStyleName(transformedMethodName));
      addSideEffect(file.path, `${stylePath}`);
    } else if (this.styleLibraryDirectory) {
      const stylePath = winPath(
        join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName),
      );
      addSideEffect(file.path, `${stylePath}`);
    } else if (style === true) {
      addSideEffect(file.path, `${path}/style`);
    } else if (style === 'css') {
      addSideEffect(file.path, `${path}/style/css`);
    } else if (typeof style === 'function') {
      const stylePath = style(path, file);
      if (stylePath) {
        addSideEffect(file.path, stylePath);
      }
    }
  }
  return { ...pluginState.selectedMethods[methodName] };
}

addSideEffect, addDefaultа такжеaddNamedда@babel/helper-module-importsВсе три метода создаютimportметод, удельная производительность:

addSideEffect

addSideEffect(path, 'source');

      ↓ ↓ ↓ ↓ ↓ ↓

import "source"

addDefault

addDefault(path, 'source', { nameHint: "hintedName" })

      ↓ ↓ ↓ ↓ ↓ ↓

import hintedName from "source"

addNamed

addNamed(path, 'named', 'source', { nameHint: "hintedName" });

      ↓ ↓ ↓ ↓ ↓ ↓

import { named as _hintedName } from "source"

больше о@babel/helper-module-importsВидеть:@babel/helper-module-imports

Суммировать

Считаем 1 2 3 вместе,babel-plugin-importЧто нужно сделать, то сделано.

Подведем итоги,babel-plugin-importи общийbabelКак и плагин, он будет проходить кодast, затем вastсделал несколько вещей:

  1. Собрать зависимости:оказатьсяimportDeclaration, проанализируйте пакетaи зависимостиb,c,d....,еслиaа такжеlibraryNameсогласен, будетb,c,d...собранный внутри
  2. определить, использовать ли: в различных ситуациях (например, упомянутых вCallExpression) судить о собранномb,c,d...Используется ли он в коде, если используется, то называетсяimportMethodгенерировать новыеimpportутверждение
  3. Сгенерировать код импорта: создание кода и стилей на основе элементов конфигурации.importутверждение

Однако здесь не упомянуты некоторые детали, например, как удалить старый.importПодождите... Если вам интересно, вы можете прочитать исходный код самостоятельно.

Прочитав исходный код, вы нашли что-нибудь?antdа такжеelementВ дополнение к большим библиотекам компонентов можно использовать любую библиотеку компонентов.babel-plugin-importДля достижения загрузки по требованию и стилей автоматической загрузки.

Да, такие как наши обычно используемыеlodash, вы также можете использоватьbabel-plugin-importРазличные способы загрузки, вы можете попробовать.

Практическая реализация babel-plugin-import

Прочитав так много, я сам придумал упрощенную версиюbabel-plugin-importБар.

Если вы не знаете, как реализоватьBabelплагин, могу читать[Введение в плагин Babel] Как использовать Babel для автоматического добавления зависимостей в код

Простейшая реализация функции

Согласно вышеизложенному, наиболее важными элементами конфигурации являются три:

{
  "libraryName": "antd",
  "libraryDirectory": "lib",
  "style": true,
}

Поэтому мы реализуем только эти три элемента конфигурации.

И, как упоминалось выше, есть много способов вызвать компонент в реальных ситуациях, мы не рассматриваем здесь эти сложные ситуации, а реализуем только самые распространенные.<Button />передача.

входной файл

Роль входного файла состоит в том, чтобы получить элементы конфигурации, переданные пользователем, и применить основной код подключаемого модуля кastначальство.

import Plugin from './Plugin';

export default function ({ types }) {
  let plugins = null;

  // 将插件作用到节点上
  function applyInstance(method, args, context) {
    for (const plugin of plugins) {
      if (plugin[method]) {
        plugin[method].apply(plugin, [...args, context]);
      }
    }
  }

  const Program = {
    // ast 入口
    enter(path, { opts = {} }) {
      // 初始化插件实例
      if (!plugins) {
        plugins = [
          new Plugin(
            opts.libraryName,
            opts.libraryDirectory,
            opts.style,
            types,
          ),
        ];
      }
      applyInstance('ProgramEnter', arguments, this);
    },
    // ast 出口
    exit() {
      applyInstance('ProgramExit', arguments, this);
    },
  };

  const ret = {
    visitor: { Program },
  };

  // 插件只作用在 ImportDeclaration 和 CallExpression 上
  ['ImportDeclaration', 'CallExpression'].forEach(method => {
    ret.visitor[method] = function () {
      applyInstance(method, arguments, ret.visitor);
    };
  });

  return ret;
}

основной код

реальная модификацияastКодpluginРеализовано:

import { join } from 'path';
import { addSideEffect, addDefault } from '@babel/helper-module-imports';

/**
 * 转换成小写,添加连接符
 * @param {*} _str   字符串
 * @param {*} symbol 连接符
 */
function transCamel(_str, symbol) {
  const str = _str[0].toLowerCase() + _str.substr(1);
  return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`);
}

/**
 * 兼容 Windows 路径
 * @param {*} path 
 */
function winPath(path) {
  return path.replace(/\\/g, '/');
}

export default class Plugin {
  constructor(
    libraryName,                                   // 需要使用按需加载的包名
    libraryDirectory = 'lib',                      // 按需加载的目录
    style = false,                                 // 是否加载样式
    types,                                         // babel-type 工具函数
  ) {
    this.libraryName = libraryName;
    this.libraryDirectory = libraryDirectory;
    this.style = style;
    this.types = types;
  }

  /**
   * 获取内部状态,收集依赖
   * @param {*} state 
   */
  getPluginState(state) {
    if (!state) {
      state = {};
    }
    return state;
  }

  /**
   * 生成 import 语句(核心代码)
   * @param {*} methodName 
   * @param {*} file 
   * @param {*} pluginState 
   */
  importMethod(methodName, file, pluginState) {
    if (!pluginState.selectedMethods[methodName]) {
      // libraryDirectory:目录,默认 lib
      // style:是否引入样式
      const { style, libraryDirectory } = this;
      // 组件名转换规则
      const transformedMethodName = transCamel(methodName, '');
      // 兼容 windows 路径
      // path.join('antd/lib/button') == 'antd/lib/button'
      const path = winPath(join(this.libraryName, libraryDirectory, transformedMethodName));
      // 生成 import 语句
      // import Button from 'antd/lib/button'
      pluginState.selectedMethods[methodName] = addDefault(file.path, path, { nameHint: methodName });
      if (style) {
        // 生成样式 import 语句
        // import 'antd/lib/button/style'
        addSideEffect(file.path, `${path}/style`);
      }
    }
    return { ...pluginState.selectedMethods[methodName] };
  }
  
  ProgramEnter(path, state) {
    const pluginState = this.getPluginState(state);
    pluginState.specified = Object.create(null);
    pluginState.selectedMethods = Object.create(null);
    pluginState.pathsToRemove = [];
  }

  ProgramExit(path, state) {
    // 删除旧的 import
    this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
  }

  /**
   * ImportDeclaration 节点的处理方法
   * @param {*} path 
   * @param {*} state 
   */
  ImportDeclaration(path, state) {
    const { node } = path;
    if (!node) return;
    // 代码里 import 的包名
    const { value } = node.source;
    // 配在插件 options 的包名
    const { libraryName } = this;
    // babel-type 工具函数
    const { types } = this;
    // 内部状态
    const pluginState = this.getPluginState(state);
    // 判断是不是需要使用该插件的包
    if (value === libraryName) {
      // node.specifiers 表示 import 了什么
      node.specifiers.forEach(spec => {
        // 判断是不是 ImportSpecifier 类型的节点,也就是是否是大括号的
        if (types.isImportSpecifier(spec)) {
          // 收集依赖
          // 也就是 pluginState.specified.Button = Button
          // local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
          // imported.name 是真实导出的变量名
          pluginState.specified[spec.local.name] = spec.imported.name;
        } else { 
          // ImportDefaultSpecifier 和 ImportNamespaceSpecifier
          pluginState.libraryObjs[spec.local.name] = true;
        }
      });
      // 收集旧的依赖
      pluginState.pathsToRemove.push(path);
    }
  }


  /**
   * React.createElement 对应的节点处理方法
   * @param {*} path 
   * @param {*} state 
   */
  CallExpression(path, state) {
    const { node } = path;
    const file = (path && path.hub && path.hub.file) || (state && state.file);
    // 方法调用者的 name
    const { name } = node.callee;
    // babel-type 工具函数
    const { types } = this;
    // 内部状态
    const pluginState = this.getPluginState(state);

    // 如果方法调用者是 Identifier 类型
    if (types.isIdentifier(node.callee)) {
      if (pluginState.specified[name]) {
        node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
      }
    }

    // 遍历 arguments 找我们要的 specifier
    node.arguments = node.arguments.map(arg => {
      const { name: argName } = arg;
      if (
        pluginState.specified[argName] &&
        path.scope.hasBinding(argName) &&
        path.scope.getBinding(argName).path.type === 'ImportSpecifier'
      ) {
        // 找到 specifier,调用 importMethod 方法
        return this.importMethod(pluginState.specified[argName], file, pluginState);
      }
      return arg;
    });
  }
}

При этом достигается простейшееbabel-plugin-importПлагин, который может автоматически загружать отдельные пакеты и стили.

Полный код:GitHub.com/Axue Temple/Папа…

Суммировать

Благодаря анализу исходного кода и практическим занятиям в этой статье представлены подробные объяснения простым языком.babel-plugin-importПринцип работы плагина, надеюсь, прочитав эту статью, вы сможете четко понять, что делает этот плагин.

Для получения дополнительных статей вы можете обратить внимание на публичный аккаунт «Front-end Trial» и делиться ежедневными избранными статьями.

оBabelНекоторые ссылки, которые вы будете использовать: