# UI 库

https://create-react-app.dev/docs/setting-up-your-editor/

https://usehooks.com/page/2

# scss 中的 import

css 中的 import 命令,每次调用的时候,都会创建一个额外的 http 请求。但是 scss 的 import 是将文件包含在 css 中,不需要额外的 http 请求。我们以 _ 开头定义文件, scss 是不会将这些文件编译到 css 文件中;没法编译成单独的 css 文件,只能被导入;但是我们在导入这些文件的时候,不需要添加下划线。

// config
@import "variables";

// layout
@import "reboot"

然后在 index.tsx 中进行引入我们的初始化样式:

import './styles/index.scss';

Sass 作用域是从上到下的,在 button 的样式中引用了 variables 中定义的变量,就要放在下面:

// config
@import "variables";

// layout
@import "reboot";

// button
@import "../components/Button/style";

# 使用 classnames 库

安装 npm install classnames --save; npm install @types/classnames --save 使用:

import classNames from 'classnames'
// 默认有 btn 类以及外部传入的 className
const classes = classNames('btn', className, {
  [`btn-${btnType}`]: btnType,
  [`btn-${size}`]: size,
  'disabled': (btnType === ButtonType.Link) && disabled
})
return (
<a
  className = { classes }
  href={href}
  // 其他属性
  {...restProps}
>
  { children }
</a>
)

# 使用联合类型合并 DOM 原生属性以及新增属性;使用 Partial 将属性变为可选属性

interface BaseButtionProps {
  className?: string;
  disabled?: boolean;
  size?: ButtonSize;
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string
}
// 联合类型
type NativeButtonProps = BaseButtionProps & React.ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtionProps & React.AnchorHTMLAttributes<HTMLElement>
// 两种类型的按钮 a 或者 button 的属性不一定两种都需要,需要变为可选属性,使用 Partial
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>

# scss 中的 @each 、Maps

$sizes: 40px, 50px, 80px;
@each $size in $sizes {
  // 注意这里选择器的名称要访问变量需要使用 #{} 来包裹
  .icon-#{$size} {
    font-size: $size;
    height: $size;
    width: $size;
  }
}

Maps 类似 js 中的 Object,键值对。使用圆括号来创建

$icons: ("eye": "\f112", "start": "\f12e", "stop": "\f12f");

@each $name, $glyph in $icons {
  .icon-#{$name}:before {
    display: inline-block;
    font-family: "Icon Font";
    content: $glyph;
  }
}

# 增加测试用例

新建 jest.test.js ,react 框架中包含 jest 工具。

// 一个用例。每个用例测试一个独立的功能点
// 第一个参数为用例的名称,第二个是用例的逻辑,一个回调函数
test('test common matcher', () => {
  expect(2 + 2).toBe(4)
  expect(2 + 2).not.toBe(5)
})

test('test to be true or false', () => {
  expect(1).toBeTruthy()
  expect(0).toBeFalsy()
})

test('test number', () => {
  expect(4).toBeGreaterThan(3)
  expect(2).toBeLessThan(3)
})
test('test object', () => {
  // toBe 是完全相同 toEqual 是值相同
  expect({name: 'jiegiser'}).toEqual({name: 'jiegiser'})
})

然后运行命令 npx jest jest.test.js,可以添加 --watch 命令,让他一直运行:npx jest jest.test.js --watch

# 使用 testing-library 工具进行测试 react 组件库相关用例

react-script 3.3.0 版本已经将 testing-library 添加了依赖,如果低于这个版本的需要自己安装,运行命令 npm install --save-dev @testing-library/react 这个只是针对类型的断言,还有一个 @testing-library/jest-dom 是针对 dom 元素的断言,也可以进行安装,新版本的同样已经添加了依赖对于这个库;npm install --save-dev @testing-library/jest-dom; 在新版本中会有一个 setupTests.ts 文件,每当运行 test 或者 npm run test 的时候会先运行这个文件,这个文件可以存放测试的通用的内容;

下面是给 button 组件的测试用例:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps, ButtonSize, ButtonType } from './button'

const defaultProps = {
  // 测试函数是否被调用到
  onClick: jest.fn()
}

const testTypes: ButtonProps = {
  btnType: ButtonType.Primary,
  size: ButtonSize.Large,
  className: 'klass'
}

const disabledProps: ButtonProps = {
  disabled: true,
  // 测试 disabled 状态下是否执行 click 事件
  onClick: jest.fn()
}

