手记

Deer-ui:一个简单高效的react组件库

1、前言

自己一直想搭建一套自己的博客系统,今年年末终于有时间开发,在开发后台cms系统时,使用antd组件库,突然一天问自己,组件库是怎么实现的,内部是怎么运行的,打包部署是怎么个原理,带着这些疑问,我开始了萌生了自己开发一套组件库的想法。说干就干,自己花费了大概2个多月的时间完成了deer-ui第一版的组件开发,现在分享出来,记录下开发过程踩过的坑。

2、组件库开发准备工作

需求准备

首先确定需求,初期先调研了流行的组件库,如antdvantzarm-web,cuke-ui,iview等等,最后选定antd作为ui参考。

技术准备

  • 组件库预览网站技术选择,对比了几个(docz,styleguidist,storebook),最后选择storebook作为展示网站,不仅限于此,项目中还集成了docz,styleguidist打包,如有需要可实现。
  • 组件调试方案,最后确定了两种,可按需求。1、使用源码中使用create-react-app搭建了一个react环境,在example文件夹下,使用npm run dev,即可打开调试环境,引入编写的组件即可.
    2、源码中搭建了一套组件库的文档部署环境,使用命令npm run storybook,即可进入文档模式,引入编写的组件即可.
  • 组件库代码打包,在参考几大流行的组件库后,决定自己手写打包配置文件
  • 打包发版,组件库引入自动化发版,运行脚本后直接发布npm仓库
  • 一期组件分类,由于时间原因不可能一次性开发完全部的组件,所以一期准备开发下面16个组件

3、搭建项目

3.1 项目结构

  • .storebook storebook 的一些配置
  • components 放置所有组件
  • example 组件调试环境代码
  • scripts 发布,打包的脚本文件
  • stories 项目静态文档,负责 demo 演示
  • 以及一些配置文件

3.2 打包脚手架开发

使用webpack4完成组件库的打包,组件库用户一般通过es modulecommonjs以及script脚本的方式引入,这就需要我们的组件库满足这些规范,通常使用webpack打包成umd规范即可满足上面不同的引入。

  ...
  mode: "production",
  entry: {
    [name]: ["./components/index.js"]   
  },
  output: {
    path: path.join(process.cwd(), "dist"),
    library: name,
    libraryTarget: "umd",
    umdNamedDefine: true,
    filename: "index.js",
  },
  ...

打包的入口是components/index.js文件,主要是导出组件

export { default as Button } from './button';
export { default as Tabs } from './tabs';
export { default as Icon } from './icons';

最后打包后会在dist目录下生成index.js文件,为所有打包后组件。webpack其他配置不一一详述了,如有需要查看scripts目录。

3.3按需加载组件打包

3.3.1 打包js文件

通过上述webpack打包的组件库代码,是全量引入的,所有的组件代码都打包在一起。为了减小引入组件库大小,一般都采用按需加载。按需加载的核心是要单独将每个组件打包出来,通过import方式单独引入。

通过 babel 打包js文件
"build:lib": "cross-env OUTPUT_MODULE=commonjs babel components -d lib" 
"build:es": "babel components -d es",

3.3.2 打包样式文件

将各组件的样式文件提取到lib,es文件对应的组件下面,css打包代码如下,这块是借鉴cuke-ui的配置,通过gulp流的方式,打包css文件。

/**
 * @name gulpfile.js
 * @description 打包项目css依赖
 */

const path = require("path");
const gulp = require("gulp");
const concat = require("gulp-concat");
const less = require("gulp-less");
const autoprefixer = require("gulp-autoprefixer");
const cssnano = require("gulp-cssnano");
const size = require("gulp-filesize");
const sourcemaps = require("gulp-sourcemaps");
const rename = require("gulp-rename");
const { name,browserList } = require("../package.json");
const DIR = {
  less: path.resolve(__dirname, "../components/**/*.less"),
  buildSrc: [
    path.resolve(__dirname, "../components/**/style.less"),
    path.resolve(__dirname, "../components/**/index.less")
  ],
  lib: path.resolve(__dirname, "../lib"),
  es: path.resolve(__dirname, "../es"),
  dist: path.resolve(__dirname, "../dist")
};

gulp.task("copyLess", () => {
  return gulp
    .src(DIR.less)
    .pipe(gulp.dest(DIR.lib))
    .pipe(gulp.dest(DIR.es));
});

