从零开始创建自己的UI组件库

4,627 阅读10分钟

前言

近期,团队内部要求每个人都要做技术分享,思来想去也没有什么高大上的东西可以分享。索性现学现用,花费了几天时间学习后,终于完成了从创建到发布一个小型UI组件库的“任务”。为了方便以后回顾,刻意整理成文章。有兴趣的朋友跟着我一起从零开始,创建并发布自己的第一个UI组件库吧!

创建项目

使用React官方的脚手架create-react-app创建项目 npx create-react-app btns-ts --template typescript,命令中的btns-ts为本次演示组件库的名称。--template typescript表示创建的是TypeScript项目。

关于TypeScript的介绍和使用请参照官方的文档,这里只是代码层面做简单的注释,因为我们的重点不是这个。

TypeScript文档 www.tslang.cn/docs/handbo…

创建组件

为了更好的管理项目,在src下创建components文件夹,用于存放项目中所有的公共组件,为了逻辑更清晰明了,本次只创建Button组件。

image.png

开始写Button组件

Button组件的jsx代码

button组件引入classnames这个库,用于类名的合成,这个库可以很方便的合并多个类名,并且支持条件判断等功能,具体使用请参考它的文档。

classnames文档地址:github.com/JedWatson/c…

// 文件路径src/components/Button/button.tsc

import React, { ReactNode, ButtonHTMLAttributes, AnchorHTMLAttributes, FC } from 'react';
// 类名合并的小工具,安装命令是: npm install classnames @types/classnames --save
import classnames from 'classnames'; 

// 定义按钮的类型和大小
export type ButtonType = 'primary' | 'default' | 'danger' | 'link' | 'dash'
export type Size = 'lg' | 'md' | 'lg'

// 用interface定义按钮props类型约束
interface baseBtnType {
    children?: ReactNode,
    className?: string,
    btnType?: ButtonType,
    size?: Size
}

// 定义两种联合类型,一种是button型的,一种是a标签型的按钮
type NativeButtonProps = baseBtnType & ButtonHTMLAttributes<HTMLElement> // 基础按钮类型,props类型定义
type AnchorButtonProps = baseBtnType & AnchorHTMLAttributes<HTMLElement> // a标签类型的按钮,props类型定义

// 用Partial组合两种类型
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> // 按钮最终的prop类型定义

// 使用泛型定义函数的入参及返回值类型
export const Button: FC<ButtonProps> = (props) => {
    let {
        className,
        children,
        btnType,
        size,
        ...restProps
    } = props

    // 类名合并工具classnames
    const finalClassName = classnames('dd-btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
    })

    return (
        <button
            className={finalClassName}
            {...restProps}
        >
            {
                children
            }
        </button>
    )
}

Button.defaultProps = {
    disabled: false,
    btnType: 'primary',
    size: 'md'
}

export default Button

Button组件的css代码

为了文章不那么长(容易吓跑读者),Button的样式文件,只列出一部分,文末会给出项目的Github地址。

/*文件路径:src/components/Button/_button.scss*/

/*重置button的默认样式*/
button {
    outline: 0;
    // border: none;
    border: 1px solid #d9d9d9;
}

/*button的基础样式,使用了sass的变量和mixins*/
.dd-btn {
    position: relative;
    display: inline-block;
    font-weight: $btn-font-weight;
    line-height: $btn-line-height;
    color: $body-color;
    white-space: nowrap;
    text-align: center;
    vertical-align: middle;
    background-image: none;
    user-select: none;
    @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-border-radius);
    cursor: pointer;
    &.disabled,
    &[disabled] {
        cursor: not-allowed;
        opacity: $btn-disabled-opacity;
        box-shadow: none;
        > * {
            pointer-events: none;
        }
    }
}
/*...其他略*/

预览Button组件的效果

组件预览就简单了,直接引入到app.tsx文件中,运行npm start命令就可以在浏览器查看效果了。

文件路径:src/app.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
import Button from './components/Button'; // 引入Button组件
import './styles/index.scss'; // 引入样式

function App() {
  return (
    <div className="App">
      <h1>Button</h1>
      <h2>不同类型的按钮</h2>
      <div style={{ display: 'flex', alignItems: 'flex-start' }}>
        <Button btnType="default">default</Button>
        <Button btnType="primary">primary</Button>
        <Button btnType="danger">danger</Button>
        <Button btnType="dash">dash</Button>
        <Button btnType="link">link</Button>
      </div>
    </div>
  );
}