// test('our first react test case', () => {
//   // 渲染组件
//   const wrapper = render(<Button>Nice</Button>)
//   const element = wrapper.queryByText('Nice')
//   // 判断按钮是否存在
//   expect(element).toBeTruthy()
//   // 判断组件是否在文档中
//   expect(element).toBeInTheDocument()
// })

// 对测试用例进行分类
describe('test Button component', () => {
  // 第一个参数为描述,it() 跟 test() 是一样的
  it('shoule render the correct default button', () => {
    // 渲染组件
    const wrapper = render(<Button {...defaultProps}>Nice</Button>)
    // dom 查找,查找是否有 Nice 的文本节点
    const element = wrapper.getByText('Nice') as HTMLButtonElement
    // 判断组件是否在文档中
    expect(element).toBeInTheDocument()
    // 测试是否是一个 button
    expect(element.tagName).toEqual('BUTTON')
    // 测试类是否存在
    expect(element).toHaveClass('btn btn-default')
    expect(element.disabled).toBeFalsy()
    // 模拟点击事件
    fireEvent.click(element)
    // onClick 方法被调用到
    expect(defaultProps.onClick).toHaveBeenCalled()
  })
  // 根据不同的 props 显示不同的组件
  it('shoule render the correct component based on different props', () => {
    // 渲染组件
    const wrapper = render(<Button {...testTypes}>Nice</Button>)
    const element = wrapper.getByText('Nice')
    // 判断组件是否在文档中
    expect(element).toBeInTheDocument()
    // 测试传入的 props
    expect(element).toHaveClass('btn-primary btn-lg klass')
  })
  // 测试 link 类型
  it('should render a link when btnType equals link and href is provided', () => {
    // 渲染组件
    const wrapper = render(<Button btnType={ButtonType.Link} href="http://dddd.com">Link</Button>)
    const element = wrapper.getByText('Link')
    // 判断组件是否在文档中
    expect(element).toBeInTheDocument()
    // 测试是否是一个 a 标签
    expect(element.tagName).toEqual('A')
    expect(element).toHaveClass('btn btn-link')
  })
  // 测试 disabled 属性
  it('should render disabled button when disabled set to true', () => {
    // 渲染组件
    const wrapper = render(<Button {...disabledProps}>Nice</Button>)
    const element = wrapper.getByText('Nice') as HTMLButtonElement
    // 判断组件是否在文档中
    expect(element).toBeInTheDocument()
    // 是否有 disabled 属性
    expect(element.disabled).toBeTruthy()
    fireEvent.click(element)
    // 按钮事件没有执行
    expect(disabledProps.onClick).not.toHaveBeenCalled()
  })
})

# 测试中的 beforeEach 钩子函数

如果我们写一个测试用例的时候,每个 case 中都需要一个相同的变量,我们可以将变量放在 beforeEach 函数中:

let wrapper: RenderResult, menuElement: HTMLElement, activeElement: HTMLElement, disabledElement: HTMLElement
describe('test Menu and MenuItem component', () => {
  // 每个 case 执行的时候都会首先进入这个函数,公用的属性以及方法可以写在这里
  beforeEach(() => {
    wrapper = render(generateMenu(testProps))
    // 取 Menu 最外层的元素,需要给 Menu 添加一个 data-testid 属性
    menuElement = wrapper.getByTestId('test-menu')
    // 也可以这样 ------ wrapper.container 是获取该节点
    wrapper.container.getElementsByClassName('jiegiser-menu')
  })
})

# jest 测试中的 cleanup

使用 cleanup() 可以清除之前 case 渲染的的 dom 元素,比如我们在 beforeEach 中渲染的元素。 其他的用例在调用 beforeEach 中渲染的元素,会自动执行 cleanup 方法。

# jest 相关笔记

  • 模拟点击事件
import { fireEvent } from '@testing-library/react'
// 模拟事件
const testProps: MenuProps = {
  defaultIndex: '0',
  onSelect: jest.fn(),
  className: 'test'
}

const thirdItem = wrapper.getByText('xyz')
fireEvent.click(thirdItem)
  • 模拟鼠标移动事件
const dropdownElement = wrapper.getByText('dropdown')
fireEvent.mouseEnter(dropdownElement)
fireEvent.mouseLeave(dropdownElement)
  • 测试代码中模拟样式
const createStyleFile = () => {
  const cssFile: string = `
    .viking-submenu {
      display: none;
    }
    .viking-submenu.menu-opened {
      display:block;
    }
  `
  const style = document.createElement('style')
  style.type = 'text/css'
  style.innerHTML = cssFile
  return style
}
  beforeEach(() => {
    wrapper = render(generateMenu(testProps))
    wrapper.container.append(createStyleFile())
  })
  • 判断节点是否可见
