基于Antd+Dumi搭建组件库

1、组件库文档工具对比

组件库文档的要求:

  • 支持组件的渲染,可交互
  • 支持markdown
  • 列出组件的所有属性(API),包括类型、说明、默认值等

搜罗市面上知名度角度较高的组件库文档框架:doczstorybookdumivuepress对比如下:

docz Storybook dumi VuePress
将引入模块写在代码示例中
自动生成组件库 API
文档内嵌在组件目录中
支持编写的组件库类型 ALl(React、Vue、Angular) ALl(React、Vue、Angular) REACT Vue
社区活跃度 低 (2020年停止更新) 高(仍在更新) 高(仍在更新)
配置复杂度 简单 复杂,上手难度较高,配置相对复杂,学习曲线较陡 简单

我们vrp-component组件库是基于react+antd进行二次开发,几种组件库文档框架对比,显然dumi是更适合,

由蚂蚁集团(Ant Design团队)开发,配置简单,升级成本较小。话不多说,选它!

2、升级

2.1 初始化 d.umijs.org/guide/initi...

选择React Library,初始化完之后的目录结构如下:

npm start 可以看到初始化默认首页啦

2.2 修改首页

位置:docs/index.md

logo、主题颜色修改在.dumirc配置

2.3 首页顶部导航

.dumirc.ts文件配置nav导航:

更新日志:位置:docs/changelogs/index.md

更新日志 Emoji 释义: 位置:docs/changelogs/emojiParaphrase.md

以上顶部更新日志Tab下会左侧会自动生成 更新日志更新日志 Emoji 释义 两个子目录。

nav-title :导航分组

nav-order:子目录顺序。

其他顶部导航类推。

2.4 修改默认布局

首页的顶部导航默认在左侧,搜索在右侧,希望修改成 顶部导航默认在右侧,搜索偏左侧。

找到dumi2.0默认主题的源码,在文件夹.dumi下创建一个theme文件夹,把默认主题的slots下的Header复制到我们的theme/slots/Header目录下,

同时复制对应所需要的style文件,然后进行修改,文件结构如图:

加上样式修改即可。想要修改其他样式方法类似。

2.5 迁移组件

同时复制对应所需要的style文件,然后进行修改,文件结构如图:

dumi2.0 组件默认在src目录下,并且每个组件下增加index.mdreadme.md即可自动生成组件文档。

但是默认的第一个组件是顶部导航的名称,所以需要修改下。

我们的组件下第一个子目录是getting start说明,所以可以在docs下创建一个组件nav,内容为getting start说明,业务组件统一放置在src/components下,但md的nav需要一致:

Getting Started的nav:

Card 卡片组件的nav:

详见dumi的约定式路由

业务组件统一放置在src/components下修改了默认的识别结构,所以还需要在.dumirc配置组件目录

2.6 迁移组件md

除了顶部nav和代码演示的嵌入demo方式需要修改,其他语法可直接复用。

jsx 和 tsx 的代码块将会被 dumi 解析为 React 组件。如果只想要展示源代码,添加 pure标识即可,详见

dumi也支持嵌入demo,默认支持使用外部文件,使用code标签即可。

.dumirc配置别名,就可以实现通过一个指向包名的引用方式,而不是相对路径

3.gulp打包

js 复制代码
const gulp = require('gulp');
const babel = require('gulp-babel');
const less = require('gulp-less');
const autoprefixer = require('gulp-autoprefixer');
const cssnano = require('gulp-cssnano');
const through2 = require('through2');

const paths = {
  dest: {
    lib: 'lib',
    es: 'es',
    dist: 'dist',
  },
  styles: [
    'src/components/**/*.less',
    '!src/components/**/__demo__/*.{less,css}', 
    '!src/components/**/__tests__/*.{less,css}',
  ],
  scripts: [
    'src/components/**/*.{ts,tsx}',
    '!src/components/**/__demo__/*.{ts,tsx}',
    '!src/components/**/__tests__/*.{ts,tsx}',
  ],
};