export default App;

效果图如下:

image.png

组件库样式管理

细心的同学可能已经发现,button.tsx中并没有引入_button.scss样式文件,样式为何会生效?答案就是在其他地方引入了,这就是我接下来要介绍的——组件库的样式

既然是写组件库,自然会有很多的样式文件,如何组织样式文件才更利于维护和复用?答案就是利用Sass来书写样式。因为Sass支持模块化,变量,mixins等原生css没有的特性。废话不多说,一起看看如何管理组件库的样式的。

如下图所示,在src目录下创建styles文件夹,用于存放不同功能的sass样式文件。

image.png

每个scss文件的作用如下:

  • _animation.scss存放所有css动画
  • _minxins.scss 存放sass的mixin函数,把公共的功能抽离成sass函数,达到更好的复用
  • _variables.scss 存放所有的sass变量,这个是写组件库的必备,方便后期实现一键换主题等功能
  • index.scss styles入口文件,用于引入项目中所有的scss文件(包括组件的样式文件)

一起来看看index.scss文件的真面目:

// normalize
@import '../../node_modules/normalize-scss/sass/normalize';
@include normalize();

// config
@import "variables";

// mixins
@import "mixins";

// 引入Button组件的样式
@import "../components/Button/button";

留意最后一行,我们就是在这里引入Button组件的样式的,细心的小伙伴可能已经发现,_button.scss和_ariables.scss等文件都是下划线开头的。这是Sass的特性之一,下划线开头的sass文件表示内部样式文件,可以使用@import 文件名(去掉下划线和后缀) 的方式引入。

这里的样式引入顺序也很关键,首先引入_normalize.scss抹平不同浏览器的差异,接着引入_variables.scss_mixins.scss文件,组件样式放到最后,这样每个组件就可以直接使用前面定义好的变量和mixins,开发新组件时,在文件的末尾引入其样式即可。

组件库打包

组件写好了,也预览了功能,接下来的步骤就是组件的打包。所谓打包,就是把src下的typescript代码打包到特定的文件目录下(多么通俗易懂的解释!)。为了完成这个简单的打包过程,我们需要完成以下几个步骤。

1、配置tsconfig.build.json文件

使用文章开头创建项目的命令npx create-react-app btns-ts --template typescript,创建出来的项目目录下本身就有一个tsconfig.json文件,为啥我们还要再创建一个?因为这样解释起来更清晰啊!!

image.png

tsconfig.build.json是一个配置文件,每个配置项的含义请看注释。

文件路径:项目根目录/tsconfig.build.json

{
    "compilerOptions": {
        "outDir": "build", // 打包输出的路径
        "module": "esnext",  // 指定生成哪个模块系统代码
        "target": "es5", // 指定ECMAScript目标版本(默认"ES3")
        "declaration": true, // 是否生成相应的 .d.ts文件。
        "declarationDir":"./build", // 生成.d.ts文件的路径
        "jsx": "react", // 在 .tsx文件里支持JSX: "React"或 "Preserve"
        "moduleResolution": "node", // 决定如何处理模块。或者是"Node"对于Node.js/io.js,或者是"Classic"(默认)。
        "allowSyntheticDefaultImports": true // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
    },
    "include": [
        "src"
    ],
    "exclude": [
        "src/**/*.test.tsx",
        "src/**/*.stories.tsx",
        "src/stories/"
    ]
}

更多的配置项,请参照官方文档

typescript文档地址: www.tslang.cn/docs/handbo…

2、配置package.json文件

配置package.json文件,是为了打包和发包做准备,name,version等配置都比较简单,大家看注释就好。需要注意的是,有注释的是发包必须配置的选项!

这里我们只着重讲打包命令,即build-lib,该命令是一个组合命令,它首先会调用clean命令,清空原来的build文件夹;接着调用build-ts命令,将typescript代码编译成js代码;最后调用的是build-css,将scss文件编译成css文件。

配置和命令写好了,是不是可以打包了?理论上是的,但其实还不可以!还需要修改一下index.tsx文件。

文件路径:项目根目录/package.json