expect(wrapper.queryByText('drop1')).toBeVisible()
  • 给 input 添加 change 事件
const testNode = wrapper.getByPlaceholderText('test-input') as HTMLInputElement
expect(testNode).toBeInTheDocument()
expect(testNode).toHaveClass('jiegiser-input-inner')
fireEvent.change(testNode, { target: { value: '23' } })

# 测试代码中解决异步问题

如果我们写的代码中有异步操作,直接写 case 会不通过,我们需要引入 wait 方法: 我们的代码中有异步操作:

  const handleMouse = (e: React.MouseEvent, toggle: boolean) => {
    clearTimeout(timer)
    e.preventDefault()
    timer = setTimeout(() => {
      setOpen(toggle)
    }, 300)
  }
import { render, RenderResult, fireEvent, wait } from '@testing-library/react'
let wrapper: RenderResult, menuElement: HTMLElement, activeElement: HTMLElement, disabledElement: HTMLElement
describe('test Menu and MenuItem component', () => {
  it('should show dropdown items when hover on subMenu', async () => {
    // queryByText 会返回 HTMLElement 或者 none
    expect(wrapper.queryByText('drop1')).not.toBeVisible()
    // getByText 返回 HTMLElement
    const dropdownElement = wrapper.getByText('dropdown')
    fireEvent.mouseEnter(dropdownElement)
    await wait(() => {
      expect(wrapper.queryByText('drop1')).toBeVisible()
    })
  })
})

# 如果 ts 提示一个变量有两种类型报错

如果提示报错,需要进行判断:

  const passedContext: IMenuContext = {
    // currentActive 可能为 number 或者 undefined ,这里需要做一个判断
    index: currentActive ? currentActive : 0,
    onSelect: handleClick
  }

# type

type 类型,可以类似 Enum 类型使用:

type MenuMode = 'horizontal' | 'vertial'
export interface MenuProps {
  mode?: MenuMode
}

# react 内置的 style 的类型注释

interface MenuProps {
  style: React.CSSProperties;
}

# 将属性混入到实例中

遍历 this.props.children 使用 React.Children.map;复制属性 React.cloneElement 方法,可以将属性混入到新创建的元素。

  const renderChildren = () => {
    return React.Children.map(children, (child, index) => {
      // menuItem 的实例
      const childElement = child as React.FunctionComponentElement<MenuItemProps>
      const { displayName } = childElement.type
      if (displayName === 'MenuItem' || displayName === 'SubMenu') {
        // return child
        return React.cloneElement(childElement, {
          index: index.toString()
        })
      } else {
        console.error("Warning: Menu has a child which is not a MenuItem component")
      }
    })
  }

# 事件类型的注释

比如注释点击事件、change 事件、键盘事件:

  import React, { ChangeEvent, KeyboardEvent } from 'react'
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value.trim()
    setInputValue(value)
    triggerSearch.current = true
  }
  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    switch(e.keyCode) {
      case 13:
        if (suggestions[highlightIndex]) {
          handleSelect(suggestions[highlightIndex])
        }
        break
      case 38:
        highlight(highlightIndex - 1)
        break
      case 40:
        highlight(highlightIndex + 1)
        break
      case 27:
        setShowDropdown(false)
        break
      default:
        break
    }
  }

# css 中的 :scope 伪类

:scope 属于 CSS 伪类,它表示作为选择器要匹配的参考点的元素。当需要获取已检索到的的直接后代元素时,:scope 伪类很有用。

var context = document.getElementById('context');
var selected = context.querySelectorAll(':scope > div');

document.getElementById('results').innerHTML = Array.prototype.map.call(selected, function (element) {
    return '#' + element.getAttribute('id');
}).join(', ');
<div id="context">
    <div id="element-1">
        <div id="element-1.1"></div>
        <div id="element-1.2"></div>
    </div>
    <div id="element-2">
        <div id="element-2.1"></div>
    </div>
</div>
<p>
    Selected elements ids :
    <span id="results"></span>
</p>

结果: Selected elements ids : #element-1, #element-2

# sass 中的循环

使用 maps 创建可迭代的对象,然后通过 @each 进行循环;

$theme-colors: 
(
  "primary":    $primary,
  "secondary":  $secondary,
  "success":    $success,
  "info":       $info,
  "warning":    $warning,
  "danger":     $danger,
  "light":      $light,
  "dark":       $dark
);

