资讯专栏INFORMATION COLUMN

React 造轮子系列:Icon 组件思路

instein / 3017人阅读

简介

本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。

这里我也是通过别人学的,主要做些总结及说明造各个轮子的一种思路,方便今后使用别人的的轮子时自己脑中有造轮子的思想,能通过修改源码及时修改 bug,按时上线。

本文的 Icon 组件主要是参考 Framework7 中的 Icon React Component 写的。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

为什么要造轮子

1.为了不求人

假设你使用某个UI框架发现有一个 bug,于是你反馈给开发者,开发者说两周后修复,而你的项目一周后就要上线,你怎么办?

为什么很多大公司都不使用其他公司的轮子,要自己造?为了把控自己的业务,不被别人牵着走。

2.为了不流于平庸

大家都是写增删改查,你跟别人比有什么优势?你如果能说一句【我公司的人都在用我写的UI框架】是不是就很牛逼?造 UI 轮子会遇到很多技术层面而非业务层面的知识?比如一些算法。

3.为了创造

你为别人做了这么久的事情,有没有自己做什么?自驱动力。

4.为什么是 UI 轮子,不是其他方面的轮子

比如,为什么不自己写一个 React 框架,要写 React UI 框架呢?

React.FunctionComponent 与 IconPropps

本轮子使用 React + TypeScript 来写的,那么在 ts 中如何声明函数组件及级 Icon 组件传递参数呢,答案是使用React提供的静态方法 React.FunctionComponent 及 TypeScript 提供的接口定义。

// lib/icon.tsx

import React from "react"

interface IconProps {
  name: string
}

const Icon: React.FunctionComponent = () => {
  return (
    icon
  )
}

export default Icon

在 index.txt 中调用:

import React from "react";
import ReactDOM from "react-dom";
import Icon from "./icon"
  
ReactDOM.render(
, document.body)

对于上面的定义方式,后面的轮子会经常使用,所以不必担心看不懂。

使用 svg-sprite-loader 加载 SVG

在上面我们指定了 Iconnamewechat,那怎么让它显示微信的图标呢,首先在阿里的 Iconfont 下载对应的 SVG

接着如何显示 svg? 这里我们使用一个 svg-sprite-loader 库,然后在对应的 webpack下的 rules 中添加:

{
  test: /.svg$/,
  loader: "svg-sprite-loader"
}

在 Icon 中引用,当然对应 tsconfig.json 也要配置(这不是本文的重点):

import React from "react"
import wechat from "./icons/wechat.svg"

console.log(wechat)
interface IconProps {
  name: string
}

const Icon: React.FunctionComponent = () => {
  return (
    
      
        
      
    
  )
}

export default Icon

运行效果:

当然 svg 里面不能直接写死,我们需要根据外部传入的 name 来指定对应的图像:

// 部分代码
import  "./icons/wechat.svg"
import "./icons/alipay.svg"

const Icon: React.FunctionComponent = (props) => {
  return (
    
      
        
      
    
  )
}

外部调用:

ReactDOM.render(
, document.getElementById("root"))

运行效果:

importAll

大家有没有注意到,我需要使用哪个 svg, 需要在对应的 icon 组件导入对应的 svg,这样要是我需要100个 svg ,我就要导入100次,这样做太傻,文件也会变得冗长。

因此我们需要一个动态导入全部 SVG 的方法:

 // lib/importIcons.js
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
  importAll(require.context("./icons/", true, /.svg$/))
} catch (error) {
  console.log(error)
}

要想看懂上诉的代码,可能需要一点 node.js 的基础,这边建议你直接收藏好啦,下次有用到,直接拷贝过来用就行了。

接着在 Icon 组件里面导入就行了: import "./importIcons"

React.MouseEventHandler 的使用

当我们需要给 Icon 注册事件的时候,如果直接在组件上写 onClick 事件是会报错的,因为它没有声明接收 onClick 事件类型,所以需要声明,如下所示:

/lib/icon.tsx

import React from "react"
import "./importIcons"
import "./icon.scss";
interface IconProps {
  name: string,
  onClick: React.MouseEventHandler
}

const Icon: React.FunctionComponent = (props) => {
  return (
    
      
        
      
    
  )
}

export default Icon

调用方式如下:

import React from "react";
import ReactDOM from "react-dom";
import Icon from "./icon"

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(
, document.getElementById("root"))
让Icon响应所有事件

上述我们只监听了 onClick 事件 ,但对于其它事件是不支持了,所以我们需要进一步完善。这里我们不能一个一个添加对应的事件类型,需要一个统一的事件类型,那这个是什么呢?

通过 react 我们会找到一个 SVGAttributes 类,这里我们需要继承它:

/lib/icon.tsx
import React from "react"
import "./importIcons"
import "./icon.scss";
interface IconProps extends React.SVGAttributes {
  name: string;
}

const Icon: React.FunctionComponent = (props) => {
  return (
    
      
        
      
    
  )
}

export default Icon

调用方式:

import React from "react";
import ReactDOM from "react-dom";
import Icon from "./icon"

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(
console.log("enter")} onMouseLeave = { () => console.log("leave")} />
, document.getElementById("root"))

上述还是会有问题,我们还有 onFocus, onBlur, onChange 等等事件,也不可能一个一个传递进来,那还有什么方法呢。

icon.tsx 中我们会发现我们用的都是通过 props 传递进来的。聪明的朋友的可能立马想到了使用展开运算符的形式 {...props},改写如下:

...
const Icon: React.FunctionComponent = (props) => {
  return (
    
      
        
      
    
  )
}
...

上述还是会有问题,如果使用的人也传入 className 呢,用过 Vue 就知道 Vue 是真的好,它会把传入和里面的合并起来,但 React 就不一样了,传入的会覆盖里面的,所以需要自己手动处理:

...
const Icon: React.FunctionComponent = (props) => {
  const { className, ...restProps} = props
  return (
    
      
        
      
    
  )
}
...

上达写法还存在问题的,如果外面没有写 className ,那么内部会多出一个 undefined

聪明你的可能就想到了使用三目运算符来做判断,如:

className={`fui-icon ${className ? className : ""}`}

但这种情况如果有多个参数要怎么办呢?

所以有人就非常聪明专门写了一个库存 classnames,这个库有多火呢,每周有300多万的下载量,它的作用就是处理 className 的情况。

当然我们这边只做简单的处理,如下所示

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.join(" ")
}

export default classes

使用方式:

...
const Icon: React.FunctionComponent = (props) => {
  const { className, name,...restProps} = props
  return (
    
      
        
      
    
  )
}
...
    
    

这样最终渲染出来的 className还是会多出一个空格,作为完美者,并不希望有空格的出现的,所以需要进一步处理空格,这里使用 es6 中数组的 filters 方法。

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.filter(Boolean).join(" ")
}

export default classes

单元测试

首先我们对我们的 classes 方法时行单元测试,这里使用 Jest 时行测试,也是 React 官网推荐的。

classes 测试用例如下:

import classes from "../classes"
describe("classes", () => {
  it("接受 1 个 className", () => {
    const result = classes("a")
    expect(result).toEqual("a")
  })
  it("接受 2 个 className", ()=>{
    const result = classes("a", "b")
    expect(result).toEqual("a b")
  })
  it("接受 undefined 结果不会出现 undefined", ()=>{
    const result = classes("a", undefined)
    expect(result).toEqual("a")
  })
  it("接受各种奇怪值", ()=>{
    const result = classes(
      "a", undefined, "中文", false, null
    )
    expect(result).toEqual("a 中文")
  })
  it("接受 0 个参数", ()=>{
    const result = classes()
    expect(result).toEqual("")
  })
})

使用Snapshot测试UI