"name": "btns-ts", // 组件库名称,如果重明就换一个或加上自己的前缀,例如: @linlif-btns-ts
"version": "0.1.0", // 版本号,每次发包到npm的版本号都要不一样,且要遵从命名规范
"private": false, // 是否私有,修改为false
"main": "build/index.js", // 组件库的入口
"module": "build/index.js", // 组件库es模块入口
"types": "build/index.d.ts", // 组件库types声明入口
"decription": "A wonderful react UI component", // 组件库描述
"author": "linlif", // 作者
"keyword": [ // 关键词
  "UI",
  "React",
  "Component"
],
...省略...
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "build-ts": "tsc -p tsconfig.build.json",
  "build-css": "node-sass ./src/styles/index.scss ./build/index.css",
  "clean": "rimraf ./build",
  "build-lib": "npm run clean && npm run build-ts && npm run build-css"
},
...省略...

3、修改index.tsx文件

为什么要修改入口文件?因为我们写的是React组件库,不是React应用!

默认情况下,index.tsx的内容如下:

文件路径:src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

这一坨内容的意思是,使用ReactDOM的render方法,将APP.tsx这个组件,渲染到index.html中的,id为root的div中。现在,我们把所有内容都删掉!然后加上一条代码。

修改后,index.tsx的内容如下所示:

文件路径:src/index.tsx

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

// 多个组件时,就多写几条导出语句咯,这不用我教你吧!

这么修改是告诉打包工具,我现在要导出的是一个个的组件,而不是将代码打包到index.html文件中。 经过以上三步的修改,你就可以允许打包命令了。是不是很累,累就对了!我写作也很累啊,你怎么不点个赞?

4、运行打包命令

打开控制台,cd到项目的根目录(package.json所在目录叫根目录),输入npm run build-lib,稍等片刻,就可以看到根目录下出现build文件夹,这个文件夹就是打包的产物了。

image.png

使用npm link调试组件库

经过一翻翻云覆雨,咳咳,是一波辛苦的操作后,组件库终于打包出来,接下来就是调试了。

组件库的调试,需要创建另一个项目(总不能自己调试自己吧?!)才可以完成。创建项目相信大家已经很熟悉了(create-react-app 一把梭!不会的请参照文章开头),这里不再赘述!

如何使用npm link命令调试本地的npm包?

假设npm包名称(即组件库名称)为:btns-ts,测试项目名称为:my-project

// 先去到btns-ts的根目录,把它 link 到全局
# cd path/to/btns-ts
# npm link

// 再去项目目录通过包名来 link,需要注意的是,my-project项目的node_modules目录中不要有btns-ts包,如果有请删除
# cd path/to/my-project
# npm link btns-ts

执行完以上操作后,打开my-project项目的App.tsx文件,引入组件库中的Button组件,并使用它,看看各项功能是否符合预期,这就是调试的过程了。

npm link本地调试的好处在于,当你的组件库修改重新打包后(无需运行npm link命令),测试项目中就可以立刻看到效果。该命令的存在就是为了方便调试组件,避免每一次调试都要发包的尴尬!

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { Button } from 'btns-ts' // 引入组件库中的Button组件
import 'dd-ui-react/build/index.css'

const App: React.FC = () => {
    return (
        <div style={{ padding: 20 }}>
            <h2>btn-ts Button 示例</h2>
            <Button btnType="primary">Primary</Button> // 使用Button组件
        </div>
    );
}

export default App;

看看调试效果吧:

image.png

调试完以后,你可能需要取消/删除npm link的包。

// 取消link(取消 my-project 和 my-utils 的 link 关系)
# cd path/to/my-project
# npm unlink my-utils

// 删除link(删除全局 my-utils 的 link# cd path/to/my-utils
# npm unlink my-utils

将组件库发布到NPM

经过严格的本地调试,组件库已经比较完善,终于可以将组件库发布到NPM了(God!鬼知道我经历了什么~)。

1、第一步,注册一个npm账号(已注册请跳过),注册的地址为:www.npmjs.com/signup

2、cd到项目的根目录,如果是第一次发包,执行npm adduser添加用户,如果不是第一次了,就执行npm login登录你的账号。由于我已经不是第一次,所以只能演示login给你们看了。

image.png

3、输入npm publish执行发布,等命令执行完,登录到npm,点击头像,点击packages,就可以看到我们刚刚发布的组件库了。是不是很简单!

image.png

结语

看到这里,相信你已了解如何使用TypeScript和React创建组件库、如何调试和发布组件库。此次手把手只是一次开始,更多深入的知识还需要大家自行扩展。我就不扩展了,咻~闪现溜走...

组件库地址: github.com/linlif/btns…