@each $key, $val in $theme-colors {
  .icon-#{$key} {
    color: $val;
  }
}

# css 中 display 以及 react 中的动画

css 中的 display: none 到 display: block 的变化中添加 translate 是不起作用的;所以我们在设置一个元素从无到有的渐变只能通过 opacity 属性来实现,但是这个属性只是设置元素的透明度,元素本身还是显示在那里,存在于 dom 结构中。这时候我们可以借助 react 的一些动画实现的库,这里使用 react-transition-group ,他在元素从无到有的生命周期上添加了 class 来控制动画;

import React from 'react'
import { CSSTransition } from 'react-transition-group'
import { CSSTransitionProps } from 'react-transition-group/CSSTransition'

type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right'

interface TransitionProps extends CSSTransitionProps {
  animation?: AnimationName,
  wrapper? : boolean,
}

const Transition: React.FC<TransitionProps> = (props) => {
  const {
    children,
    classNames,
    animation,
    wrapper,
    ...restProps
  } = props
  return (
    <CSSTransition
      classNames = { classNames ? classNames : animation}
      {...restProps}
    >
      {wrapper ? <div>{children}</div> : children}
    </CSSTransition>
  )
}
Transition.defaultProps = {
  unmountOnExit: true,
  appear: true,
}

export default Transition

transition 属性不会继承

# storybook 的使用

首先进行安装:npx -p @storybook/cli sb init

https://storybook.js.org/docs/configurations/typescript-config/

首先配置 storybook 的 ts 开发环境:

  1. 修改 storybook 的配置文件:

修改 .storybook 下的 main.js 文件 只需要将 stories.js 修改为 stories.tsx

module.exports = {
  stories: ['../src/**/*.stories.tsx'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ]
};

然后将 src/stories 下面的所有文件都修改为 tsx 后缀的文件就可以了。

如果需要加载全局样式:

// '../src/styles/index.scss' 直接在这里配置即可
module.exports = {
  stories: ['../src/**/*.stories.tsx', '../src/styles/index.scss'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ]
};

增加自己的 story,在自己的组件新建一个 button.stories.tsx 文件:

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'

import Button from './button'

const defaultButton = () => (
  <Button onClick={action('clicked')}>default button</Button>
)
// 第一个参数为左侧菜单父级菜单名, 第二个参数为固定参数 module
storiesOf('Button Component', module)
// 给父级菜单添加子级菜单,也就是添加组件
// 第一个参数为菜单名,第二个参数为一个函数,返回组件
  .add('默认 Button', defaultButton)

我们可以是用 Addons 为 storybook 添加插件,Addons 有大类,其中一种就是装饰器(Decorators),他返回一个函数。还有一种就是 Native Addons; storybook 添加装饰器,比如我们可以设置我们组件显示的外面的样式: 通过 addDecorator(CenterDecorator) 方法进行添加装饰器。

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'

import Button from './button'

const styles: React.CSSProperties = {
  textAlign: 'center'
}
const CenterDecorator = (storyFn: any) => (
  <div style={styles}>{storyFn()}</div>
)

const defaultButton = () => (
  <Button onClick={action('clicked')}>default button</Button>
)

const buttonWithSize = () => (
  <>
    <Button size="lg"> large button </Button>
    <Button size="sm"> small button </Button>
  </>
)

const buttonWithType = () => (
  <>
    <Button btnType="primary"> primary button </Button>
    <Button btnType="danger"> danger button </Button>
    <Button btnType="link" href="https://baidu.com"> link button </Button>
  </>
)
// 第一个参数为左侧菜单父级菜单名, 第二个参数为固定参数 module
storiesOf('Button Component', module)
  .addDecorator(CenterDecorator)
// 给父级菜单添加子级菜单,也就是添加组件
// 第一个参数为菜单名,第二个参数为一个函数,返回组件
  .add('默认 Button', defaultButton)

如果需要全局添加,可以在 .storybook 中新建 preview.js :

// './Decorator/Center'
import React from 'react'

const styles: React.CSSProperties = {
  textAlign: 'center'
}
const CenterDecorator = (storyFn: any) => (
  <div style={styles}>{storyFn()}</div>
)
export default CenterDecorator

// preview.js
import CenterDecorator from './Decorator/Center'
import { addDecorator } from '@storybook/react';
addDecorator(CenterDecorator);

更多 addons :https://storybook.js.org/addons/ https://storybook.js.org/docs/addons/introduction/#1-decorators

# storybook/addons-info 插件的使用

