基于业务需要,需要自己实现一个在鼠标点击处弹出一个popup组件然后点击选择上传方式并弹出弹窗进行操作的功能
业务要求
需要在B端(React框架)和C端(Vue框架)实现一个功能相同的上传组件,该上传组件具备以下功能:
点击指定区域(通常是上传按钮)时,弹出一个如图的popup弹窗,供用户选择上传方式
点击”素材库选择”时,打开一个网盘选择窗口,在其中选择某个符合业务要求的文件,并将文件信息返回给业务系统
点击”本地上传”时,先打开文件选择窗口,选择本地文件上传,并在上传完成之后选择是否要同步到素材库中,如果是单个图片就进行尺寸裁切(裁切比例按照业务系统传入的参数确定),裁切后进行上传再选择是否要同步到素材库
需要框架不同的两个端均可以实现相同功能,样式和逻辑完全一致
思考
在该功能之前,已经因为之前的需求实现了在指定容器中渲染一个网盘文件管理器(带有查看、上传、编辑、删除文件或文件夹功能)的react为基础的sdk,因此该次应考虑在该sdk基础上添加新的方法来实现该功能,同时这样也能保证多端的统一性和后续维护的简便性
实践
1、实现将react项目打入一个sdk中供人引用
该部分之前已经实现过,具体方式为将一个React项目的初始化部分作成一个构造器函数, 并在webpack打包配置中将该函数作为入口打包到单一文件中,页面通过载入该文件(可以通过js引入或npm的import方式),
将构造器函数挂载到window对象上,在需要使用时将容器元素和其他sdk内部要求的参数作为新建实例的参数实例化, 实例化过程中会自动将该React项目初始化在指定的容器上
入口文件例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class SDKName { constructor(options) { this.options = options this._init() }
_init() { this.options.container && this._initDom() }
_initDom() { ReactDOM.render(<App {...this.options} />, this.container) }
unmount() { this.options.container && ReactDOM.unmountComponentAtNode(this.options.container) } }
export default SDKName
|
webpack配置文件例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const options = { entry: { SDKName: './src/index.js' }, output: { filename: '[name].min.js', library: 'SDKName', libraryTarget: 'window', libraryExport: 'default', } }
|
这样便能实现打包时生成一个SDKName.min.js的文件,并且该文件载入之后会将SDKName这个class类挂载到window对象上,以供调用
使用例:
1 2 3 4 5 6 7 8 9
| <script src="https://someWebSite.com/SDKName.min.js"></script> <div id="root"></div> <script> const sdk = new SDKName({ container: document.querySelector('#root') }) </script>
|
2、实现点击特定元素弹出由SDK控制的弹出菜单
实现思路:应当向SDK对象上挂载两个方法,一个可以在指定的容器*里渲染弹出菜单,一个可以由外部主动删除渲染的菜单
*指定的容器主要是针对弹出菜单后页面会有上下滚动的需求而设定(参考了ant-design的设计),实际上渲染的菜单的页面相对位置是由传入组件的点击事件event的点击位置决定的
挂载渲染和卸载菜单方法:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| class SDKName {
unmountSelection() { if (this.uploadSelectorContainer) { ReactDOM.unmountComponentAtNode(this.uploadSelectorContainer) this.uploadSelectorContainer.parentNode.removeChild(this.uploadSelectorContainer) this.uploadSelectorContainer = null } }
showUploadSelection({types, event, getPopupContainer}) { console.log('=============参数:', types, event, getPopupContainer) if (event.target) { if (isClickSelf(event)) { console.log('=============点击在组件内,不处理', event) return null } } this.unmountSelection()
const newUploadSelectorContainer = document.createElement('div') newUploadSelectorContainer.id = SELECTOR_CONTAINER_ID
const parent = getPopupContainer ? getPopupContainer() : document.body parent.appendChild(newUploadSelectorContainer)
ReactDOM.render(<UploadSelector event={event} types={types} container={parent}/>, newUploadSelectorContainer)
this.uploadSelectorContainer = newUploadSelectorContainer return {unmount: this.unmountSelection, eventEmitter: uploadSelectionSubject} } }
|
这样就可以实现在任意html节点上挂载和销毁我们的弹出菜单,但是弹出菜单的位置应当是点击事件的位置,所以UploadSelector组件内部需要对event做处理
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| import React, {useEffect, useState} from 'react'
import {Button, Modal} from 'antd' import s from './index.module.less'
const selectionIdealHeight = 96 const selectionIdealWidth = 126
const UploadSelector = ({container, event, types}) => {
const [style, setStyle] = useState({})
const [selectorVisible, setSelectorVisible] = useState(false) useEffect(() => { const rect = container.getBoundingClientRect()
let left = Math.max(0, event.clientX - rect.x) let top = Math.max(0, event.clientY - rect.y)
const windowHeight = window.innerHeight
const windowWidth = window.innerWidth
if (windowHeight - event.clientY < selectionIdealHeight) { top -= selectionIdealHeight }
if (windowWidth - event.clientX < selectionIdealWidth) { left -= selectionIdealWidth }
setStyle({ left, top }) }, [event, container])
const showMaterial = () => { } const uploadBySelf = e => { uploadSelectionSubject.next({ event: UPLOAD_EVENTS.UPLOAD_BY_SELF }) }
return <div className={s.selectorWrapper} style={style}> <div className={s.btnWrapper}> <Button onClick={showMaterial} className={s.btn}>素材库选择</Button> </div> <div className={s.btnWrapper}> <Button onClick={uploadBySelf} className={s.btn}>本地上传</Button> </div> </div> }
export default UploadSelector
|
当event或container参数变化时,计算渲染的具体位置并改变容器元素selectorWrapper的style,selectorWrapper的css中的position属性是absolute,所以对应的left和top应该是相对于传入的容器的位置