乐趣区

关于前端:关于组件文档从编写到生成的那些事

前言

说到前端畛域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是以后的前端攻城狮们都免不了要理论应用到的根底组件库。而在理论工作中,咱们也总免不了要依据本人的工作内容,整顿一些适宜本人业务格调的一套组件库,根底组件局部能够基于下面开源的组件库以及 less 框架等多主题款式计划做本人的定制,但更多的是一些基于这些根底组件整顿出适宜本人业务产品的一套业务组件库。

而说到开发组件库,咱们或抉择 Monorepo 单仓库多包的模式(参考网文 https://segmentfault.com/a/11… 等)或其余 Git 多仓库单包的模式来保护组件代码,最终都免不了要将组件真正落到一个文档中,提供给其余共事去参考应用。

本篇文章就产出组件文档这件事,聊聊我在产出文档过程中的一系列思考过程,解决组件开发这「最初一公里」中的体验问题。

组件文档的编写

标准与搭建的调研

组件文档是要有肯定的标准的,标准是任何软件工程阶段的第一步。对于组件来说,文档透出的内容都应蕴含哪些内容,决定着组件开发者和使用者单方的所有的体验。确定这些标准并不难,参考开源的组件库的文档,咱们会有一些初步的印象。

因为咱们团队应用的是 React 技术栈,这里咱们参考 Ant Design 组件库。

比方这个最根本的 Button 组件,官网文档从上至下的内容构造是这样:

  1. 显示组件题目,下跟组件的简略形容。
  2. 列出组件的应用场景。
  3. 不同应用场景下的成果演示、代码案例。可外跳 CodeSandbox、CodePen 等在线源码编辑器网站编辑实时查看成果。
  4. 列出组件可配置的属性、接口办法列表。列表中蕴含属性 / 办法名、字段类型、默认值、应用形容等。
  5. 常见问题的 FAQ。
  6. 面向设计师的一些 Case 阐明链接。

这些文档内容很丰盛,作为一个凋谢的组件库,简直思考到的从设计到开发视角的方方面面,应用体验特地好。而在好奇心驱使下,我去查看了官网源码方库,比方 Button 组件:https://github.com/ant-design…。在源码库下,搁置了和组件入口文件同名的别离以 .zh-CN.md.en-US.md 后缀命名的 Markdown 文件,而在这些 Markdown 文件中,便是咱们看到的官网文档内容 … 咦?不对,如同短少了什么,案例演示和示例代码呢?

难道 AntD 官网文档是另外本人手动开发保护的?这么大的一个我的项目必定不至于,依据其官网相似 docs/react/introduce-cn 这种拜访门路在源码库中有对应的 Markdown 文件来看,官网的文档必定是官网仓库依据一种标准来生成的。那么是怎么生成的呢?第一次做组件文档标准的我被挑起了趣味。

而作为一个前端工程新手,我很熟练地关上了其 package.json 文件,通过查看其中的 scripts 命令,轻易便发现了其下的 site 命令(源码仓库 package.json):

npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js

原来如此,网站的构建应用了 bisheng。通过查阅理解 bisheng 这个工具库,发现它的确是一个文档零碎的主动生成工具,其下有一个插件 bisheng-plugin-react,能够将 Markdown 文档中的 JSX 源码块转换成能够运行演示的示例。而每个组件本身的示例代码文档,则在每个组件门路下的
demo 目录下保护。

Emmm,bisheng 的确是很好很弱小,还能反对多语言,联合肯定的文档标准束缚下,可能疾速搭建一个文档的主站。但在深刻理解 bisheng 的过程中,发现其文档相对来说比拟不足,包装的货色又那么多,应用过程中黑盒感重大,而咱们团队的组件库其实要求很简略,一是能做到不便流通,而是只在外部流通应用,不会开源。那么,有没有更简略的搭建文档的形式呢?

更多的文档工具库的调研

在谷歌搜寻中敲入如 React Components Documentation 等关键字,咱们很快便能搜寻出很多与 React 组件文档相干的工具库,这里我看到了如下这些:Docz、StoryBook、React Styleguidist、UMI 构建体系下的 dumi 等等。

这些工具库都反对解析 Markdown 文档,其中 Docz 和 StoryBook 还反对应用 mdx 格局(Markdown 和 JSX 的混合写法),且在文档内容格局都能反对到组件属性列表、示例代码演示等性能。

接下来,咱们别离简略看下这些工具库对于组件文档的反对状况。

Docz

在理解过程中,发现 Docz 其实是一个比拟老牌的文档零碎搭建工具了。它自身便主推 MDX 格局的文档,根本不须要什么配置便能跑起来。反对本地调试和构建生成可公布产物,反对多包仓库、TypeScript 环境、CSS 处理器、插件机制等,齐全满足性能须要。

只是 Docz 貌似只反对 React 组件(当然对于咱们来说够用),且看其 NPM 包最近更新曾经是两年之前。另外 MDX 格局的文档尽管了解老本很少但对于应用不多的共事来说还是有肯定的承受和纯熟上手的老本。临时备选。

StoryBook

在首次理解到 StoryBook 时便被其 66.7K 的 Star 量惊到了(Docz 是 22K),绝对 Docz 来说,StoryBook 相干的社区内容十分丰盛,它不依赖组件的技术栈体系,当初曾经反对 React、Vue、Angular、Web Components 等数十个技术栈。

StoryBook 搭建文档零碎的形式不是去主动解析 Markdown 文件,而是裸露一系列搭建文档的接口,让开发者本人为组件手动编写一个个的 stories 文件,StoryBook 会主动解析这些 stories 文件来生成文档内容。这种形式会带来肯定的学习和了解接口的老本,但同时也基于这种形式实现了反对跨组件技术栈的成果,并让社区显得更为丰盛。

官网示例:https://github.com/storybookj…。

StoryBook 的弱小毋庸置疑,但对于咱们团队的状况来说还是有些杀鸡用牛刀了。另外,其须要额定了解接口性能并编写组件的 stories 文件在团队内很难推动起来:大家都很忙,组件开发散布在团队几十号人,状况比较复杂,将文档整顿束缚到一个人身上又不事实。持续调研。

React Styleguidist

React Styleguidist 的 Star 量没有 StoryBook 那么夺目(10K+),但包体的下载量也比拟大,且近期的提交也是相当沉闷。由名字可知,它反对的是 React 组件的环境。它是通过主动解析 Mardown 文件的模式来生成文档的,实现形式是主动解析文档中 JSX 申明代码块,依照名称一一对应的规定查找到组件源码,而后将申明的代码块通过 Webpack 打包产生出对应的演示示例。

而在持续试用了 React Styleguidist 的一些根底案例后,它的一个性能让我眼前一亮:它会主动解析组件的属性,并解析出其类型、默认值、正文形容等内容,而后将解析到的内容主动生成属性表格搁置在演示示例的上方。这就有点 JSDOC 的意思了,对于一个组件开发者来说,TA 的确须要关怀组件属性的透出、正文以及文档案例的编写,但编写完也就够了,不必去思考怎么适应搭建出一个文档零碎。

另外,React Styleguidist 解析组件属性是基于解析 AST 以及配合工具 react-docgen 来实现的,并且还反对配合 react-docgen-typescript 来实现解析 TypeScript 环境下的组件,另外还能很多配置项反对更改文档站点相干的各个局部的展现款式、内容格局等,配置自定义反对相当灵便。

当然,它也有一些毛病,比方内嵌 Webpack,对于曾经将编译组件库的构建工具换为 Rollup.js 的状况是一个额定的配置累赘。

总的来说,React Styleguidist 在我看来是一个小而美的工具库,很适宜咱们团队合作参加人多、且大都日常开发工作沉重的状况。临时备选。

dumi

理解到 dumi 是因为咱们团队内曾经有局部组件文档站点是基于它来搭建的了。dumi 一样是通过主动解析 Markdown 文档的形式来实现搭建文档零碎,同样根本零配置,也有很多灵便的配置反对更改文档站点一些局部的显示内容、(主题)款式等,整体秉承了 UMI 体系的格调:开箱即用,封装极好。它能独自应用,也能联合 UMI 框架一起配置应用。

只是相比于下面曾经理解到的 React Styleguidist 来说,并未看到有其余显著的劣势,且貌似没有看到有主动解析组件属性局部的性能,对于我来说没有 React Styleguidist 下得一些亮点。能够参考,不再思考。

组件文档的生成

在多方比照了多个文档搭建的工具库后,我最终还是选用了 React Styleguidist。在我看来,天然是其基于 react-docgen 来实现解析组件属性、类型、正文形容等的性能吸引到了我,这个性能一方面能在较少的额定工夫付出下标准团队共事开发组件过程中一系列标准,另一方面其 API 接口的接入模式可能通过对立构建配置而对立产出文档内容格局和款式,不便各业务接入应用。

决定了技术计划后,便是如何具体实现基于其封装一个工具,便于各业务仓库接入了。

咱们团队有本人对立的 CLI 构建工具,再多一个 React Styleguidist 的 CLI 配置会在了解上有肯定的相熟老本,但我能够基于 React Styleguidist 的 Node API 接入模式,将 React Styleguidist 的性能别离融入咱们本身 CLI 的 devbuild 命令。

首先,基于 React Styleguidist API 的模式,对立一套配置,将生成 React Styleguidist 示例的代码形象进去:

// 定义一套对立的配置,生成 react-styleguidist 实例
import styleguidist from 'react-styleguidist/lib/scripts/index.esm';
import * as docgen from 'react-docgen';
import * as docgenTS from 'react-docgen-typescript';

import type * as RDocgen from 'react-docgen';

export type DocStyleguideOptions = {
  cwd?: string;
  rootDir: string;
  workDir: string;
  customConfig?: object;
};

const DOC_STYLEGUIDE_DEFAULTS = {cwd: process.cwd(),
  rootDir: process.cwd(),
  workDir: process.cwd(),
  customConfig: {},};

export const createDocStyleguide = (
  env: 'development' | 'production',
  options: DocStyleguideOptions = DOC_STYLEGUIDE_DEFAULTS,
) => {
  // 0. 解决配置项
  const opts = {...DOC_STYLEGUIDE_DEFAULTS, ...options};
  const {
    cwd: cwdPath = DOC_STYLEGUIDE_DEFAULTS.cwd,
    rootDir,
    workDir,
    customConfig,
  } = opts;

  // 标记:是否正在调试所有包
  let isDevAllPackages = true;

  // 解析工程根目录包信息
  const pkgRootJson = Utils.parsePackageSync(rootDir);

  // 1. 解析指定要调试的包下的组件
  let componentsPattern: (() => string[]) | string | string[] = [];
  if (path.relative(rootDir, workDir).length <= 0) {
    // 抉择调试所有包时,则读取根门路下 packages 字段定义的所有包下的组件
    const {packages = [] } = pkgRootJson;
    componentsPattern = packages.map(packagePattern => (path.relative(cwdPath, path.join(rootDir, packagePattern, 'src/**/[A-Z]*.{js,jsx,ts,tsx}'))
    ));
  } else {
    // 抉择调试某个包时,则定位至抉择的具体包下的组件
    componentsPattern = path.join(workDir, 'src/**/[A-Z]*.{js,jsx,ts,tsx}');
    isDevAllPackages = false;
  }

  // 2. 获取默认的 webpack 配置
  const webpackConfig = getWebpackConfig(env);

  // 3. 生成 styleguidist 配置实例
  const styleguide = styleguidist({title: `${pkgRootJson.name}`,
    // 要解析的所有组件
    components: componentsPattern,
    // 属性解析设置
    propsParser: (filePath, code, resolver, handlers) => {if (/\.tsx?/.test(filePath)) {
        // ts 文件,应用 typescript docgen 解析器
        const pkgRootDir = findPackageRootDir(path.dirname(filePath));
        const tsConfigParser = docgenTS.withCustomConfig(path.resolve(pkgRootDir, 'tsconfig.json'),
          {},);
        const parseResults = tsConfigParser.parse(filePath);
        const parseResult = parseResults[0];
        return (parseResult as any) as RDocgen.DocumentationObject;
      }
      // 其余应用默认的 react-docgen 解析器
      const parseResults = docgen.parse(code, resolver, handlers);
      if (Array.isArray(parseResults)) {return parseResults[0];
      }
      return parseResults;
    },
    // webpack 配置
    webpackConfig: {...webpackConfig},
    // 初始是否开展代码样例
    // expand: 开展 | collapse: 折叠 | hide: 不显示;
    exampleMode: 'expand',
    // 组件 path 展现内容
    getComponentPathLine: (componentPath) => {const pkgRootDir = findPackageRootDir(path.dirname(componentPath));
      try {const pkgJson = Utils.parsePackageSync(pkgRootDir);
        const name = path.basename(componentPath, path.extname(componentPath));
        return `import ${name} from '${pkgJson.name}';`;
      } catch (error) {return componentPath;}
    },
    // 非调试所有包时,不显示 sidebar
    showSidebar: isDevAllPackages,
    // 日志配置
    logger: {
      // One of: info, debug, warn
      info: message => Utils.log('info', message),
      warn: message => Utils.log('warning', message),
      debug: message => console.debug(message),
    },
    // 笼罩自定义配置
    ...customConfig,
  });

  return styleguide;
};

这样,在 devbuild 命令下能够别离调用实例的 server 接口办法和 build 接口办法来实现调试和构建产出文档动态资源。

// dev 命令下启动调试
// 0. 初始化配置
const HOST = process.env.HOST || customConfig.serverHost || '0.0.0.0';
const PORT = process.env.PORT || customConfig.serverPort || '6060';

// 1. 生成 styleguide 实例
const styleguide = createDocStyleguide(
  'development',
  {
    cwd: cwdPath,
    rootDir: pkgRootPath,
    workDir: workPath,
    customConfig: {
      ...customConfig,
      // dev server host
      serverHost: HOST,
      // dev server port
      serverPort: PORT,
    },
  },
);

// 2. 调用 server 接口办法启动调试
const {compiler} = styleguide.server((err, config) => {if (err) {console.error(err);
  } else {const url = `http://${config.serverHost}:${config.serverPort}`;
    Utils.log('info', `Listening at ${url}`);
  }
});
compiler.hooks.done.tap('done', (stats: any) => {
  const timeStr = stats.toString({
    all: false,
    timings: true,
  });

  const statStr = stats.toString({
    all: false,
    warnings: true,
    errors: true,
  });

  console.log(timeStr);

  if (stats.hasErrors()) {console.log(statStr);
    return;
  }
});
// build 命令下执行构建

// 生成 styleguide 实例
const styleguide = MonorepoDev.createDocStyleguide('production', {
  cwd,
  rootDir,
  workDir,
  customConfig: {styleguideDir: path.join(pkgDocsDir, 'dist'),
  },
});
// 构建文档内容
await new Promise<void>((resolve, reject) => {
  styleguide.build((err, config, stats) => {if (err) {reject(err);
      } else {if (stats != null) {
          const statStr = stats.toString({
            all: false,
            warnings: true,
            errors: true,
          });
          console.log(statStr);
          if (stats.hasErrors()) {reject(new Error('Docs build failed!'));
            return;
          }
          console.log('\n');
          Utils.log('success', `Docs published to ${path.relative(workDir, config.styleguideDir)}`);
        }
        resolve();}
    },
);

最初,在组件多包仓库的每个包下的 package.json 中,别离配置 devbuild 命令即可。实现了反对无感启动调试和构建产出文档资源。

小结

本文次要介绍了我在调研实现组件文档标准和搭建过程中的一个思考过程,诚如文中介绍其余文档零碎搭建工具时所说,有很多优良的开源工具可能反对实现咱们想要的成果,这是前端攻城狮们的侥幸,也是可怜:咱们能够站在前人的肩膀上,但要在这么多优良库中抉择一个适宜本人的,更须要多做一些理解和收益点的衡量。一句老话经久不衰:适宜本人的才是最好的。

心愿这篇文章对看到这里的你能有所帮忙。

作者:ES2049 / 靳志凯

文章可随便转载,但请保留此原文链接。
十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com。

退出移动版