这个插件就是显示组件的一些信息,还可以进行复制组件代码,首先进行安装: npm i -D @storybook/addon-info 支持 ts: npm install --save @types/storybook__addon-info

https://github.com/storybookjs/storybook/tree/release/3.4/addons/info

使用:

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { withInfo } from '@storybook/addon-info'
import Button from './button'

const defaultButton = () => (
  <Button onClick={action('clicked')}>default button</Button>
)
storiesOf('Button Component', module)
  // 使用
  .addDecorator(withInfo)
  // 修改属性--直接链式操作,添加其他配置
  .addParameters({
    info: 'this is a very niece component',
    inline: true
  })
  .add('默认 Button', defaultButton)

注意他的 info 属性支持很多语法,比如 markdown 语法,以及可以进行写脚本:

  .addDecorator(withInfo)
  .addParameters({
    info: `
      this is a very niece component
      ### this is a header
      ~~~js
        const a = 'hello'
      ~~~
    `,
    inline: true
  })

也可以在 add 方法的第三个参数添加属性信息:优先级最高。

  .add('不同尺寸的 Button', buttonWithSize, {
    inline: false
  })

# react-docgen

首先需要安装 react-docgen-typescript-loader 库。

一个文档生成器,根据组件的一些属性,生成表格; 首先需要改造自己的组件,将一些属性通过 import 的方法进行引入:

// 通过 import 的方式导出所有属性
import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from 'react'
// 需要将自己的组件导出
export const Button: FC<ButtonProps> = (props) => {

}
// 注意这里需要添加一个分号
export default Button;

在 webpack.config.js 中添加:

module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.tsx?$/,
    use: [
      {
        loader: require.resolve("babel-loader"),
        options: {
          presets: [require.resolve("babel-preset-react-app")]
        }
      },
      // 添加 loader 
      {
        loader: require.resolve("react-docgen-typescript-loader"),
        options: {
          // 去掉原生属性的添加
          shouldExtractLiteralValuesFromEnum: true,
          // 这里是过滤不添加 react 中的一些属性
          propFilter: (prop) => {
            if (prop.parent) {
              return !prop.parent.fileName.includes('node_modules')
            }
            return true            
          }
        }
      }
    ]
  });

  config.resolve.extensions.push(".ts", ".tsx");

  return config;
};

我们发现我们导出的表格的属性并没有一些说明以及注释,需要使用 jsDoc 的模式来进行对 js进行注释,这样注释内容会被解析并展示出来:

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from 'react'
interface BaseButtionProps {
  /**设置 Button 的样式 */
  className?: string;
  /**设置 Button 是否被禁用 */
  disabled?: boolean;
  /**设置 Button 的尺寸 */
  size?: ButtonSize;
  /**设置 Button 的类型 */
  btnType?: ButtonType;
  children: ReactNode;
  /**设置链接 Button 的地址,仅在 btnType 为 link 有用 */
  href?: string
}
/**
 * 页面中最常用的按钮元素,适合于完成特定的交互
 * 
 * > ### 引用方法
 *   
 * ~~~js
 * import { Button } from 'jiegiserUI'
 * ~~~
 * @param props 
 */
export const Button: FC<ButtonProps> = (props) => {
}

export default Button;

注意主要将 add() 方法的第一个参数跟组件名一直才会展示出来:比如我的组件名为 Button,add 方法添加时候,必须为 'Button';

// 第一个参数为左侧菜单父级菜单名, 第二个参数为固定参数 module
storiesOf('Button Component', module)
  .addDecorator(CenterDecorator)
// 给父级菜单添加子级菜单,也就是添加组件
// 第一个参数为菜单名,第二个参数为一个函数,返回组件
  .add('Button', defaultButton)

# ts 的 Omit 来忽略属性

Omit 是用来移除或者忽略接口中的一个属性:

比如我们自定义的组件有一个 size 属性,但是我们还需要给用户去自定义更多的 html 原生的更多属性,所以需要继承 InputHTMLAttributes, 但是该属性有一个 size 属性,所以这个时候我们需要忽略这个属性,或者修改我们自身定义的属性名:

// Omit 第一个参数是 interface ,第二个是需要忽略的属性名
export interface InputProps extends Omit<InputHTMLAttributes<HTMLElement>, 'size'> {
  size?: InputSize
}

# 指定运行测试用例

可以使用命令 npm test -- -t "auto" 来只运行指定的测试用例,注意如果测试用例为 autoComplete.test.tsx,就写成 npm test -- -t "auto" 是可以匹配到的。

# Fetch