/**
 * 当前组件样式 import './index.less' => import './index.css'
 * 依赖的其他组件样式 import '../test-comp/style' => import '../test-comp/style/css.js'
 * 依赖的其他组件样式 import '../test-comp/style/index.js' => import '../test-comp/style/css.js'
 * @param {string} content
 */
function cssInjection(content) {
  return content
    .replace(/\/style\/?'/g, "/style/css'")
    .replace(/\/style\/?"/g, '/style/css"')
    .replace(/\.less/g, '.css');
}

/**
 * 编译脚本文件
 * @param {string} babelEnv babel环境变量
 * @param {string} destDir 目标目录
 */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel()) // 使用gulp-babel处理
    .pipe(
      through2.obj(function z(file, encoding, next) {
        this.push(file.clone());
        // 找到目标
        if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
          const content = file.contents.toString(encoding);
          file.contents = Buffer.from(cssInjection(content)); // 处理文件内容
          file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名
          this.push(file); // 新增该文件
          next();
        } else {
          next();
        }
      }),
    )
    .pipe(gulp.dest(destDir));
}

/**
 * 编译cjs
 */
function compileCJS() {
  const { dest } = paths;
  return compileScripts('cjs', dest.lib);
}

/**
 * 编译
 */
function compileES() {
  const { dest } = paths;
  return compileScripts('es', dest.es);
}

const buildScripts = gulp.series(compileCJS, compileES);

/**
 * 拷贝less文件
 */
function copyLess() {
  return gulp
    .src(paths.styles)
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.es));
}

/**
 * 生成css文件
 */
function less2css() {
  return gulp
    .src(paths.styles)
    .pipe(less()) // 处理less文件
    .pipe(autoprefixer()) // 根据browserslistrc增加前缀
    .pipe(cssnano({ zindex: false, reduceIdents: false })) // 压缩
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.es));
}

const build = gulp.parallel(buildScripts, copyLess, less2css);

exports.build = build;

exports.default = build;

4. father 打包

dumi 默认使用 father 打包,所以也想增加这种打包方式。先来看看它是啥玩意:github.com/umijs/fathe...

支持ESModule 及 CommonJS 产物,(感觉这个很好用了),但在此基础之上还需要支持:

1.LESS 到 CSS 的转换

2.处理样式中的 @import 语句

3.自动为 CSS 属性添加浏览器前缀,提高兼容性

.fatherrc.ts

jsx 复制代码
import { defineConfig } from 'father';

export default defineConfig({
  // more father config: https://github.com/umijs/father/blob/master/docs/config.md
  esm: {
    input: 'src/components',
    output: 'es',
    ignores: ['src/components/*/__demo__/**/*', 'src/components/**/*.md'],
    platform: 'browser', // 指定平台
    transformer: 'babel', // 使用 babel 转换器
  },
  cjs: {
    input: 'src/components',
    output: 'lib',
    ignores: ['src/components/*/__demo__/**/*', 'src/components/**/*.md'],
    platform: 'node', // 指定平台
    transformer: 'babel', // 使用 babel 转换器
  },
  // 使用 babel-less-to-css.js 把 js/ts 文件中的 '.less' 字符转为 '.css'
  // 没有这个babel插件,less文件不会被转换为css文件 
  // extraBabelPlugins必须在plugins前面
  extraBabelPlugins: [
    [
      './babel-less-to-css.js', //  '.less' 字符转为 '.css'
      {
        test: '\\.less',
      },
    ],
    [
      'babel-plugin-import',
      {
        libraryName: 'antd',
        style: true,
      },
    ],
  ],
  plugins: [
    './postcss-loader.ts', // 实现 loader 功能
  ],

});

less to css loader:

jsx 复制代码
const path = require('path');
const fs = require('fs');
const less = require('less');
const postcss = require('postcss');
const syntax = require('postcss-less');
const atImport = require('postcss-import'); //处理css文件中的import的语句的
const autoprefixer = require('autoprefixer');

const loader = function (lessContent) {
  const callback = this.async();
  this.setOutputOptions({
    ext: '.css',
  });
  postcss([
    autoprefixer({
      // 提升兼容性
      overrideBrowserslist: ['last 10 versions'],
    }),
    atImport({
      resolve: (id) => {
        const currentPath = this.resource;
        if (id.startsWith('@')) {
          // 处理别名路径,把 @ 替换成 src/components
          const srcPath = path.join(__filename, './src/components');
          const targetPath = id.replace(/^@/, srcPath);
          return targetPath;
        } else {
          // 处理相对路径
          const relativePath = id;
          const targetPath = path.resolve(currentPath, '..', relativePath);
          return targetPath;
        }
      },
    }),
  ])
    .process(lessContent, { syntax })
    .then((result) => {
      // less 转 css
      less.render(result.content, (err, css) => {
        if (err) {
          console.error(err);
          return;
        }
        callback(null, css.css);
      });
    })
    .catch((err) => {
      console.error(err);
    });
};

//这个转化也可
// function loader(content, sourcemap, meta) {
//   // 获取当前处理的文件路径
//   const filePath = this.resourcePath;
//   const callback = this.async();

//   // 1. 先复制原始 less 文件到目标目录
//   const relativePath = path.relative(
//     path.join(process.cwd(), 'src/components'),
//     filePath,
//   );
//   const esOutputPath = path.join(process.cwd(), 'es', relativePath);
//   const libOutputPath = path.join(process.cwd(), 'lib', relativePath);

//   // 确保目录存在
//   fs.mkdirSync(path.dirname(esOutputPath), { recursive: true });
//   fs.mkdirSync(path.dirname(libOutputPath), { recursive: true });

//   // 复制原始 less 文件
//   fs.copyFileSync(filePath, esOutputPath);
//   fs.copyFileSync(filePath, libOutputPath);

//   // 2. 编译 less 到 css
//   less
//     .render(content, {
//       filename: filePath,
//       sourceMap: { outputSourceFiles: true },
//     })
//     .then((result) => {
//       // 写入编译后的 css 文件
//       const cssEsPath = esOutputPath.replace(/\.less$/, '.css');
//       const cssLibPath = libOutputPath.replace(/\.less$/, '.css');

//       fs.writeFileSync(cssEsPath, result.css);
//       fs.writeFileSync(cssLibPath, result.css);

//       // 将内容传递给下一个 loader
//       callback(null, content, sourcemap, meta);
//     })
//     .catch((err) => {
//       callback(err);
//     });
// }
module.exports = loader;

5.调试

增加调试日志,打包之后,生成es、lib,

在本目录下运行 npm run link,创建全局目录软链接至该项目。

在业务系统项目目录下运行 npm run link @vrp/components

将会替换 node_modules/@vrp/components,其软链接至刚刚创建的全局目录。

检查页面样式以及功能是否正常。

6. 组件文档站点

运行npm run docs:build ,生成docs-dist,进入此目录运行live-server,页面正常访问

7. 自动生成API

组件遵从 TypeScript 类型定义 + JS Doc 注解,dumi 能自己推导出 API 的内容。在 md 文件末尾,追加上 dumi 内置的组件 API ,用来渲染API表格。

效果如下,描述、类型啥的,已经自动帮我们填充好了

相关推荐
小小小小宇18 分钟前
前端模拟一个setTimeout
前端
萌萌哒草头将军22 分钟前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加1 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam2 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖2 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby3 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife3 小时前
Fiber 架构
前端·react.js
3Katrina3 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
hubber3 小时前
一次 SPA 架构下的性能优化实践
前端
可乐只喝可乐4 小时前
从0到1构建一个Agent智能体
前端·typescript·agent