在日常的开发中我们早已厌倦了反复无常的码代码,总有一颗躁动的心,自己整一个来玩玩,虽然市面上已有的组件库已经很完善了,但还是想体验一把用自己组件库的成就感,顺带也提升提升自己!
涉及技术: Typescript
Create-React-AppsassJestStorybookClassnamesHuskyFontawesome
Create-React-App 搭建工程
普通模板:
npx create-react-app my-app
cd my-app
npm start
typescript模板:
npx create-react-app my-app --template typescript
规划目录结构
├ .storybook
├ main.js storybook配置,包括自定义的一些配置
├ preview.js 页面展示相关配置
├ manager.js 插件设置配置 (页面很多东西都是可以自行配置)
├ yourTheme.js 例子:更改左上角logo
├ .vscode vscode配置
├ public 公共文件
├ src 源码
├ components 组件
├ Button
...
├ stories storybook文件
├ button.stories.tsx 示例:button组件引入stories.tsx后就可以预览组件
...
├ styles 样式文件
├ _mixin.scss 全局mixin
├ _reboot.scss 全局样式, 类型reset.css
├ _varialbes.scss 全局样式变量定义
├ inde.scss 所有样式文件引入一并管理
├ index.tsx 入口
├ .gitignore git
├ package.json package
├ package-lock.json package包版本号固定
├ yarn.lock
├ tsconfig.json ts配置文件
├ tsconfig.build.json 组件编译
├ README.md 说明文档
SCSS
- 确定好全局样式,后续用到直接以变量的形式使用,便于管理
_variables.scss
基本色
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
主题色
$blue: #0d6efd !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #d63384 !default;
$red: #dc3545 !default;
$orange: #fd7e14 !default;
$yellow: #fadb14 !default;
$green: #52c41a !default;
$teal: #20c997 !default;
$cyan: #17a2b8 !default;
// 字体大小
$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`
$font-size-lg: $font-size-base * 1.25 !default;
$font-size-sm: $font-size-base * .875 !default;
$font-size-root: null !default;
...
- 全局的 normalize.css
- 全局 minxin
定义
@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) {
padding: $padding-y $padding-x;
font-size: $font-size;
border-radius: $border-radius;
}
使用
@include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $border-radius);
- 样式集成管理 注意文件名定义 _xxxx.scss 引用的时候 xxxx , 可以省略 _ .scss
// config
@import "variables";
//layout
@import "reboot";
//mixin
@import "mixin";
// animation
@import "animation";
// button样式
@import "../components/Button/style";
// icon
@import "../components/Icon/style";
// menu样式
@import "../components/Menu/style";
// input样式
@import "../components/Input/style";
//upload
@import "../components/Upload/style";
//progress
@import "../components/Progress/style";
当然还有其他高级的功能,可自行学习
组件编写
文件布局方式
├ Button
├ _style.scss 组件样式
├ button.tsx 组件源码
├ button.test.tsx 组件单元测试
├ index.tsx 组件导出
button.tsx 示例
/*
* @Descripttion:
* @version:
* @Author: zoucw (326359613@qq.com)
* @Date: 2021-02-14 13:54:04
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-02-17 16:42:36
*/
import React from 'react';
import classNames from 'classnames';
export type ButtonSize = 'lg' | 'sm'
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'
interface BaseButtonProps {
className?: string;
/**设置 Button 的禁用 */
disabled?: boolean;
/**设置 Button 的尺寸 */
size?: ButtonSize;
/**设置 Button 的类型 */
btnType?: ButtonType;
children: React.ReactNode;
href?: string;
}
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>
// Partial把所有属性变成可选属性
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
const Button: React.FC<ButtonProps> = (props) => {
const {
btnType,
className,
disabled,
size,
children,
href,
...restProps
} = props;
// btn btn-lg btn-primary btn-default
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === 'link') && disabled
})
if (btnType === 'link' && href) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled}
{...restProps}
>
{children}
</button>
)
}
}
Button.defaultProps = {
disabled: false,
btnType: 'default'
}
export default Button
组件预览调试
过往看别人项目预览组件都是在项目里搭建页面来展示组件,这里给大家推荐一个UI组件预览神器,只需手动配置就可以展示组件测试组件而且可以展示组件源码,它就是Storybook
这里不做过多介绍,安装完跑起来很容易,其他一些自定义的展示功能要仔细研究研究
安装
npx sb init
npm run storybook
在你的项目根目录下执行npx sb init后,storybook会自动在你的项目根目录新增.storybook文件夹,在你的component文件夹的平级处新增stories文件夹,可以按照已有的实例模仿研究一下
新增组件预览 stories -> xxx.stories.tsx -> 引入自己的组件
配置完成大概样式就长这么样,很多样式可以自定义哦
发布前的编译
- 首先是要编译tsx文件为js
"scripts": {
...
"build-ts": "tsc -p tsconfig.build.json",
}
tsconfig.build.json
{
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"target": "es5",
"declaration": true,
"jsx": "react",
"moduleResolution":"Node",
"allowSyntheticDefaultImports": true,
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.tsx",
"src/**/*.stories.tsx",
"src/setupTests.ts",
"src/reportWebVitals.ts"
]
}
- 编译scss
"scripts": {
"build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
}
编译完成之后会构建出如下文件
注意: 我们在开发组件的过程,迭代了功能怎么去模拟引入生产的组件来测试? 1.完善package.json配置
{
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
}
2.组件项目的根目录下执行 npm link
3.切换到测试项目的根目录,执行npm link name (注: 这个name是你组件项目package.json里的name)
4.打开测试项目的node_modules,看是否有名称为 name 的依赖包且后面带有一个icon,说明link成功
5.在你的测试项目引入组件一样使用(npm安装的包怎么用就怎么用)
当你测试到某些组件是又会报一个错误:❌
大概意思就是引入的组件内部
import的react与测试项目的react产生了冲突关系
咋办,继续link UI组件项目react到当前的测试项目
示例
npm link ../cute-spring/node_modules/react
此时你是不是会想引用npm包是也这样吗? No,请继续往下看
Npm 发布
1.你是否已经注册npm账号(如果没有,点击注册)
2.继续完善package.json配置项,因为npm展示组件包的信息都是来自package.json里配置的内容
{
...
"description": "this is my component",
// 配置入口文件
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"author": "xxx",
"private": false,
"license": "MIT",
"keywords": [
"Component",
"UI",
"React"
],
"homepage": "https://github.com/Spring-Listening/cute",
"repository": {
"type": "git",
"url": "https://github.com/Spring-Listening/cute"
},
// 这一项配置是关键,因为`npm publish`发布时会上传两部分文件,一部分是用户定义在files里,一部分是默认上传的文件,比如readme.md package.json
"files": [
"dist"
],
...
}
3.解决npm包与使用组件项目react冲突的问题
首先把package.json里的依赖 devDependencies Dependencies 关系处理好,开发过程用到的依赖包放devDependencies, 组件库正常使用就需要依赖的包放Dependencies
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
tips: 意思是安装此UI组件之前,你的开发项目必须安装符合如上要求的依赖包,进而组件库内的组件可以使用开发项目的依赖,进而组件库可以不用去安装重复的依赖,也不会有上面那种冲突的现象
4.完善一些工程化相关的东西,比如eslint 组件单元测试 husky
{
"scripts": {
// eslint
"lint": "eslint --ext js,ts,tsx src --max-warnings 5",
// 单元测试
"test:nowatch": "cross-env CI=true react-scripts test",
// 编译ts scss文件
"build": "npm run clean && npm run build-ts && npm run build-css",
// 执行npm publish时会执行这条命令,然后将构建好文件上传npm服务器
"prepare": "npm run build",
},
"husky": {
"hooks": {
"pre-commit": "npm run test:nowatch && npm run lint"
}
},
}
执行 npm publish
人生的路总是不会那么平坦,你可能会遇见各种403错误导致上传失败
当你遇到困难时不要灰心,按照这个自检清单逐一排查:
1.检查是否登录了npm (npm whoami)
2.你是否有多个账号记混了,退出重新登录
3.你注册号时所填的邮箱是否被验证了,如果没有刷新页面点击顶部通知栏重发验证邮件
4.检查你的包名是否已被占用
5.检查你的版本号是否是用过的版本号
6.检查你的npm源是否是npm官方源(npm config list)
Jest 单元测试
在日常的工作中一般业务可能都不会去搞这个单元测试,一来是业务频繁变化,测试用例没法复用,导致测试用例使用率不高,造成效率的低下。二来写测试用户也会对效率打一定的折扣,不符合当今资本家的嘴脸。但是对于这种组件开发,业务逻辑稳定,不太容易完全推翻重做,比较适合来做单元测试。
测试的依赖包主要分三部分:Jest、jest-dom、@testing-library/react (或者是其他框架的包) jest主要是这个测试的核心包,提供了一个基础功能的api,因为在日常的开发中,涉及到dom方面的东西,jest-dom这个包主要提供了跟dom相关的api,@testing-library这个包主要是提供了当前市面上用的比较多的框架配套的测试api
以上是jest提供的一些基础api
toBe() 用于检验基本数据类型的值是否相等
toEqual() 用于检验引用数据类型的值,由于js本身object数据类型的本身特性,引用数据类型对比只是指针的对比,但是需要对比对象的每个值,所以这时候用到的是toEqual()
Truthiness 布尔值判断的匹配器
toBeNull 只匹配 null
toBeUndefined 只匹配 undefined
toBeDefined 与 toBeUndefined 相反
toBeTruthy 匹配任何 if 语句为真
toBeFalsy 匹配任何 if 语句为假
数字匹配器 用于判断数字值之间的对比
toBeGreaterThan 大于匹配器
toBeGreaterThanOrEqual 大于等于匹配器
toBeLessThan 小于匹配器
toBeLessThanOrEqual 小于等于匹配器
tobe 和 toequal 都是等价功能相同的对于数字
toMatch 字符串匹配器 和字符串的match相同
toContain 数组匹配器 用于判断数组中是否包含某些值
toThrow 报错匹配器 用于测试特定的抛出错误,可以判断报错语句的文字(支持正则匹配),也可以判断报错类型。
异步测试示例
// 提前了解async和await的使用方法和场景
test('the data is peanut butter', async () => {
// 切记添加断言测试
expect.assertions(1);
const data = await fetchData();
expect(data).toBe('peanut butter');
});
@testing-library/react
import {render, fireEvent} from '@testing-library/react'
test('should render the correct default button', () => {
const warpper = render(<Button {...defaultProps}>Nice</Button>)
const element = warpper.getByText('Nice') as HTMLButtonElement
expect(element).toBeInTheDocument()
expect(element.tagName).toEqual('BUTTON')
expect(element).toHaveClass('btn btn-default')
expect(element.disabled).toBeFalsy()
// 模拟点击事件
fireEvent.click(element)
expect(defaultProps.onClick).toHaveBeenCalled()
})
更多有关react的测试示例@testing-library/react
jest-dom
这些dom相关的api语义还是很明显的,看了api名就知道是什么功能,具体用法可以看这个文档 github.com/testing-lib…
CI CD 持续集成,持续部署
组件开发完后,我们的组件文档怎么发到网上去,而且是一键发布,不用手动去发,繁琐!下面我们来优化一下流程
- 打开travis www.travis-ci.com/
- 使用GitHub授权登陆,此时就可以选中本次的组件库到Travis
- 在项目添加一个文件,没有这个没法自动触发这套流程
.travis.yml
// 语言
language: node_js
// node版本
node_js:
- "stable"
cache:
directories:
- node_modules
// 环境
env:
- CI=true
script:
// 构建命令
- npm run build-storybook
// 下面这一段的意思是把构建好的包发到 `GitHub` 的 `pages`
deploy:
provider: pages
skip_cleanup: true
// 配置token 关键
github_token: $github_token
// 构建出来的包的文件夹名称
local_dir: storybook-static
on:
branch: master
配置GitHub-token
GitHub -> 点击头像下拉箭头 -> settings -> developer settings -> personal access tokens -> generate new token
在生成token之前你需要勾选这个token有哪些权限,勾选后, 点击按钮创建,如下图
创建token成功,一定要复制下来
Travis配置刚生成的token
首页右上角 -> more options -> settings
一切配置已完毕,提交代码,就会触发cicd流程
成功触发
怎么访问到我们发上去的包
spring-listening.github.io/包名/?path=/
完结
纸上得来终觉浅,绝知此事要躬行
无论你研究别人的文档有多清楚,实践起来总会遇到各种问题,所以动起来
有不足的地方希望各位大佬补充,共同进步,大家好才是真的好!