缺点:

  • 只对网路请求报错,对 400,500 都当做成功的请求
  • 默认不会带 cookie
  • 不支持 abort,不支持超时控制
  • 没有办法原生检测请求的进度

# mock server

  • https://jsonplaceholder.typicode.com/
  • https://designer.mocky.io/design

# useState 更新数组对象

我们一般使用 useState 更新数据时,直接使用它返回的第二个方法参数进行修改;因为这个方法更新数据是一个异步的过程; 我们如果想要实时获取更新后的数据,可以在第二个方法中,传入一个函数进行修改: 注意更新的时候是返回一个新的对象,而不是对原有的进行修改。

const [ fileList, setFileList] = useState<UploadFile[]>([])
// setFileList
updateFileList(_file, {
  percent: percentage,
  status: 'uploading'
})
// 如果直接使用 setFileList 更新数据,获取到的是最后一次更新的结果
// _file.percent = percentage
// _file.status = 'uploading'
// setFileList([_file])

// {...file, ...updateobj} 返回替换属性后的新的对象
// 第一个参数为需要更新的文件,第二个参数是需要更新的属性, partial 的意思是更新的属性为 UploadFile 中的可选的一些属性,并不是所有的都更新
const updateFileList = (updateFile: UploadFile, updateobj: Partial<UploadFile>) => {
  setFileList(prevList => {
    return prevList.map(file => {
      if (file.uid === updateFile.uid) {
        return {...file, ...updateobj}
      } else {
        return file
      }
    })
  })
}

# jest mock 一个库

比如我们 mock 一下 axios 库:

import axios from 'axios'
// 将 axios 转换为测试的mocke对象
const mockedAxios = axios as jest.Mocked<typeof axios>

describe('test upload component', () => {
  it('upload process should works file', async () => {
    // 第一种方法
    // mockedAxios.post.mockImplementation(() => {
    //   return Promise.resolve({'data': 'cool'})
    // })
    // 第二种方法
    mockedAxios.post.mockResolvedValue({'data': 'cool'})
  })
})

# jest 测试 是否包含某些特定的属性

我们在写测试的时候,不一定需要测试一个属性中所有的属性是否存在,只需要测试某些特定的属性是否存在:可以使用 expect.objectContaining 以及 toHaveBeenCalledWith 结合使用:

    expect(testProps.onRemove).toHaveBeenCalledWith(expect.objectContaining({
      raw: testFile,
      status: 'success',
      name: 'test.png'
    }))

# js 模块化

以前的 js 就是通过引入不同的文件,到后来通过自执行函数,来定义全局变量,到现在的 commjs、amd、es6 module:

// common.js
const bar = require('./bar')
// 模块产出
module.exports = function () {
  // ...
}

// AMD -- 使用 require.js 才能在浏览器运行
define(function (require) {
  // 通过相对路径获得依赖模块
  const bar = require('./bar')
  // 模块产出
  return function () {
    // ...
  }
})

// es6 module
// 通过相对路径获得依赖模块
import bar = from './bar'
// 模块产出
export default function() {
  // ...
}

# 创建组件库模块入口文件

webpack 支持 package.json 中的 module 字段,这样就可以启用 tree shake 机制。我们的项目默认入口文件是 index.tsx 。修改:

import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
library.add(fas)

export { default as Button } from './components/Button'
export { default as Menu } from './components/Menu'
export { default as AutoComplete } from './components/AutoComplete'
export { default as Icon } from './components/Icon'
export { default as Input } from './components/Input'
export { default as Progress } from './components/Progress'
export { default as Transition } from './components/Transition'
export { default as Upload } from './components/Upload'

这里有一些问题,比如,我们的 menu 组件我们新建了一个 index.tsx 文件用于导出组件: 因为这个组件有两个子组件,也是在 Menu 组件里面使用,所以将 Item 以及 SubItem 组件导出为 Menu 组件的属性,类似 Menu.item 这样使用

import { FC } from 'react'
import Menu, { MenuProps } from './menu'
import SubMenu, { SubMenuProps } from './subMenu' 
import MenuItem, { MenuItemProps } from './menuItem'

export type IMenuComponent = FC<MenuProps> & {
  Item: FC<MenuItemProps>,
  SubItem: FC<SubMenuProps>
}

const TransMenu = Menu as IMenuComponent

TransMenu.Item = MenuItem
TransMenu.SubItem = SubMenu

export default TransMenu

在根路径下面新建一个 tsconfig.build.json 文件,用于配置打包环境的 ts 配置:

{
  "compilerOptions": {
    "outDir": "build", // 编译好的文件输出路径
    "module": "esnext", // 输出的 mudule 类型
    "target": "es5", // 确定编译后的 es 版本
    "declaration": true, // 对每一个 ts 文件生成 .d.ts 类型注释文件
    "jsx": "react", // 指定 jsx 的类型
    "moduleResolution": "Node", // 配置模块的加载方式
    "allowSyntheticDefaultImports": true // 支持 default import 方式
  },
  // 需要编译的文件
  "include": [
    "src"
  ],
  // 不编译的文件
  "exclude": [
    "src/**/*.test.tsx",
    "src/**/*.stories.tsx"
  ]
}

然后在 package.json 中添加编译 ts ,可以指定 ts config 文件:

  "scripts": {
    "build-ts": "tsc -p tsconfig.build.json",
  },

处理我们的 scss 文件:在 package.json 中添加编译 scss :

  "scripts": {
    "build": "npm run build-ts && npm run build-css",
    "build-ts": "tsc -p tsconfig.build.json",
    "build-css": "node-sass ./src/styles/index.scss ./build/index.css",
  },

我们发现每次进行进行打包的时候,都需要手动删除一次 build 文件夹,我们可以使用功能 rimraf 库,每次在打包之前进行删除这个文件夹,首先进行安装:npm install --save-dev rimraf@3.0.1;然后在 package.json 中添加删除 build 文件夹的命令:

  "scripts": {
    "clean": "rimraf ./build",
    "build": "npm run clean && npm run build-ts && npm run build-css",
  },

比如我们本地的有一个项目 Test 依赖的我们发布的组件库 testUI,如果我们本地的 testUI 修改了一个 ui,然后我们需要在我们我们的 Test 项目进行测试修改的结果;第一种方式是再次发布一个版本的 testUI 组件库,然后我们的 Test 项目重新安装依赖,这样一系列的操作会很麻烦,而且要迭代版本;我们可以使用 npm link 的方式,将我们 Test 项目中依赖的 testUI 组件库进行软连接。

首先我们在本地的 testUI 文件夹的地址,输入:

这样会创建一个软链接。创建软链接到全局。使用全局的 npm 进行创建。

npm link

然后在我们的项目文件夹中输入命令: 将上一步的软链接,链接到我们本地的项目;

npm link testUI

然后我们的项目会根据 testUI 组件库的 package.json 文件中的入口文件配置:main 字段的配置;获取组件库中的内容,当然需要在我们的 Test 项目的 package.json 中的依赖包添加 testUI 依赖。这样,就可以在 Test 项目中使用本地的 testUI 组件库了。

我们在使用自己发布的组件库这样在本地测试的时候,有可能还会遇到两个 react 的依赖。比如我们自己的 Test 项目会装有 react 的依赖,我们的 testUI 中也会依赖 react,这样在本地测试我们的 testUI 的时候会报错。我们可以在 testUI 组件库中再次执行 npm link ../Test/node_modules/react,使得我们的 testUI 依赖的 react 为我们项目 Test 的 react,这样就不会产生两个 react 的依赖。

# 发布 npm 包

发布包首先需要登录,登录有两种方式,一种是我们直接在 npm 官网去登录,另一种是使用命令行登录:首先输入命令 npm whoami 来检测是否登录;这里主要注意的是要查看一下你本地配置的 npm 的源有没有被修改,npm config ls 如果修改成了淘宝的镜像,那登录不了,可以修改回来。然后使用 npm adduser 进行登录或者注册;

修改 package.json 文件: 主要修改 private 为 false(不是一个私有的包);增加入口文件:main、module、types 选项; 对于版本号,具体可以查看这个语义化版本的规范:https://semver.org/lang/zh-CN/

{
  "name": "jiegiserui",
  "version": "0.1.0",
  "description": "React components library",
  "author": "jiegiser",
  "private": false,
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "license": "MIT",
  "keywords": [
    "Component",
    "UI",
    "React"
  ],
  "hoempage": "https://github.com/jiegiser/jiegiserui",
  "repository": {
    "type": "git",
    "url": "https://github.com/jiegiser/jiegiserui"
  },
  // 代表哪些文件上传到 npm 中,如果不写,会默认使用 .gitignore 文件里面的内容;
  "files": [
    "dist"
  ],
  "scripts": {
    "clean": "rimraf ./dist",
    "build": "npm run clean && npm run build-ts && npm run build-css",
    "build-ts": "tsc -p tsconfig.build.json",
    "build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
    // 添加生命周期函数,在发布之前运行 build
    "prepublish": "npm run build"
  },
}