gulp.task("copyCss", () => {
  return gulp
    .src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(
      less({
        outputStyle: "compressed"
      })
    )
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(size())
    .pipe(cssnano())
    .pipe(gulp.dest(DIR.lib))
    .pipe(gulp.dest(DIR.es));
});

gulp.task("dist", () => {
  return gulp
    .src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(
      less({
        outputStyle: "compressed"
      })
    )
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(concat(`${name}.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))

    .pipe(cssnano())
    .pipe(concat(`${name}.min.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(size())
    .pipe(gulp.dest(DIR.dist));
});

gulp.task("default", gulp.series(["copyLess", "copyCss", "dist"]));

3.3.3组件库文档搭建

文档采用storybook来搭建,文档地址预览,具体配置如下

import React from "react";
import { configure, addDecorator,addParameters } from '@storybook/react';
const { name, repository, version } = require("../package.json")
import { configureActions } from '@storybook/addon-actions';
import '@storybook/addon-console';
import '@storybook/addon-options/register';
import "../stories/style/index.less"
import "../stories/style/code.less"

function loadStories() {
  // 介绍
  require('../stories/index');
  //基础组件
  require('../stories/basis');
  //数据展示
  require('../stories/showData');
  //操作反馈
  require('../stories/feedback');
  //交互组件
  require('../stories/interaction');
  //布局组件
  require('../stories/layout');
}

configureActions({
  depth: 100
})


//加载配置
addParameters({
  options: {
    name: `${name} v${version}`,
    title: "Deer-ui",
    url: repository,
    showSearchBox: false,
    showPanel: false,
    enableShortcuts:false,
    isToolshown: false,
    selectedPanel: undefined,
    hierarchySeparator: null,
    hierarchyRootSeparator: null,
    showAddonPanel: false,
  }})
//中间content边距
addDecorator(story => <div style={{ padding: "0 60px 50px" }}>{story()}</div>)
configure(loadStories, module);

编写组件文档

storiesOf("操作反馈", module)
  .add("Spin 加载中", () => (
    <div>
      <h4>基本使用</h4>
      <div style={{ marginBottom: "30px" }}>
        <Spin />
      </div>
      <CodeView
        value={`
        import { Spin } from 'deer-ui'
        <Spin />
      `}
      ></CodeView>
     
  ))

最后单独给storybook配置webpack.config.js,注意配置格式和一般的webpack有点区别

const webpack = require("webpack");
module.exports = async ({ config, mode }) => {
  config.resolve = {
    extensions: [".js", ".jsx", ".json", ".jsx"]
  };

  config.module.rules.push({
    test: /\.(js|jsx)?$/,
    loaders: [require.resolve("@storybook/source-loader")],
    enforce: "pre",
    exclude: /node_modules/
  });

  config.module.rules.push({
    test: /\.scss$/,
    use: [
      "style-loader",
      "css-loader",
      "postcss-loader",
      {
        loader: "sass-loader"
      }
    ]
  },
  {
    test: /\.less$/,
    use: [
      "style-loader",
      "css-loader",
      "postcss-loader",
      {
        loader: "less-loader",
        options: {
          javascriptEnabled: true
        },
      }
    ]
  });
  config.plugins.push(
    new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn|en-gb/)
  );
  return config;
};

4、开发组件

项目框架已经搭建完成,在components目录下就可以开发组件了,例如button组件,删减版

...省略
class Button extends Component {
    return (
      <div className={cls(prefixCls, {[`${prefixCls}-block`]:block})}>
        <button
          type="button"
          {...isDisabled}
          className={cls(`${prefixCls}-btn`,className,`${prefixCls}-btn-${type}`
          </button>
      </div>
    );
  }
}

export default Button;

在index.js文件中导出button组件

export { default as Button } from './button';

5、发布

开发完成组件库后,通过把代码发布到npm仓库即可,默认大家都会发布npm包,如果有不了解请参考掘金上有关npm问题。框架提供了自动化发布命令,打包,发版,lint,日志等功能

5.1 发布组件库

    npm run pub:prod    //自动完成css,js,es,lin,umd打包,自动生成changelog,发布npm仓库,为修订版版本号。1.0.*
    
    npm run pub:major  //都会完成上述不同,唯一区别是,打的npm版本号不同,此命令是打主版本号,不经常用 *.0.0
    
    npm run pub:minor  //都会完成上述不同,唯一区别是,打的npm版本号不同,此命令是打次版本号,不经常用 1.*.0

    "pub:prod": "npm run standard:patch && npm run build && npm publish --registry https://registry.npmjs.org && git push" //打版本号和发版

5.2 发布组件库文档

组件库提供两种发布方式

1、npm run pub:docs   采用storybook的方式去发布,该方式需要在package.json中配置账号信息。
"storybook-deployer": {
    "gitUsername": "deer-ui",
    "gitEmail": "your email",
    "commitMessage": "docs: deploy docs"
},

2.npm run deploy  //该命令会执行脚本deploy.sh文件,打包并发布组件库文档

#!/bin/bash

# 确保脚本抛出遇到的错误
set -e
echo "start build..."
# 打包文档
npm run build:docs

echo "√ build success"

# 进入生成的文件夹
cd .docs

echo "start publish..."
# 提交到  gh-pages
git config  --get remote.origin.url
git init
git config user.name "xxxx"
git config user.email "xxxx"
git add .
git commit -m 'docs:publish'

git push --force --quiet git@github.com:zhangboyang123/deer-ui.git master:gh-pages
echo "√ publish success ?"

cd -

6、主题定制

6.1、主题变量

Deer-ui使用less作为样式开发语言,并定义了一系列全局/组件的样式变量,你可以根据需求进行相应调整。
以下是一些最常用的通用变量,所有样式变量可以在 这里 找到。

@primary-color: #31c27c;   //全局色
@warning-color: #fca130;    //警告色
@error-color: #f93e3e;      //失败色
@success-color: #35C613;    //成功色
@info-color: #61affe;       //信息展示色
@default-color: #d9d9d9;    //默认色
@border-color: #e8e8e8;     //边框颜色
@border-radius: 4px;        //边框圆角
@font-size: 14px;           //默认组件字体大小
@font-size-small: 12px;     //小字体
@font-size-large: 16px;     //大字体
@bg-color: #FAFAFA;         //组件背景色
@font-color: rgba(0, 0, 0, .65);    //字体颜色
@disabled-font-color: fade(@font-color, 30%);  //禁用字体颜色

6.2 主题定制原理

主题定制原理上是使用 less 提供的 modifyVars 的方式进行覆盖变量。使用webpack中配置less-loaderoptions。注意javascriptEnabled要打开。

// webpack.config.js
module.exports = {
  rules: [{
    test: /\.less$/,
    use: [{
      loader: 'style-loader',
    }, {
      loader: 'css-loader', // translates CSS into CommonJS
    }, {
      loader: 'less-loader', // compiles Less to CSS
+     options: {
+       modifyVars: {
+         'primary-color': '#1DA57A',
+         'info-color': '#1DA57A',
+         'font-size': '12px',
+         // or
+         'hack': `true; @import "your-less-file-path.less";`, // 或者引用本地样式文件覆盖
+       },
+       javascriptEnabled: true,
+     },
    }],
  }],
}

注意,定制主题后,less-loader 的处理范围不能过滤掉 node_modules 下的 deer-ui 包。

7、按需加载

7.1 单独引入

import Button from 'deer-ui/es/button';
import 'deer-ui/es/button/style.less';
// 单独使用在.babelrc.js中配置
module.exports = {
  plugins: [
    ["import", {
      "libraryName": "deer-ui",
      "libraryDirectory": "es",
      "style":true
    },"deer-ui"], 
  ]
}
// 多个组件库,例如antd
module.exports = {
  plugins: [
    ["import", {
      "libraryName": "deer-ui",
      "libraryDirectory": "es",    
      "style": true            
    },'deer-ui'], 
    
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true  
    },'antd'], 
  ]

8、后续计划

  • 第一阶段组件已经开发完毕,基本完成message,input,radio,button,table,checkbox,collapse,tabs,empty.loading,icon,divider等基础组件的开发;完成Deer-ui组件库框架搭建,实现自动化打包部署,增加stylelint,eslint,commitlint,自动生成changelog,组件库测试环境搭建,组件库官方文档网站搭建,以及主题定制等功能。

  • 后续增加组件库的自动化测试,国际化功能。

  • 继续完成后面组件的开发。

  • 最后畅想下,使用ts完成组件库的重构。

    9、总结

    开发完组件库,也许没啥意义, 但是通过这个组件库, 让我学到了很多平时 接触不到的知识点,有时看着很简单的东西,自己动手会发现里面有好多坑。总体来说,去年给自己定的小目标已经实现,今年继续在前端的路上不停的折腾,正所谓,生命不息,coding不止?。

2人推荐
随时随地看视频
慕课网APP