摘要:本文是造輪系列第二篇。實現方式事件處理跟差不多,唯一多了一步就是當點擊或者的時候,如果外部有回調就需要調用對應的回調函數。
本文是React造輪系列第二篇。
1.React 造輪子系列:Icon 組件思路
本輪子是通過 React + TypeScript + Webpack 搭建的,至于環境的搭建這邊就不在細說了,自己動手谷歌吧。當然可以參考我的源碼。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
UI對話框一般是我們點擊按鈕彈出的這么一個東西,主要類型有 Alter, Confirm 及 Modal, Modal 一般帶有半透明的黑色背景。當然外觀可參考 AntD 或者 Framework 等。
確定 APIAPI 方面主要還是要參考同行,因為如果有一天,別人想你用的UI框架時,你的 API 跟他之前常用的又不用,這樣就加大了入門門檻,所以API 盡量保持跟現有的差不多。
對話框除了提供顯示屬性外,還要有點擊確認后的回放函數,如:
alert("你好").then(fn) confirm("確定?").then(fn) modal(組件名)實現
Dialog 源碼已經上傳到這里。
dialog/dialog.example.tsx, 這里 state ,生命周期使用 React 16.8 新出的 Hook,如果對 Hook 不熟悉可以先看官網文檔。
dialog/dialog.example.tsx
import React, {useState} from "react" import Dialog from "./dialog" export default function () { const [x, setX] = useState(false) return () }
dialog/dialog.tsx
import React from "react" interface Props { visible: boolean } const Dialog: React.FunctionComponent= (props) => { return ( props.visible ? dialog: null ) } export default Dialog
運行效果
顯示內容上述還有問題,我們 dialog 在組件內是寫死的,我們想的是直接通過組件內包裹的內容,如:
// dialog/dialog.example.tsx ... ...
這樣寫,頁面上是不會顯示 hi 的,這里 children 屬性就派上用場了,我們需要在 dialog 組件中進一步驟修改如下內容:
// dialog/dialog.tsx ... return ( props.visible ?顯示遮罩{props.children}: null ) ...
通常對話框會有一層遮罩,通常我們大都會這樣寫:
// dialog/dialog.tsx ... props.visible ?: null ...{props.children}
這種結構有個不好的地方就是點擊遮罩層的時候要關閉對話框,如果是用這種結構,用戶點擊任何 div,都相當于點擊遮罩層,所以最好要分開:
// dialog/dialog.tsx ......{props.children}
由于 React 要求最外層只能有一個元素, 所以我們多用了一個 div 包裹起來,但是這種方法無形之中多了個 div,所以可以使用 React 16 之后新出的 Fragment, Fragment 跟 vue 中的 template 一樣,它是不會渲染到頁面的。
import React, {Fragment} from "react" import "./dialog.scss"; interface Props { visible: boolean } const Dialog: React.FunctionComponent完善頭部,內容及底部= (props) => { return ( props.visible ? : null ) } export default Dialog {props.children}
這里不多說,直接上代碼
import React, {Fragment} from "react" import "./dialog.scss"; import {Icon} from "../index" interface Props { visible: boolean } const Dialog: React.FunctionComponent= (props) => { return ( props.visible ? : null ) } export default Dialog 提示 {props.children}
從上述代碼我們可以發現我們寫樣式的名字時候,為了不被第三使用覆蓋,我們自定義了一個 fui-dialog前綴,在寫每個樣式名稱時,都要寫一遍,這樣顯然不太合理,萬一哪天我不用這個前綴時候,每個都要改一遍,所以我們需要一個方法來封裝。
咱們可能會寫這樣方法:
function scopedClass(name) { return `fui-dialog-${name}` }
這樣寫不行,因為我們 name 可能不傳,這樣就會多出一個 -,所以需要進一步的判斷:
function scopedClass(name) {
return `fui-dialog-${name ? "-" + name : ""}` }
那還有沒有更簡潔的方法,使用 filter 方法:
function scopedClass(name ?: string) { return ["fui-dialog", name].filter(Boolean).join("-") }
調用方式如下:
....... 提示 {props.children}
大家在想法,這樣寫是有問題,每個組件都寫一個函數嗎,如果 Icon 組件,我還需要寫一個 fui-icon, 解決方法是把 前綴當一個參數,如:
function scopedClass(name ?: string) { return ["fui-dialog", name].filter(Boolean).join("-") }
調用方式如下:
className={scopedClass("fui-dialog", "mask")}
這樣寫,還不如直接寫樣式,這種方式是等于白寫了一個方法,那怎么辦?這就需要高階函數出場了。實現如下:
function scopeClassMaker(prefix: string) { return function (name ?: string) { return [prefix, name].filter(Boolean).join("-") } } const scopedClass = scopeClassMaker("fui-dialog")
scopeClassMaker 函數是高級函數,返回一個帶了 prefix 參數的函數。
事件處理在寫事件處理之前,我們 Dialog 需要接收一個 buttons 屬性,就是顯示的操作按鈕并添加事件:
// dialog/dialog.example.tsx ... ...
咱們看到這個,第一反應應該是覺得這樣寫很麻煩,我寫個 dialog, visible要自己,按鈕要自己,連事件也要自己寫。請接受這種設定。雖然麻煩,但非常的好理解。這跟 Vue 的理念是不太一樣的。當然后面會進一步驟優化。
組件內渲染如下:
運行起來你會發現有個警告:
主要是說我們渲染數組時,需要加個 key,解決方法有兩種,就是不要使用數組方式,當然這不治本,所以這里 React.cloneElemen 出場了,它可以克隆元素并添加對應的屬性值,如下:
{ props.buttons.map((button, index) => { React.cloneElement(button, {key: index}) }) }
對應的點擊關閉事件相對容易這邊就不講了,可以自行查看源碼。
接下來來看一個樣式的問題,首先先給出我們遮罩的樣式:
.fui-dialog { position: fixed; background: white; min-width: 20em; z-index: 2; border-radius: 4px; top: 50%; left: 50%; transform: translate(-50%, -50%); &-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: fade_out(black, 0.5); z-index: 1; } .... 以下省略其它樣式 }
我們遮罩 .fui-dialog-mask 使用 fixed 定位感覺是沒問題的,那如果在調用 dialog 同級在加以下這么元素:
666
運行效果:
發現遮罩并沒有遮住 666 的內容。這是為什么?
看結構也很好理解,遮罩元素與 666 是同級結構,且層級比 666 低,當然是覆蓋不了的。那咱們可能就會這樣做,給.fui-dialog-mask設置一個 zIndex 比它大的唄,如 9999。
效果:
恩,感覺沒問題,這時我們在 Dialog 組件在嵌套一層 zIndex 為 9 的呢,如:
運行效果如下:
發現,父元素被壓住了,里面元素 zIndex 值如何的高,都沒有效果。
那這要怎么破?答案是不要讓它出現在任何元素的里面,這怎么可能呢。這里就需要引出一個神奇的 API了。這個 API 叫做 傳送門(portal)。
用法如下:
return ReactDOM.createPortal( this.props.children, domNode );
第一個參數就是你的 div,第二個參數就是你要去的地方。
import React, {Fragment, ReactElement} from "react" import ReactDOM from "react-dom" import "./dialog.scss"; import {Icon} from "../index" import {scopedClassMaker} from "../classes" interface Props { visible: boolean, buttons: Array, onClose: React.MouseEventHandler, closeOnClickMask?: boolean } const scopedClass = scopedClassMaker("fui-dialog") const sc = scopedClass const Dialog: React.FunctionComponent = (props) => { const onClickClose: React.MouseEventHandler = (e) => { props.onClose(e) } const onClickMask: React.MouseEventHandler = (e) => { if (props.closeOnClickMask) { props.onClose(e) } } const x = props.visible ? : null return ( ReactDOM.createPortal(x, document.body) ) } Dialog.defaultProps = { closeOnClickMask: false } export default Dialog 提示 {props.children}
運行效果:
當然這樣,如果 Dialog 層級比同級的 zIndex 小的話,還是覆蓋不了。 那 zIndex 一般設置成多少比較合理。一般 Dialog 這層設置成 1, mask 這層設置成2。定的越小越好,因為用戶可以去改。
zIndex 的管理zIndex 管理一般就是前端架構師要做的了,根據業務產景來劃分,如廣告肯定是要在頁面最上面,所以 zIndex 一般是屬于最高級的。
便利的 API 之 Alert上述我們使用 Dialog 組件調用方式比較麻煩,寫了一堆,有時候我們想到使用 alert 直接彈出一個對話框這樣簡單方便。如
example 3
我們想直接點擊 button ,然后彈出我們自定義的對話框內容為1 ,需要在 Dialog 組件內我們需要導出一個 alert 方法,如下:
// dialog/dialog.tsx ... const alert = (content: string) => { const component = const div = document.createElement("div") document.body.append(div) ReactDOM.render(component, div) } export {alert} ...
運行效果:
但有個問題,因為對話框的 visible 是由外部傳入的,且 React 是單向數據流的,在組件內并不能直接修改 visible,所以在 onClose 方法我們需要再次渲染一個新的組件,并設置新組件 visible 為 ture,覆蓋原來的組件:
... const alert = (content: string) => { const component = const div = document.createElement("div") document.body.append(div) ReactDOM.render(component, div) } ..便利的 API 之 confirm
confirm 調用方式:
第一個參數是顯示的內容,每二個參數是確認的回調,第三個參數是取消的回調函數。
實現方式:
const confirm = (content: string, yes?: () => void, no?: () => void) => { const onYes = () => { ReactDOM.render(React.cloneElement(component, {visible: false}), div) ReactDOM.unmountComponentAtNode(div) div.remove() yes && yes() } const onNo = () => { ReactDOM.render(React.cloneElement(component, {visible: false}), div) ReactDOM.unmountComponentAtNode(div) div.remove() no && no() } const component = ( ) const div = document.createElement("div") document.body.appendChild(div) ReactDOM.render(component, div) }
事件處理跟 Alter 差不多,唯一多了一步就是 confirm 當點擊 yes 或者 no 的時候,如果外部有回調就需要調用對應的回調函數。
便利的 API 之 modalmodal 調用方式:
modal 對應傳遞的內容就不是單單的文本了,而是元素。
實現方式:
const modal = (content: ReactNode | ReactFragment) => { const onClose = () => { ReactDOM.render(React.cloneElement(component, {visible: false}), div) ReactDOM.unmountComponentAtNode(div) div.remove() } const component = const div = document.createElement("div") document.body.appendChild(div) ReactDOM.render(component, div) }
注意,這邊的 content 類型。
運行效果:
這還有個問題,如果需要加按鈕呢,可能會這樣寫:
這樣是關不了的,因為 Dialog 是封裝在 modal 里面的。如果要關,必須控制 visible,那很顯然我從外面控制不了里面的 visible,所以這個 button 沒有辦法把這個 modal 關掉。
解決方法就是使用閉包,我們可以在 modal 方法里面把 close 方法返回:
const modal = (content: ReactNode | ReactFragment) => { const onClose = () => { ReactDOM.render(React.cloneElement(component, {visible: false}), div) ReactDOM.unmountComponentAtNode(div) div.remove() } const component = const div = document.createElement("div") document.body.appendChild(div) ReactDOM.render(component, div) return onClose; }
最后多了一個 retrun onClose,由于閉包的作用,外部調用返回的 onClose 方法可以訪問到內部變量。
調用方式:
const openModal = () => { const close = modal(重構 API你好
) }close()}>close modal
在重構之前,我們先要抽象 alert, confirm, modal 中各自的方法:
alert | confirm | modal |
onClose | onClose * 2 | onClose |
component | component | component |
render | render | render |
return api | ||
從表格可以看出,modal 與其它兩個只多了一個 retrun api,其實其它兩個也可以返回對應的 Api,只是我們沒去調用而已,所以補上:
alert | confirm | modal |
onClose | onClose * 2 | onClose |
component | component | component |
render | render | render |
return api | return api | return api |
這樣一來,這三個函數從抽象層面上來看是類似的,所以這三個函數應該合成一個。
首先抽取公共部分,先取名為x ,內容如下:
const x= (content: ReactNode, buttons ?:Array, afterClose?: () => void) => { const close = () => { ReactDOM.render(React.cloneElement(component, {visible: false}), div) ReactDOM.unmountComponentAtNode(div) div.remove() afterClose && afterClose() } const component = const div = document.createElement("div") document.body.append(div) ReactDOM.render(component, div) return close }
alert 重構后的代碼如下:
const alert = (content: string) => { const button =close()}>ok const close = x(content, [button]) }
confirm 重構后的代碼如下:
const confirm = (content: string, yes?: () => void, no?: () => void) => { const onYes = () => { close() yes && yes() } const onNo = () => { close() no && no() } const buttons = [yes ,no ] const close = modal(content, buttons, no) }
modal 重構后的代碼如下:
const modal = (content: ReactNode | ReactFragment) => { return x(content) }
最后發現其實 x 方法就是 modal 方法,所以更改 x 名為 modal,刪除對應的 modal 定義。
總結scopedClass 高階函數的使用
傳送門 portal
動態生成組件
閉包傳 API
本組件為使用優化樣式,如果有興趣可以自行優化,本節源碼已經上傳至這里中的lib/dialog。
參考方應杭老師的React造輪子課程
你的點贊是我持續分享好東西的動力,歡迎點贊!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109833.html
摘要:本文是造輪系列第三篇。造輪子系列組件思路造輪系列對話框組件思路想閱讀更多優質文章請猛戳博客一年百來篇優質文章等著你初始化參考組件分別分為五個組件。參考方應杭老師的造輪子課程交流干貨系列文章匯總如下,覺得不錯點個,歡迎加群互相學習。 本文是React造輪系列第三篇。 1.React 造輪子系列:Icon 組件思路 2.React造輪系列:對話框組件 - Dialog 思路 想閱讀更多優質...
摘要:但是,最后一步,事件怎么綁定呢這塊沒有深入研究了,不過我想,應該這樣去實現也是沒有問題的。的具體做法是,把方法放到了一個叫做的組件上去實現這個功能,然后再把內容放進這個組件。其他的邏輯比如顯示隱藏之類,全部都放到組件自身上去實現。 1、Dialog組件提供什么功能,解決什么問題? zent的Dialog組件,使用姿勢是這樣的(代碼摘自zent官方文檔:https://www.youza...
摘要:為什么會慢呢因為對的修改為影響網頁的用戶界面,重繪頁面是一項昂貴的操作。太多的操作會導致一系列的重繪操作,為了確保執行結果的準確性,所有的修改操作是按順序同步執行的。回流操作主要會發生在幾種情況下當對節點執行新增或者刪除操作時。 一. 函數式編程 React 把用戶界面抽象成一個個組件,如按鈕組件 Button、對話框組件 Dialog、日期組件 Calendar。 開發者通過組...
摘要:經過派發器,調用回調,修改數據層。這是一個循環往復的過程,最大的特點是數據單向流動。是團隊開發并開源的一款數據流框架。它是一些修改組件狀態的函數大體與類似,一般也稱之為集合。的第一個參數始終是。簡單到極致,就是我們設計的初衷。 Flux 是 React 框架的好伴侶。它優秀的單向數據流設計,使得數據的流向更加清晰,能幫助開發者更好的管理和調試組件的內部狀態。Facebook 官方出 F...
摘要:發布是由團隊開源的,操作接口庫,已成為事實上的瀏覽器操作標準。本周正式發布,為我們帶來了,,支持自定義頭部與腳部,支持增強,兼容原生協議等特性變化。新特性介紹日前發布了大版本更新,引入了一系列的新特性與提升,本文即是對這些變化進行深入解讀。 showImg(https://segmentfault.com/img/remote/1460000012940044); 前端每周清單專注前端...
閱讀 4172·2021-11-22 13:52
閱讀 2089·2021-09-22 15:12
閱讀 1128·2019-08-30 15:53
閱讀 3463·2019-08-29 17:12
閱讀 2196·2019-08-29 16:23
閱讀 1661·2019-08-26 13:56
閱讀 1778·2019-08-26 13:44
閱讀 1896·2019-08-26 11:56