Инструмент автоматического создания документации библиотеки компонентов

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

предисловие

Когда мы используем библиотеку компонентов, страница документации является самым прямым окном для получения информации. Страница документации обычно содержит следующую информацию:

  1. описание компонента
  2. Демо-шоу Примерные компоненты и исходный код Описание
  3. Документация по параметрам для компонентов

Для отладки и отображения чисто компонентных примеров мы, конечно, можем выбрать такие инструменты, как storybook, но, учитывая красоту и сложность восстановления по схеме проекта, нам все равно придется подумать о написании самостоятельно настраиваемого и расширяемого инструмента.

анализировать

Давайте разберем наши требования с информацией, содержащейся на странице документации выше:

  1. Самый лаконичный синтаксис для написания страниц
  2. Самый лаконичный синтаксис для демонстрации Демо + исходный код + описание примера
  3. Документирование параметров обслуживания с минимальными затратами

С точки зрения синтаксиса мы должны предпочесть уценку, синтаксис лаконичный и достаточно мощный.

Чтобы отобразить демо и исходный код, чтобы поддерживать более эффективный и недорогой, мы должны поместить пример Демо + исходный код + описание примера в файл, максимально демультиплексировать и уменьшить код, который необходимо поддерживается. Отображение примера по сути такое же, как перевод уценки, то есть уценка -> html, но нам нужно расширить правила перевода.

Есть много проблем с ручным обслуживанием при ведении документов параметров:Высокая стоимость, непростая синхронизация с кодом(Каждое изменение должно вручную изменять документ параметров), поэтому мы должны рассмотреть возможность автоматического извлечения информации из объявления ts для формирования документа параметров.

а такжеmarkdown -> html, нам на самом деле нужен толькоwebpack loaderВот и все, разбираем процесс следующим образом:

image.png

выполнить

Предварительная обработка входных файлов

Наш общий входной файл представляет собой файл уценки, который является страницей, которую мы генерируем.Вся структура страницы выглядит следующим образом (конечно, порядок этой структурырегулируемыйиз):

image.png

Выдержка из объявлений TypeScriptимя параметра,описывать,Типы,По умолчанию, мы можем использоватьreact-docgen-typescriptЭтот инструмент, мы делаем небольшую модификацию.

Настройте компилятор для фильтрации параметров, которые сами по себе не требуют отображения.

const parse = require('react-docgen-typescript').withDefaultConfig({
  propFilter: (prop) => {
    if (prop.parent == null) {
      return true;
    }
    return prop.parent.fileName.indexOf('node_modules/@types/react') < 0;
  },
}).parse;

Получить необходимую информацию по компоненту ts

const params = parse(filePath);
const info = params[0];

Выше мы получили информацию, которая содержит компонентимя параметра,описывать,Типы,По умолчаниюи другую информацию, нам просто нужно преобразовать эту информацию в уценкуНаписание столаЗатем вставьте его в указанное место основного файла уценки записи.

Webpack loader

Загрузчик Webpack сам обрабатывает указанный тип файла и выводит фактически упакованный код js Теперь нам нужно преобразовать уценку в фактически работающий код jsx реакции.

На предыдущем шаге мы получилиСодержит информацию о параметрахОбщий файл уценки записи, для этой уценки мы резервируем слот%%Content%%(Конечно, мы можем указать его по желанию), который используется для вставки Демо-примера позже.

Чтобы облегчить обслуживание, мы помещаем демонстрационные примеры каждого компонента в соответствующий каталог компонентов и сохраняем их в папке с именем demo.

Каждый демонстрационный пример представляет собой файл уценки, и содержимое файла уценки выглядит следующим образом:

---
order: 0
title: 不同类型的按钮
---

按钮分为 默认按钮,主要按钮,危险按钮,高危按钮,虚线按钮,文本按钮六种。

import { Button } from '@bytedesign/web-react';

