Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node

  • VSCode插件

  • Webpack

  • Redux

  • React源码

  • 组件库

    • Dialog组件封装
      • 0.前言
      • 1. 实现方案
        • 1.1 确定大致的 api
      • 2.如何设计 Dialog 的 HTML 部分?
        • 细节1:遮罩层与组件的 HTML 的层级关系
        • 细节2:使用穿梭门Portal 解决 zIndex 的问题?
        • 细节3:传入buttons 时无需显式绑定key
      • 3. 如何编写Dialog的 CSS 样式?
        • 1. 使用 scss 作为预处理器
        • 2. 处理同类元素
        • 3. 如何让 svg 着色?
      • 4. 扩展组件【操控 JSX.ELement】
        • 1. 如何实现 React.Element 元素的渲染?
        • 2. 如何实现 React.Element 元素的卸载?
        • 3. 完整 API 总览
    • 组件导入思考
    • Keepalive 伪代码
    • React 组件:Loading 弹窗
  • React高阶系列

  • UMI插件

  • 前端工程化

  • 单元测试vitest

  • 重点技术
  • 组件库
wangjiasheng
2022-10-07
目录

Dialog组件封装

# 0.前言

本篇博客将从零开始封装一个 Dialog 组件,其中涉及实现细节较多。主要包含以下内容:

  1. 待完成后总结
  2. 待完成后总结

组件演示地址:https://wangjs-jacky.github.io/jacky-workspace-html/ (opens new window)

项目仓库地址:https://github.com/wangjs-jacky/jacky-workspace (opens new window)

# 1. 实现方案

# 1.1 确定大致的 api

首先,我们大致确定下 Dialog 组件的基本使用方式:

const Demo: React.FC = () => {
  const [isShow, { toggle, setFalse }] = useBoolean(false);
  return (
    <div style={{ zIndex: 9 }}>
      <button onClick={() => toggle()}>切换</button>
      <Dialog
        visible={isShow}
        buttons={[
          <button onClick={setFalse}>确定</button>,
          <button onClick={setFalse}>取消</button>,
        ]}
        onClose={setFalse}
      >
        <strong>hi</strong>
      </Dialog>
    </div>
  );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Dialog 对话框【基础版】实现参数

  • visible 用于控制组件是否显示
  • buttons 用于底部footer 栏的显示与隐藏。
  • onClose 为关闭弹窗的回调。

# 2.如何设计 Dialog 的 HTML 部分?

重新回顾下 Dialog 需求:

  1. 通过 visible 完成对 JSX Element 的更新,即大致结构如下:

    return visible ? JSXElEment : null
    
    1

    说明:当用户点击关闭按钮时,可能存在两种使用场景。一、将组件直接销毁。二、是将组件隐藏。这里我们只考虑第一种,第二种方案,后期会再开一篇博客从零搭建 KeepAlive 组件。

  2. 通过 buttons 传入 React.ReactNode[] 结构。

在 React 中的 HTML 设计如下:

const Dialog: React.FC<PropsType> = ({ visible, buttons, children, onClose }) => {
  const result = visible ? (
    <>
      <div className="sui-dialog-mask" onClick={onClickMask}></div>
      <div className="sui-dialog">
        <div className="sui-dialog-close" onClick={onClickClose}>
          <Icon name="close" />
        </div>
        <div className="sui-dialog-header"> 提示 </div>
        <div className="sui-dialog-main">{children}</div>
        <div className="sui-dialog-footer">
          {buttons &&
            buttons.map((button, index) => {
              /* 在 Element 中绑定 key  */
              return React.cloneElement(button, { key: index });
            })}
        </div>
      </div>
    </>
  ) : null;
  return ReactDOM.createPortal(result, document.body);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 细节1:遮罩层与组件的 HTML 的层级关系

上述 HTML 整体结构分为两层:遮罩层(sui-dialog-mask) 及 组件自身(sui-dialog)。

此处 HTML 两者的关系并非采用常见的嵌套结构,而是并排结构,如下:

  • 嵌套结构:

    <div className="sui-dialog-mask">
      <div className="sui-dialog"></div>
    </div>
    
    1
    2
    3
  • 并排结构(推荐)

    <div className="sui-dialog-mask">
    </div>
    <div className="sui-dialog">
    </div>
    
    1
    2
    3
    4

    选择此方案的目的是为了实现:点击 Dialog 的遮罩层关闭弹窗功能。若为嵌套关系的话,需要考虑事件的冒泡。

# 细节2:使用穿梭门Portal 解决 zIndex 的问题?

在组件设计时,需要考虑到用户对组件 zIndex 的设定。在 HTML 的图层显示中存在一个特性,即一个组件的图层 level 永远取决于父组件的 level,见下:

<div style={{zIndex:10}}></div>
<div style={{zIndex:5}}>
  <div style={{zIndex:9999}}"></div>
</div>
1
2
3
4

在上述案例中,第二个div 的 children 的图层永远受限于其父组件的图层。因为我们无法限定用户使用组件的方式,故采取以下两个约定:

  1. 其一、在提供组件时,应在手册中明确组件库各组件的 zIndex 层级范围,如:

    Component zIndex
    Cascader 80
    DatePicker 80
    Menu 80
    Popover 90
    Affix 100
    LightUp 100
    Message 100
    Modal 100
  2. 将 Dialog 组件通过 Portal 穿梭到根节点。

    const result:JSX.ELEMent = xxxx;
    ReactDOM.createPortal(result, document.body);
    
    1
    2

    特别注意: Portal 只会影响 HTML DOM 结构,而不会影响 React 组件树。

# 细节3:传入buttons 时无需显式绑定key

Dialog 组件支持接受 buttons 数组,实现对底部按钮的自定义渲染。

对于自定义组件的渲染实现思路有二:

  1. 定义 obj对象,将 obj 转化为 ReactElement

    // 用户编写
    let obj = [
      {key:"1",content:"按钮1",onClick:()=>{}},
      {key:"2",content:"按钮2",onClick:()=>{}}
    ]
    
    // Dialog 组件内部处理
    const obj2Ele = (obj)=>{
      return obj.map(i => <button key={i.key} onClick={i.onClick}>i.content</button>)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. (本案例使用)

    // 用户编写
    let buttons = [
      <button onClick={()=>{}}>按钮1</button>,
      <button onClick={()=>{}}>按钮2</button>
    ]
    
    // Dialog 组件内部处理
    // 如果需要添加额外的属性,如 key, 可通过 React.cloneElement 实现。
    buttons.map((ele,index) => React.cloneElement(ele,{key:index}))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

两个方案皆可,取决于用户使用时的自由裁量度,第一种方案只能用于生成 button 按钮,而第二种用户可自定义组件。

# 3. 如何编写Dialog的 CSS 样式?

这里直接给出本案例的 CSS 实践方式

# 1. 使用 scss 作为预处理器

  1. 定义全局变量 _variable.scss

    注:这里有下划线代表私有文件。

    $main-color: #1890ff;
    
    1
  2. 通过 @import 的方式导入

    @import "../variable"
      
    .sui-dailog{
      &-mask{
        /* &-mask 即对 .sui-dialog-mask 的类名设定*/
      }
    }
    
    1
    2
    3
    4
    5
    6
    7

# 2. 处理同类元素

当存在多个同类元素时,需要单独处理第一个元素以及最后一个元素,如下:

button {
  /* 处理第一个元素 */
  &:first-child{
    margin-left: 0px;
  }
  /* 处理最后一个元素 */
  &:last-child{
    margin-right: 0px;
  }
}
1
2
3
4
5
6
7
8
9
10

# 3. 如何让 svg 着色?

对于 svg 图标,可以设置 fill 这个属性,如 icon 作为 svg 图标

.sheng-icon{
  fill: currentColor;
}
1
2
3

使用时,直接设置 color 即可:

&-close{
  color: white;
}
1
2
3

# 4. 扩展组件【操控 JSX.ELement】

在前节中已将 Dialog 的基础功能完成,本节基于此组件进一步封装出两个实用 api:

  • alert

    用于取代 windows 的 alert 函数。

  • confirm

    在 alert 的基础上扩展出 confirm 弹窗。

预期效果:函数触发后,将 Dialog 渲染在页面上。

设计思路:

目前 Dialog 组件已提供基础的 HTML 布局和 CSS 样式,也即 React.ReactElement 或 JSX.Element 元素可以获取。如果能控制 Element 元素的渲染和卸载即可完成此功能的实现。

JSX.Element 及React.ReactElement 结构如下:

/*JSX.Element 或 React.ReactElement*/
const component = (
  <Dialog
    visible={true}
    /* 注:可在此处插入 生命周期 (before 或者 after)*/
    onClose={() => {
      close();
      afterClose && afterClose;
    }}
    buttons={bottons}
  >
    {content}
  </Dialog>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 1. 如何实现 React.Element 元素的渲染?

在 React 中 Element 元素渲染方案很多,并且 Class Component 还是 Function Component 获取 Element 元素的方式还是不一样的,打算新开一片博客梳理此部分内容。

在本案例中,由于触发的为 Dialog 组件,且不被嵌套在Fiber tree结构中,因此使用 React.render() 方案渲染最为合适。

React.render 需要接受两个参数,一、是虚拟 JSX.Element。二、是真实 DOM 节点。

/* 1. 创建 div 元素 */
const divEle = document.createElement('div');
/* 2. 挂载到真实的 body 下面 */
document.body.append(divEle);
/* 3. 通过 render 函数将虚拟DOM(component) 渲染到真实的 DOM(divELe) 上 */
ReactDOM.render(component, divEle);/*JSX.Element 或 React.ReactElement*/
1
2
3
4
5
6

# 2. 如何实现 React.Element 元素的卸载?

如果是常规情况的组件卸载,写法是非常简单的,如下:

const App = ( ) => {
  const [visible,] = useState(false)
  const Component/*Class Component*/;
  return visible && <Component />
}
1
2
3
4
5

而通过之前的分析可知,由 alert 或者 confirm 触发的弹窗组件,最终是渲染在 body 节点下的 div 标签下,而非在原有的 Fiber 树结构下。

因此,采用 unmountComponentAtNode 函数对组件进行卸载:

注:这个 api 不是很常用,因为一般情况下,不会遇到脱离传统 DOM 渲染结构的情况,只需通过在父节点中设置一个变量的方式即可控制子组件的挂载与卸载。

unmountComponentAtNode可以对从 DOM 中移除已经挂载的 React 组件,清除相应的事件处理器和 state,在之后打算实现的 useDataModal 这个自定义 Hooks 可能还会用到这个api。

const close = () => {
    ReactDOM.render(React.cloneElement(component, { visible: false }), divEle);
    ReactDOM.unmountComponentAtNode(divEle);
    divEle.remove();
};
1
2
3
4
5

卸载分为三步:

  • 第一步:给 Dialog 组件自身传递 visible 为 false。

    说明:此处需要了解 React 组件渲染逻辑,就是将 component 组件再次渲染一遍,并通过React.cloneElement 修改 component 的 props。

  • 第二步:ReactDOM.unmountComponentAtNode(divEle) 官方专用于卸载组件,主要是将多余的div 给删除掉。

  • 第三步:.remove 是用于卸载组件。

    也是用于 div 元素的卸载,但是无法卸载事件处理函数,还是推荐使用 unmountCompoentAtNode 函数,这里只是为了起一个双保险的策略。

# 3. 完整 API 总览

  1. 封装抽象函数

    const modal = (content: ReactNode, bottons?: React.ReactElement[], afterClose?: () => void) => {
      const close = () => {
        ReactDOM.render(React.cloneElement(component, { visible: false }), divEle);
        ReactDOM.unmountComponentAtNode(divEle);
        divEle.remove();
      };
    
      const component = (
        <Dialog
          visible={true}
          /* 注:可在此处插入 生命周期 (before 或者 after)*/
          onClose={() => {
            close();
            afterClose && afterClose;
          }}
          buttons={bottons}
        >
          {content}
        </Dialog>
      );
    
      /* 1. 创建 div 元素 */
      const divEle = document.createElement('div');
      /* 2. 挂载到真实的 body 下面 */
      document.body.append(divEle);
      /* 3. 通过 render 函数将虚拟DOM(component) 渲染到真实的 DOM(divELe) 上 */
      ReactDOM.render(component, divEle);
    
      return { close };
    };
    
    /* alert 需要补充定义关闭 */
    const alert = (content: React.ReactNode) => {
      const { close } = modal(content, [<button onClick={() => close()}> OK</button>]);
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    modal 函数是 alert函数和 confirm 函数封装结束后,发现存在大量重复代码,将通用部分抽取为抽象函数,在此函数的基础上,confirm 及 alert 的实现就非常简单了。

  2. 封装 alert 函数

    const alert = (content: React.ReactNode) => {
      const { close } = modal(content, [<button onClick={() => close()}> OK</button>]);
    };
    
    1
    2
    3
  3. 封装 confirm 函数

    const confirm = (content: string, yes?: () => void, no?: () => void) => {
      const onYes = () => {
        /* todo */
        close();
        yes && yes();
      };
    
      const onNo = () => {
        close();
        no && no();
      };
      
      const buttons = [<button onClick={onYes}>yes</button>, <button onClick={onNo}>no</button>];
      const { close } = modal(content, buttons, no);
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 导出部分

    /* 扩展函数:alert,confirm 函数*/
    export { alert, confirm, modal };
    /* 核心组件*/
    export default Dialog;
    
    1
    2
    3
    4
编辑 (opens new window)
上次更新: 2022/10/09, 0:10:00
浅入 react 合成事件
组件导入思考

← 浅入 react 合成事件 组件导入思考→

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式