这里测试 UI 相关还需要使用一个库 Enzyme , Enzyme 来自 airbnb 公司,是一个用于 React 的 JavaScript 测试工具,方便你判断、操纵和历遍 React Components 输出。Enzyme 的 API 通过模仿 jQuery 的 API ,使得 DOM 操作和历遍很灵活、直观。Enzyme 兼容所有的主要测试运行器和判断库。

icon 的测试用例

import * as renderer from "react-test-renderer"
import React from "react"
import Icon from "../icon"
import {mount} from "enzyme"

describe("icon", () => {
  it("render successfully", () => {
    const json = renderer.create().toJSON()
    expect(json).toMatchSnapshot()
  })
  it("onClick", () => {
    const fn = jest.fn()
    const component = mount()
    component.find("svg").simulate("click")
    expect(fn).toBeCalled()
  })
})

IDE 提示找不到 describe 和 it 怎么办?

解决办法:

yarn add -D @types/jest

在文件开头加一句 import "jest"

这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。

如果还不行,你需要在 WebStorm 里设置对 jest 的引用:

这是因为 typescript 默认排除了 node_modules 里的类型声明。

总结

以上主要是在学习造轮子过程总结的,环境搭建就没有细说了,主要记录实现 Icon 轮子的一些思路及注意事项等,想看源码,跑跑看的,可以点击这里查看。

参考

方应杭老师的React造轮子课程

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109733.html

相关文章

  • React 轮子系列Icon 组件思路

    简介 本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。 这里我也是通过别人学的,主要做些总结及说明造各个轮子的一种思路,方便今后使用别人的的轮子时自己脑中有造轮子的思想,能通过修改源码及时修改 bug,按时上线。 本文的 Icon 组件主要是参考 Framework7 中的 Icon React ...

    stormzhang 评论0 收藏0
  • React系列:Layout 组件思路

    摘要:本文是造轮系列第三篇。造轮子系列组件思路造轮系列对话框组件思路想阅读更多优质文章请猛戳博客一年百来篇优质文章等着你初始化参考组件分别分为五个组件。参考方应杭老师的造轮子课程交流干货系列文章汇总如下,觉得不错点个,欢迎加群互相学习。 本文是React造轮系列第三篇。 1.React 造轮子系列:Icon 组件思路 2.React造轮系列:对话框组件 - Dialog 思路 想阅读更多优质...

    neroneroffy 评论0 收藏0
  • React系列:对话框组件 - Dialog 思路

    摘要:本文是造轮系列第二篇。实现方式事件处理跟差不多,唯一多了一步就是当点击或者的时候,如果外部有回调就需要调用对应的回调函数。 本文是React造轮系列第二篇。 1.React 造轮子系列:Icon 组件思路 本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。 想阅读更多优质文章请猛戳Git...

    qianfeng 评论0 收藏0
  • 精读《怎么用 React Hooks 轮子

    摘要:可以看到,这样不仅没有占用组件自己的,也不需要手写回调函数进行处理,这些处理都压缩成了一行。效果通过拿到周期才执行的回调函数。实现等价于的回调仅执行一次时,因此直接把回调函数抛出来即可。 1 引言 上周的 精读《React Hooks》 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(就是数组),但理解实现原理就可以用好了吗?学的是...

    Shihira 评论0 收藏0
  • 手摸手,带你封装一个vue component

    摘要:灵活性和针对性。所以我觉得大部分组件还是自己封装来的更为方便和灵活一些。动手开干接下来我们一起手摸手教改造包装一个插件,只要几分钟就可以封装一个专属于你的。 项目地址:vue-countTo配套完整后台demo地址:vue-element-admin系类文章一:手摸手,带你用vue撸后台 系列一(基础篇)系类文章二:手摸手,带你用vue撸后台 系列二(登录权限篇)系类文章三:手摸手,带...

    pkhope 评论0 收藏0

发表评论

0条评论

instein

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<