基于业务需要,需要自己实现一个在鼠标点击处弹出一个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应该是相对于传入的容器的位置