精简项目依赖:

  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.28",
    "@fortawesome/free-solid-svg-icons": "^5.13.0",
    "@fortawesome/react-fontawesome": "^0.1.9",
    "axios": "^0.19.2",
    "classnames": "^2.2.6",
    "react-transition-group": "^4.3.0"
  },
  // 项目必须依赖的库,写在这里就为了避免用户安装了组件库之后,存在两个 react 版本 react hooks 会报错,
  "peerDependencies": {
    "react": ">= 16.8.0",
    "react-dom": ">= 16.8.0"
  },
  "devDependencies": {
    "@storybook/addon-actions": "^5.2.8",
    "@storybook/addon-info": "^5.2.8",
    "@storybook/addon-links": "^5.2.8",
    "@storybook/addons": "^5.2.8",
    "@storybook/react": "^5.2.8",
    "@types/react-transition-group": "^4.2.3",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "@types/classnames": "^2.2.10",
    "@types/jest": "^24.9.1",
    "@types/node": "^12.12.42",
    "@types/react": "^16.9.35",
    "@types/react-dom": "^16.9.8",
    "@types/storybook__addon-info": "^5.2.1",
    "react-docgen-typescript-loader": "^3.7.2",
    "rimraf": "^3.0.1",
    "node-sass": "^4.14.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1",
    "typescript": "^3.7.5"
  }

在项目打包发布时候添加 eslit 检查以及 test 的测试,防止将有 bug 的代码发布到 npm 上,首先添加一个 eslint 检查的命令:

  "scripts": {
    "lint": "eslint --ext js,ts,tsx src --max-warnings 5",
  }

添加测试的时候,我们不能使用自带的 test 命令,因为那个命令不能很好的反馈测试结果,他是一个可交互的命令行,可以设置 CI 为 true 让所有测试返回结果: https://create-react-app.dev/docs/running-tests/#continuous-integration 但是,需要在不同的系统设置。我们可以使用 cross-env (跨平台设置环境变量) 库来解决这个问题;首先进行安装 npm install cross-env --save-dev;然后添加命令:

  "scripts": {
    "test:onwatch": "cross-env CI=true react-scripts test",
    // 添加生命周期函数,在发布之前运行 eslint 检查以及 test 然后运行 build
    "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build"
  }

使用 husky 库来进行 pre-commit 阶段做代码检查,它会在我们项目根目录下面的 .git/hooks 文件夹下面创建 pre-commit、pre-push 等 hooks。这些 hooks 可以让我们直接在 package.json 的 script 里运行我们想要在某个 hook 阶段执行的命令。 安装: npm install husky --save-dev,同样设置在代码提交之前检查 test 以及 eslint 是否通过,在 package.json 中添加配置:

  "husky": {
    "hooks": {
      "pre-commit": "npm run test:nowatch && npm run lint"
    }
  }

# CI - 持续集成、CD - 持续交付、持续部署

# CI - 持续集成

  • 频繁的将代码集成到主干(master)
  • 快速发现错误
  • 防止分支大幅偏离主干

# CD - 持续交付、持续部署

  • 频繁的将软件的新版本,交付给质量团队或者用户
  • 代码通过评审以后,自动部署到生产环境

# 使用 travis 自动运行测试

https://docs.travis-ci.com/user/languages/javascript-with-nodejs/

新建 .travis.yml 文件,添加下面的配置: 添加 cache 选项是因为,每次进行集成 npm install or npm ci ,设置缓存,下次就不需要重新安装所有的依赖。

注意在添加 .travis.yml 文件的下一次 commit 才会进行持续部署。

language: node_js
node_js:
  - "stable"
cache:
  directories:
  - node_modules
env:
  - CI=true

然后修改 package.json 中之前添加的 prepublish 修改为 prepublishOnly,不然每次进行持续集成都会进行执行这个命令。

# 使用 travis 自动发布文档页面

在新增 .travis.yml 文件,下面配置:

注意还需要设置 github token,在 github 中生成一个 token,然后填到 travis 中设置环境变量。

script:
  - npm run build-storybook
deploy:
  provider: pages
  skip_cleanup: true
  github_token: $github_token
  local_dir: storybook-static // 需要上传的文件夹
  on:
    branch: master

首先需要在 github 上生成 token,然后在 travis 的网站添加环境变量,我们这里的环境变量为 github_token;

avatar

然后 push 代码,就会将文档上传。

评 论:

更新: 11/21/2020, 7:00:56 PM