1、组件库文档工具对比
组件库文档的要求:
- 支持组件的渲染,可交互
- 支持markdown
- 列出组件的所有属性(API),包括类型、说明、默认值等
搜罗市面上知名度角度较高的组件库文档框架:docz、storybook、dumi、vuepress对比如下:
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.md 或readme.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表格。

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