ReactDOM.render(
  <Button type="primary">
    Primary
  </Button>,
  CONTAINER
);

Файл содержит такую ​​информацию, как заголовок, порядок отображения, описание, пример исходного кода и т. д.

Мы используем обычные или напрямуюfront-matter, вы можете получить заголовок и порядок отображения и т. д.информация о конфигурации, исходный код описательной информации и примеров можно получить с помощью регуляризации, и вся необходимая информация доступна.Далее нам нужно вставить эту информацию в нужную нам страницу.

Обработка дерева AST

Для обработки дерева AST мы решили использовать babel, который нам нужен:

  1. @babel/core
  2. @babel/parser
  3. @babel/template
  4. @babel/traverse
  5. @babel/generator
  6. @babel/types

AST-дерево кода представляет собой очень сложную древовидную структуру, которую мы можем передатьastexplorer.net/Этот сайт помогает создавать и просматривать деревья AST..

Для содержимого уценки мы должны использоватьmarkedПреобразуйте его в html-код. Конечно, преобразованный html — это строка. Мы не можем сгенерировать AST-дерево с помощью Babel. Нам нужно сначала преобразовать строку html в jsx. следующим образом:

function htmlToJsx(html) {
  return `import React from 'react';
  export default function() {
    return (
      <span>${html
        .replace(/class=/g, 'className=')
        .replace(/{/g, '{"{"{')
        .replace(/}/g, '{"}"}')
        .replace(/{"{"{/g, '{"{"}')}
      </span>
    );
  };`;
}

Имея в руках код jsx, мы можем с радостью сгенерировать дерево AST:

const parser = require('@babel/parser');
function parse(codeBlock) {
  return parser.parse(codeBlock, {
    sourceType: 'module',
    plugins: ['jsx', 'classProperties'],
  });
}
const ast = parse(htmlToJsx(marked(markdown)));

Создайте AST демонстрационного примера

Получите дерево AST примера исходного кода с помощью регулярных выражений:

// @arco-design/arco-components 为抽离的用于展示示例和源码的组件
const ast = parse(`
   import { CodeBlockWrapper, CellCode, CellDemo, CellDescription, Browser } from "@arco-design/arco-components";
   ${code}
`);

Пройдитесь по дереву AST и вставьте полученный пример AST, описание AST и AST исходного кода в компонент CodeBlockWrapper:

const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

traverse(ast, {
    CallExpression(_path) {
      if (
        _path.node.callee.object &&
        _path.node.callee.object.name === 'ReactDOM' &&
        _path.node.callee.property.name === 'render'
      ) {
        const demoCellElement = t.jsxElement(
          t.jsxOpeningElement(t.JSXIdentifier('CellDemo'), []),
          t.jsxClosingElement(t.JSXIdentifier('CellDemo')),
          [_path.node.arguments[0]]
        );

        const codeCellElement = t.jsxElement(
          t.jsxOpeningElement(t.JSXIdentifier('CellCode'), codeAttrs),
          t.jsxClosingElement(t.JSXIdentifier('CellCode')),
          [codePreviewBlockAst]
        );

        const descriptionCellElement = t.jsxElement(
          t.jsxOpeningElement(t.JSXIdentifier('CellDescription'), []),
          t.jsxClosingElement(t.JSXIdentifier('CellDescription')),
          [descriptionAst]
        );

        const codeBlockElement = t.jsxElement(
          t.jsxOpeningElement(t.JSXIdentifier('CodeBlockWrapper'), []),
          t.jsxClosingElement(t.JSXIdentifier('CodeBlockWrapper')),
          [descriptionCellElement, demoCellElement, codeCellElement]
        );

        const app = t.VariableDeclaration('const', [
          t.VariableDeclarator(t.Identifier('__export'), codeBlockElement),
        ]);

        _path.insertBefore(app);
        _path.remove();
      }
    },
  });

Получите код, преобразованный, как указано выше:

const babel = require('@babel/core');
const { code } = babel.transformFromAstSync(ast, null, babelConfig);

Выходной код имеет следующий формат:

const __export = <CodeBlockWrapper>
    <CellDescription>...<CellDescription>
    <CellDemo>...</CellDemo>
    <CellCode>...<CellCode>
</CodeBlockWrapper>

В нашей демонстрационной папке будет несколько примеров уценки, и каждый пример будет генерировать функциональный компонент и помещать его в массив:

const generate = require('@babel/generator').default;
const template = require('@babel/template').default;

const buildRequire = template(`
    function NAME() {
      AST
      return __export;
    }
  `);

  const finnalAst = buildRequire({
    NAME: `Demo${index}`,
    AST: code,
  });

  demoList.push(generate(finnalAst).code);

Теперь нашdemoListПо сути, это массив всех примеров компонентов компонента, и мы помещаем эти примеры в компонент, который фактически используется для отображения:

const buildRequire = template(`
    CODE
    class Component extends React.Component {
      render() {
        return React.createElement('span', { className: 'arco-components-wrapper' }, ${demoList
          .map((_, index) => `React.createElement(Demo${index}, { key: ${index} })`)
          .join(',')});
      }
    }
  `);

  const finnalAst = buildRequire({
    CODE: demoList.join('\n'),
  });

OK, FINNALAST — это AST в AST, сгенерированный вставляемой записью Markdown.

заменить заполнитель

Помните, мы оставили%%Content%%заполнитель, теперь нам нужно заменить обработанный пример на место заполнителя.

traverse(contentAst, {
  JSXElement: (_path) => {
    if (
      _path.node.openingElement.name.name === 'p' &&
      _path.node.children[0].value === '%%Content%%'
    ) {
      const expresstion = t.jsxExpressionContainer(
        t.jsxElement(
          t.jsxOpeningElement(t.JSXIdentifier('Component'), [], true),
          null,
          [],
          true
        )
      );
      _path.replaceWith(expresstion);
      _path.stop();
    }
  },
});

// 把我们处理的示例ast放到函数声明前

traverse(contentAst, {
  FunctionDeclaration: (_path) => {
    _path.insertBefore(finnalAst);
    _path.stop();
  },
});

Загрузчик Webpack, наконец, обрабатывает возвращенный код:

return generate(contentAst).code; 

использовать

В соответствии с описанным выше процессом мы успешно создали загрузчик уценки, который обрабатывает файлы уценки и будет обрабатывать демо-примеры.После использования этого плагина мы можем использовать его следующим образом:

import ButtonPage from 'components/Button/README.md';    
function Page() {    
    return <ButtonPage />;  
} 

Суммировать

Выше мы использовали загрузчик веб-пакетов для создания документов компонентов на основе уценки, Этот процесс фактически решил многие из наших проблем при написании документов компонентов:

  1. Убедитесь, что документы параметров полностью синхронизированы с исходным кодом, что значительно снижает затраты на обслуживание.
  2. Отображение примеров на официальном веб-сайте, отладка компонентов и т. д. выполняются за один шаг, а затраты на написание примеров сведены к минимуму. Просто напишите код один раз, и его можно использовать для генерации одновременноПример официального сайта,Исходный код примера официального сайта,Тесты моментальных снимков, страницы справки Github/Gitlab. Более того, этот процесс полностью автоматизирован, и он может в основном сосредоточиться только на написании логики компонентов, что значительно снижает затраты на разработку и обслуживание.
  3. Воспользовавшись тем, что файлы уценки изначально анализируются и отображаются gitlab и github, у нас есть страница описания в каждом каталоге компонентов, где вы можете увидеть такую ​​информацию, как параметры и описания.
  4. Полностью управляемый стиль официального сайта.

Если вы также разрабатываете библиотеку компонентов React или вам нужно показать примеры компонентов, связанных с React, этот документ может вам помочь.