前言

最近公司让写一个可以自由拖拽放大的图片查看器,我寻思这还不简单,一顿操作猛如虎,俩小时后:

事实证明,一旦涉及到 DOM 的变换操作,如果很多细节考虑不全,抓过来就写,那基本就凉了。于是我仔细分析了下需求,发现和蓝湖的渲染图查看功能很类似,那这回就整理一下思路,从头开始,写一个模仿蓝湖的图片查看器。

最终效果

项目地址

  • npm 包地址
  • github 项目地址

关于 react-picture-viewer 组件的更多细节和配置项,都在 github 上了,觉得好用的朋友可以给个 star⭐️,也可以 fork 下来作为参考~?

功能拆分

这个图片查看器组件,拆分下来看,其实也就两个功能:

  1. 能够使用鼠标自由拖拽图片位置
  2. 能够使用鼠标滚轮进行缩放查看图片

两个功能都不难理解,但是组合实现一定难度,功能之间会有各种联系及需要注意的问题,先来分别分析一下这两个功能的原理。

图片拖拽功能原理

上面是我画的一张图,大概解释了一下鼠标拖拽的基本原理,我们可以用文字的方式来描述这个过程:

  1. 给定一个视口,视口里放置一张需要操作的图片(图片和视口的布局呈现方式有几种,我使用的是 top 和 left 的绝对定位方式来定位和操作图片)。
  2. 在图片的任意区域按下鼠标左键,记录一下当前鼠标位置到视口的初始距离 x1y1
  3. 按住鼠标左键不放,在视口区域内移动鼠标,这个时候,实时记录当前鼠标位置到视口的距 x2y2,并且实时计算图片的距离增量:图片 x 轴上移动距离 = x2 - x1 && 图片 y 轴上移动距离 = y2 - y1,最后将图片的位移增量转换为 top left 样式呈现在网页上。
  4. 放开鼠标,记录最后一次的鼠标位置信息,并清空鼠标的初始位置信息,图片固定,至此,一次拖拽操作完成。

图片缩放功能原理

相比于图片拖拽,图片缩放原理稍微复杂一点,我在上图把一些关键的数据都标注出来了,我们可以对照图来分析:

在使用鼠标滚轮进行缩放时,我们希望图片能以鼠标位置为缩放中心进行缩放,如果要实现这一功能,那么,在缩放时既要改变图片尺寸,又要改变图片的绝对定位。

图片缩放的功能也是借助于绝对定位实现的(蓝湖的渲染图查看功能也是依附于绝对定位),最好不要用网上说的什么 transform-origin 这种原理来尝试做缩放,做不出这种效果,缩放的时候图片跟着鼠标飘,没卵用的。

图片缩放功能最关键的一步,就是如何实时计算,得到图片缩放后的 top 和 left 值

其实,在标注图下不难发现其中存在的定量关系:x1 和 originWidth 的比值;一定是等于 x2 和 currentWidth 的比值的。那么图片在 left 和 top 方向上的增量就可以写成:(x1 / originWidth) * currentWidth 和 (y1 / originHeight) * currentHeight;然后用图片初始状态的 left 和 top 分别去加上对应增量,就能得到缩放过程中实时的 left 和 top 值。

根据以上的文字描述,我们下面开始动手,用代码将它实现出来。

这边我是使用的 react 进行组件开发,使用 vue 甚至不使用任何框架/库都没问题,所以还是需要根据具体的项目选型进行修改。

准备工作

在动手开始编写具体代码前,我们需要做一些准备工作。DOM 变换本身会涉及到比较多的原生 JS 事件,原生 DOM 的位置信息以及获取方式、盒模型等,下面的这张图或许可以更好的帮助理解这些概念,对这些概念还不是很清楚的话,可以在编写代码的过程中对照问题,查漏补缺。

首先我们需要封四个基础的工具方法,分别是:

  1. 判断一个DOM元素是否包裹在另一个DOM元素中的方法
  2. 获取某个 DOM 元素相对视口的位置信息的方法
  3. 获取鼠标当前相对于某个元素位置的方法
  4. 获取图片原始尺寸信息的方法

这四个工具方法是图片查看器组件得以实现的基础,在开发的过程中会多次运用到这些工具方法,来获取各种位置信息,下面开始封装。

1. 判断一个DOM元素是否包裹在另一个DOM元素中的方法【父子关系或者层级嵌套都可以】

    /*** 判断一个DOM元素是否包裹在另一个DOM元素中【父子关系或者层级嵌套都可以】* @param  {Object} DOM 事件对象中的event.target/或者是需要检测的DOM元素* @param  {Object} targetDOM 参照节点* @return {Boolean} true 是包裹关系;false不是包裹关系*/_inTargetArea = (DOM, targetDOM) => {// 如果检测节点就是参照节点,那么也生效if (DOM === targetDOM) return truelet parent = DOM.parentNode// 向上循环查找,找到父元素就返回 true,找不到返回 falsewhile (parent != null) {if (parent === targetDOM) return trueDOM = parentparent = DOM.parentNode}return false}
复制代码

2. 获取某个 DOM 元素相对视口的位置信息

这边我使用的是getBoundingClientRect来获取 DOM 元素相对于视口的位置信息,注意,这里是相对于视口位置,不是文档位置

    /*** 获取某个 DOM 元素相对视口的位置信息* @param el {object} 目标元素* @return object {object} 位置信息对象*/_getOffset = (el) => {const doc = document.documentElementconst docClientWidth = doc.clientWidthconst docClientHeight = doc.clientHeightlet positionInfo = el.getBoundingClientRect()return {left: positionInfo.left,top: positionInfo.top,right: docClientWidth - positionInfo.right,bottom: docClientHeight - positionInfo.bottom}}复制代码

3. 获取鼠标当前相对于某个元素的位置

在上面两个方法的基础上,再封装一个获取鼠标当前相对于某个元素的位置的方法

    /*** 获取鼠标当前相对于某个元素的位置* @param e {object} 原生事件对象* @param target {DOMobject} 目标DOM元素* @return object 包括 offsetLeft 和 offsetTop** Tips:* 1.offset 相关属性在 display: none 的元素上失效,为0* 2.offsetWidth/offsetHeight 包括border-width,clientWidth/clientHeight不包括border-width,只是可见区域而已* 3.offsetLeft/offsetTop 是从当前元素边框外缘开始算,一直到定位父元素的距离,clientLeft/clientTop其实就是border-width*/_getOffsetInElement = (e, target) => {// 获取事件触发时,鼠标所在的 DOM 节点let currentDOM = e.target || e.toElement// 如果这个节点不在传入的参照节点中,则 return nullif (!this._inTargetArea(currentDOM, target)) return nulllet left, top, right, bottom// 使用前面封装好的 _getOffset 方法,获取参照节点相对于视口位置信息const { left: x, top: y } = this._getOffset(target)// 计算当前鼠标相对于参照节点的位置信息left = e.clientX - xtop = e.clientY - yright = target.offsetWidth - leftbottom = target.offsetHeight - topreturn { top, left, right, bottom }}
复制代码

4. 获取图片原始尺寸信息的方法

获取图片原始尺寸信息有多种方法,这边选择其中一种,只要能拿到准确的图片尺寸即可

    /*** 获取图片原始尺寸信息* @param image* @returns {Promise<any>}* @private*/_getImageOriginSize = (image) => {const src = typeof image === 'object' ? image.src : imagereturn new Promise(resolve => {const image = new Image()image.src = srcimage.onload = function () {const { width, height } = imageresolve({width,height})}})}
复制代码

四个基础方法封装完成,下面开始实现具体功能。

代码实现图片拖拽

由于 react 是单向数据流驱动,所以在写业务之前需要先设计好整个流程。

我的期望是,在具体的业务方法里,只做 state 的变更操作,state和props变更后引起的 DOM 变换,全部由生命周期进行监控和操作,这样的话,比较方便后期的维护和扩展以及问题的排查。

明确期望后,开始动手写业务逻辑:

constructor

首先定义好 constructor,在里面定义两个属性,这两个属性在 componentDidMount 时用来存放视口和图片的 DOM 节点

    constructor() {super()this.viewportDOM = nullthis.imgDOM = null}
复制代码

render 函数

然后把 render 写好,并且绑定不同事件的对应处理函数:

    render() {const { id, children, className } = this.propsreturn (<div id={id}className={`react-picture-viewer ${className}`}onMouseLeave={this.handleMouseLeave}onMouseDown={this.handleMouseDown}onMouseMove={this.handleMouseMove}onMouseUp={this.handleMouseUp}>{children}</div>)}
复制代码

state

我们需要在 state 里存储一些数据信息,这些数据是组件内部需要使用的,组件会根据这些数据的变化实时 re-render

    state = {focus: false, // 鼠标是否按下,处于可拖动状态imageWidth: 0, // 图片初始宽度imageHeight: 0, // 图片初始高度startX: 0, // 鼠标按下时,鼠标距离 viewport 的初始 X 位置startY: 0, // 鼠标按下时,鼠标距离 viewport 的初始 Y 位置startLeft: 0, // 图片距离 viewport 的初始 LeftstartTop: 0, // 图片距离 viewport 的初始 TopcurrentLeft: 0, // 图片当前距离 viewport 的 leftcurrentTop: 0, // 图片当前距离 viewport 的 top}
复制代码

props

props 是外部传入的属性,相当于提供给用户对组件的可配置项,这边可以思考一下,需要满足图片拖拽功能的情况下,props 需要传入哪些参数?

  1. children 组件的子组件插槽,类似于 vue 里的 slot,需要放置 <img /> 标签在里面,这个是必须的
  2. 视口组件的唯一标识 key,类似于 react 提供的 key,这个唯一标识在多组件实例的情况下很有用
  3. 视口的尺寸数据,需要留给用户定义
  4. 组件需要暴露给外部一个可添加的 className 样式类名

这里只是我提供的一些参考,具体可以根据业务需求进行删减

    static propTypes = {id: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), // 组件唯一的标识 idwidth: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), // viewport 视口的宽度height: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), // viewport 视口的高度children: PropTypes.object.isRequired, // slot 插槽className: PropTypes.string, // classNamecenter: PropTypes.bool, // 图片位置是否初始居中contain: PropTypes.bool // 图片尺寸是否初始包含在视口范围内}static defaultProps = {id: 'viewport',width: '600px',height: '400px',// children 由外部传入组件,用组件包裹嵌套即可center: true,contain: true}
复制代码

事件监听函数

处理鼠标按下事件

    /*** 处理鼠标按下* @param e*/handleMouseDown = (e) => {// 如果 mousedown 的触发对象不是图片,就 returnconst currentDOM = e.target || e.toElementif (currentDOM !== imgDOM) return// 记录当前鼠标相对于视口元素的位置let { top: startY, left: startX } = this._getOffsetInElement(e, viewportDOM)this.setState({focus: true, // 激活 focus 状态startX, // 存储鼠标的起始位置startY})}
复制代码

处理鼠标移动事件

    /*** 处理鼠标移动* @param e*/handleMouseMove = (e) => {const { focus, startX, startY, startTop, startLeft } = this.state// 如果当前状态未激活,就 returnif (!focus) return// 实时计算鼠标的当前位置let { left: currentX, top: currentY } = this._getOffsetInElement(e, viewportDOM)// 计算鼠标的移动位移差let [ diffX, diffY ] = [ currentX - startX, currentY - startY ]// 根据鼠标位移差来设置图片的实时位置this.setState({currentLeft: startLeft + diffX,currentTop: startTop + diffY})}
复制代码

处理鼠标放开事件

    /*** 处理鼠标放开*/handleMouseUp = () => {const { currentLeft, currentTop } = this.statethis.setState({focus: false, // 重置激活状态startX: 0, // 重置鼠标的初始位置startY: 0,startLeft: currentLeft, // 将鼠标放开的位置作为下一次图片运动的起始位置startTop: currentTop})}
复制代码

这块还有个小的细节优化,当鼠标拖拽图片时移除视口,需要使拖拽状态失活

    /*** 处理鼠标移出*/handleMouseLeave = () => {this.handleMouseUp()}
复制代码

生命周期

根据之前的期望,事件回调里只会处理 state,由 state / props 的变化而导致的组件的 re-render 全部放在具体的生命周期里监听执行。

上图是我在网上找的一张生命周期的执行图例,对生命周期的各个阶段拆分的比较详细,戳这里有更详细的 React 生命周期介绍。我们下面根据 react 生命周期里执行的具体顺序,来完善组件功能

componentDidMount

在这个阶段里,一般我们会执行一些初始化的操作,包括对视口的初始化,和图片的初始化。

下面的代码量有点大,因为不同于单纯的 state 变换。一旦涉及到大量的 DOM 操作,必然是脏活累活,这边还是贴出代码和注释,以供需要的朋友参考。

    componentDidMount() {const { id, width, height } = this.propsthis.viewportDOM = document.getElementById(id)this.imgDOM = this.viewportDOM.getElementsByTagName('img')[0]// 视口信息初始化this.initViewport(width, height)// 图片信息初始化this.initPicture()}
复制代码
    // 视口信息初始化initViewport = (width, height) => {// 如果是字符串,就将字符串作为尺寸设置;否则是数字的话,就在后面加 px 设置this.viewportDOM.style.width = isNaN(+width) ? width : `${width}px`this.viewportDOM.style.height = isNaN(+height) ? height: `${height}px`}
复制代码
/*** 图片初始化,包括:* 1. 记录初始图片尺寸* 2. 初始图片位置是否居中* @param nextProps 最新的 props*/initPicture = (nextProps) => {// 如果没有传递,默认使用 this.propsnextProps = nextProps || this.propsconst { children: { props: { src } }, center, contain } = nextProps// 由于获取图片尺寸是异步操作,这边的改变图片位置需要写成回调的形式const callback = center ? this.changeToCenter : this.changeToBasePoint// 这块有个执行顺序// 必须是先确定尺寸,再确定位置// 图片尺寸确定后,更改图片位置的操作作为 callback 随后执行if (contain) {// 需要图片尺寸包含在视口的情况this.changeToContain(src, callback)} else {// 图片以原始尺寸呈现的情况this.changeToOrigin(src, callback)}}
复制代码
    /*** 设置图片尺寸为 contain* @param src {String} 需要操作的图片的 src* @param callback {Function} changeToContain 完成后的回调函数,接受更新后的图片尺寸,即 imageWidth 和 imageHeight 两个参数*/changeToContain = (src, callback) => {// 有传入就用传入的,否则用默认的src = src || this.props.srccallback = isFunction(callback) ? callback : () => {}// 获取图片原始尺寸的方法,之前已经封装好了的基础方法this._getImageOriginSize(src).then(({ width: imageOriginWidth, height: imageOriginHeight }) => {// 根据图片和视口的尺寸对应关系,重新计算出新的图片尺寸const { imageWidth, imageHeight } = this.recalcImageSizeToContain(imageOriginWidth, imageOriginHeight)this.setState({imageWidth,imageHeight}, () => { callback(imageWidth, imageHeight) })}).catch(e => {console.error(e)})}
复制代码
    /*** 重新计算图片尺寸,使宽高都不会超过视口尺寸* 这边用到了递归处理,大概的思路就是:* 1. 找到图片大于视口的那一段尺寸* 2. 将这段超标图片尺寸替换为视口对应尺寸* 3. 根据原始图片的宽高比,计算另一条尺寸的新值* 4. 返回新的图片尺寸* @param imageWidth* @param imageHeight* @returns {*}*/recalcImageSizeToContain = (imageWidth, imageHeight) => {const rate = imageWidth / imageHeightconst viewportDOM = this.viewportDOMconst [ viewPortWidth, viewPortHeight ] = [ viewportDOM.clientWidth, viewportDOM.clientHeight ]if (imageWidth > viewPortWidth) {imageWidth = viewPortWidthimageHeight = imageWidth / ratereturn this.recalcImageSizeToContain(imageWidth, imageHeight)} else if (imageHeight > viewPortHeight) {imageHeight = viewPortHeightimageWidth = imageHeight * ratereturn this.recalcImageSizeToContain(imageWidth, imageHeight)} else {return { imageWidth, imageHeight }}}
复制代码
    /*** 设置图片尺寸为原始尺寸* @param src {String} 需要操作的图片的 src* @param callback {Function} changeToOrigin 完成后的回调函数,接受更新后的图片尺寸,即 imageWidth 和 imageHeight 两个参数*/changeToOrigin = (src, callback) => {// 有传入就用传入的,否则用默认的src = src || this.props.srccallback = isFunction(callback) ? callback : () => {}// 获取图片原始尺寸的方法,之前已经封装好了的基础方法this._getImageOriginSize(src).then(({ width: imageWidth, height: imageHeight }) => {this.setState({imageWidth,imageHeight}, () => { callback(imageWidth, imageHeight) })}).catch(e => {console.error(e)})}
复制代码
    /*** 设置图片位置为基准点位置* 基准点位置,基于视口: top: 0 && left: 0*/changeToBasePoint = () => {this.setState({currentLeft: 0,currentTop: 0,startLeft: 0,startTop: 0})}
复制代码

componentWillReceiveProps

componentDidMount 我们已经执行完了相关的初始化操作,当外部传入的 props 发生变动之后,我们依旧需要执行一遍初始化逻辑,不过有一处不同:

我们来考虑一下这个场景:用户使用了这个组件,但是在父组件里还有其他无关的组件及状态,只要父组件由于任何微小的改动 re-render,那么它会重新派发一份新的 props ,这样一来,就算子组件的 props 没有任何变化,子组件依旧会重新 re-render,重新走一遍生命周期,这样必然是不合理的

导致这个问题的原因其实还是在于 React 的实现理念,它所作的事情,本质上来说是提供基于数据的快照,好在我们可以在代码层面规避这种问题。

    componentWillReceiveProps(nextProps) {// 如果检测到 props 确实有变化,再去重新 initconst flag = !isEqual(this.props, nextProps, 'children') || !isEqual(this.props.children.props, nextProps.children.props)flag && this.initPicture(nextProps)}
复制代码

有兴趣的小伙伴可以看一下这个 isEqual 的实现原理,它的作用就是判断两个对象是否相同,也是使用了递归:

/*** 判断两个对象是否一样(注意,一样不是相等)* 1. 如果是非引用类型的值,直接使用全等比较* 2. 如果是数组或对象,则会先比较引用指针是否一一致* 3. 引用指针不一致,再比较每一项是否相同** @param target {All data types} 参照对象* @param obj {All data types} 比较对象* @param exceptKey {String} 不检测掉的对象 key 一旦检测到对象内含有此 key 直接默认相同,返回true* @returns {*}*/
function isEqual(target, obj, exceptKey) {if (typeof target !== typeof obj) {return false} else if (typeof target === 'object') {if (target === obj) { // 先比较引用return true} else if (Array.isArray(target)) { // 数组if (target.length !== obj.length) { // 长度不同直接 return falsereturn false} else { // 否则依次比较每一项return target.every((item, i) => isEqual(item, obj[i], exceptKey))}} else { // 对象const targetKeyList = Object.keys(target)const objKeyList = Object.keys(obj)if (targetKeyList.length !== objKeyList.length) { // 如果 keyList 的长度不同直接 return falsereturn false} else {return targetKeyList.every((key) => key === exceptKey || isEqual(target[key], obj[key], exceptKey))}}} else {return target === obj}
}
复制代码

shouldComponentUpdate

shouldComponentUpdate 生命周期一般用来做 react 组件的性能优化,它必须返回一个布尔值,如果是 true 就代表需要重新渲染组件,false 就默认阻止了 componentWillUpdaterender 的执行,具体介绍可以自行了解一下。

    shouldComponentUpdate(nextProps, nextState, nextContext) {// state 或者 props 确实有更改,才需要 re-renderreturn !isEqual(this.state, nextState) || !isEqual(this.props, nextProps, 'children') || !isEqual(this.props.children.props, nextProps.children.props)}
复制代码

componentWillUpdate

shouldComponentUpdate 生命周期执行完成,接着就到了 componentWillUpdate 阶段。其实在之前的生命周期里,还是对 state 进行操作,componentWillUpdate 作为接受 state/props 变更后、组件 re-render 前的最后一步,需要根据 state 里的状态来执行 DOM 操作,我们把涉及 DOM 变换的逻辑全部放在这步执行

    componentWillUpdate(nextProps, nextState) {const { scale, imageWidth: originWidth, imageHeight: originHeight, currentLeft, currentTop } = nextStateconst currentImageWidth = scale * originWidthconst currentImageHeight = scale * originHeight// 改变图片位置this.changePosition(currentLeft, currentTop)// 改变图片尺寸this.changeSize(currentImageWidth, currentImageHeight)}
复制代码
    /*** 改变图片位置* @param currentLeft {Number} 当前 left* @param currentTop {Number} 当前 top*/changePosition(currentLeft, currentTop) {const imgDOM = this.imgDOMimgDOM.style.top = `${currentTop}px`imgDOM.style.left = `${currentLeft}px`}
复制代码
    /*** 调整尺寸* @param width* @param height*/changeSize(width, height) {const imgDOM = this.imgDOMimgDOM.style.maxWidth = imgDOM.style.maxHeight = 'none'imgDOM.style.width = `${width}px`imgDOM.style.height = `${height}px`}
复制代码

至此,代码实现拖拽逻辑完成。

代码实现图片缩放

虽然说图片缩放的功能比图片拖拽复杂,但是在实现图片拖拽的时候,我们已经默默完成了 80% 的工作量,下面只需要在原有的代码上做些增改,很容易就能完成图片缩放的逻辑了。

props

首先,props 里加一些配置:

  1. 最小缩放限制
  2. 最大缩放限制
  3. 缩放速率
    static propTypes = {// ...+ minimum: PropTypes.number, // 缩放的最小尺寸【零点几】+ maximum: PropTypes.number, // 缩放的最大尺寸+ rate: PropTypes.number, // 缩放的速率// ...}static defaultProps = {// ...+ minimum: 0.8,+ maximum: 8,+ rate: 10,// ...}
复制代码

state

    state = {+ scale: 1 // 图片缩放比率 minimum ~ maximum}
复制代码

事件处理函数

    /*** 处理滚轮缩放* @param e {Event Object} 事件对象*/handleMouseWheel = (e) => {const imgDOM = this.imgDOMconst { minimum, maximum, rate } = this.propsconst { imageWidth: originWidth, imageHeight: originHeight, currentLeft, currentTop, scale: lastScale } = this.stateconst [ imageWidth, imageHeight ] = [ imgDOM.clientWidth, imgDOM.clientHeight ]const event = e.nativeEvent || eevent.preventDefault()// 这块的 scale 每次都需要用 1 去加,作为图片的实时缩放比率let scale = 1 + event.wheelDelta / (12000 / rate)// 最小缩放至 minimum 就不能再缩小了// 最大放大至 maximum 倍就不能再放大了if ((lastScale <= minimum && scale < 1) || (lastScale >= maximum && scale > 1)) return// 真实的图片缩放比率需要用尺寸相除let nextScale = imageWidth * scale / originWidth// 进行缩放比率检测// 如果小于最小值,使用原始图片尺寸和最小缩放值// 如果大于最大值,使用最大图片尺寸和最大缩放值nextScale = nextScale <= minimum ? minimum : nextScale >= maximum ? maximum : nextScalelet currentImageWidth = nextScale * originWidthlet currentImageHeight = nextScale * originHeight// 使用之前封装好的方法,来获取当前鼠标距离屏幕的位置let { left, top } = this._getOffsetInElement(e, this.imgDOM)let rateX = left / imageWidthlet rateY = top / imageHeightlet newLeft = rateX * currentImageWidthlet newTop = rateY * currentImageHeightthis.setState({scale: nextScale,startLeft: currentLeft + (left - newLeft),startTop: currentTop + (top - newTop),currentLeft: currentLeft + (left - newLeft),currentTop: currentTop + (top - newTop)})}
复制代码

生命周期

这块的事件处理函数,绑定方式和之前略有不同,需要将滚轮事件使用原生绑定来处理,从而解决新版本 chrome 浏览器带来的 passive event listener,在对图片进行滚动缩放时无法使用 e.preventDefault 来禁用浏览器滚动问题

    componentDidMount() {// ...// 这边需要将滚轮事件使用原生绑定来处理// 从而解决新版本 chrome 浏览器带来的 passive event listener// 在对图片进行滚动缩放时无法使用 e.preventDefault 来禁用浏览器滚动问题+ this.imgDOM.addEventListener('wheel', this.handleMouseWheel, { passive: false })// ...}
复制代码

好了,现在图片缩放的功能也完成啦。???

最后再来的看一下功能组合后的效果图。

总结

emmmm,现在总算把功能写完,不用再回退代码了。整个过程经历了一次以后,才发现很多需求并不像刚开始想的那么简单。需求很容易,就两句话,但这两句话的需求,就像冰山一脚,真正将冰山支撑起来的很大一部分,不潜入水底是根本看不到的。

组件封装的过程,也可以看作是一个知识体系整理的过程,大量的知识碎片会在这种实战过程中被串联起来,最终构成一个完整的项目,我们现在可以从头到尾,详细地梳理一下这个项目使用到的知识片段:

  1. React 的概念及各种常见用法,包括但不限于:

    • react 概念的理解
    • reactstateprops 的概念及相互间的关系
    • jsx 语法
    • react 生命周期
  2. 基于 webpack 的工程化构建(虽然本文没说到,但是很重要,其中 webpack 构建又分为单页应用和多页应用,多页应用下 webpack 构建的可以参考我的另一篇文章:【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构)

    • webpack 的概念和使用场景
    • ES6jsx 语法编译
      • babel 的概念及出现原因
      • babel 一些常见的 presetsplugins 的使用场景
      • babel-polyfillbabel-runtime 的概念和异同
    • css/postcss/sass/less 语法编译
    • 本地服务搭建
    • 热更新的实现
    • webpack 打包速度优化 / 文件大小优化 / 文件缓存策略
  3. 原生 JS 事件

    • 事件类型、事件名称及事件的绑定方式
    • 事件冒泡 / 事件捕获 / 阻止默认事件
    • JS 原生事件对象携带的事件信息
    • 浏览器兼容性
  4. DOM 、文档流及盒模型

    • 常见的 DOM 操作方式及 DOM 属性
    • 文档流的概念
    • 盒模型的概念及结构,盒模型的一些尺寸数据的获取方式
    • 原生 API 的浏览器兼容性
  5. JS 基础

    • 数据类型判断
    • 静态作用域与闭包
    • this 和静态作用域的区别
    • this 的绑定方式和飘移问题
    • 原型链和继承,ES6class 构造方式
    • ES6promise
    • 递归
      • 递归的作用及场景
      • 调用栈的概念
      • 尾递归优化
  6. 如果还要发布到开源社区,又可以写一波 git 版本控制和 npm 发包相关的注意事项。

完蛋,这么一列发现自己还有很多不太清楚的地方,溜了,学习去了。

转载于:https://juejin.im/post/5cf873fdf265da1b8e708f50

【React组件】写一个模仿蓝湖的图片查看器相关推荐

  1. Python 写一个命令行版的火车票查看器

    用python另一个抢票神器,你get到了吗? 2017年时间飞逝,转眼间距离2018年春节还有不到1个月的时间,还在为抢不到火车票发愁吗?作为程序员的我们撸一个抢票软件可好? 难以想象的数据, 预示 ...

  2. 学习写一个模仿天猫网站

    学习完了前端的HTML CSS JavaScript等各项技术之后,会有一个感慨,各个知识点分开都不难,但是要做出一个成型的,好看的,时尚的网页,就无从下手. 这就需要经验的积累了. 那么,写一个模仿 ...

  3. js 写一个前端图片查看器

    1. 前言 网上已经有不少成熟的图片查看器插件,如果是单纯想要点击图片放大预览的话,可以直接使用插件.例如viewerjs 但是,当打开图片后还需要对图片进行一些像删除.下载.标记等业务层面上的操作, ...

  4. android仿空间照片查看器,PhotoViewer 一个简单仿微信朋友圈的图片查看器

    该图片查看器是模仿微信朋友圈查看图片编写 allprojects { repositories { ... maven { url 'https://jitpack.io' } } } lastRel ...

  5. Android简易图片管理器,一个简单仿微信朋友圈的图片查看器 PhotoViewer

    PhotoViewer 该图片查看器是模仿微信朋友圈查看图片编写 allprojects { repositories { ... maven { url 'https://jitpack.io' } ...

  6. 一个仿微信朋友圈的图片查看器,使用超级简单!

    PhotoViewer 项目地址:wanglu1209/PhotoViewer  简介:一个仿微信朋友圈的图片查看器,使用超级简单! 更多:作者   提 Bug 标签:        该图片查看器是模 ...

  7. C#制作一个图片查看器,具有滚轮放大缩小,鼠标拖动,图像像素化,显示颜色RGB信息功能

    目录 前言 一.界面设计 二.关键技术 1.把图片拖入到窗体并显示 2.实现图像缩放的功能 3.实现图像的移动效果 4.实时显示当前鼠标处的RGB值 5. 右击功能的实现 6.效果展示 总结 前言 使 ...

  8. java swing awt绘制一个图片查看器 图片显示 图片控件

    感谢 java图片查看器 的代码 java似乎没有一个名字叫图片控件的 控件,使用swing 的Label显示图片 他的代码如下: package swing.draw; import java.aw ...

  9. iOS一个模仿百度音乐盒的音乐播放器(带EQ均衡器)

    工作之余, 断断续续写了这么一个音乐播放器, eq实现各种音效, 指定位置播放, 快进快退, 锁屏耳机线控等等, 基本就是参考百度音乐盒的功能来实现的.(项目地址最后放出, 项目中的资源接口, 是抓百 ...

最新文章

  1. C/C++利用三元组实现稀疏矩阵运算
  2. 对象属性结构赋值_(六)面向对象-下
  3. ICLR 2020 | 可提速3000倍的全新信息匹配架构(附代码复现)
  4. 图文:关于进程与线程,我看过最通俗的解释!
  5. c语言统计数据,数据统计
  6. CarAppFocusManager
  7. 【再探backbone 02】集合-Collection
  8. 太原市初中计算机课程视频,初中全课程教学视频
  9. Bailian4150 上机【DP】
  10. 简明python教程-Python简明入门教程
  11. 【开学】下半年简单规划
  12. 大三暑假前端实习日志
  13. 计算机无法对光盘格式化,使用驱动器X:中的光盘之前需要将其格式化,是否需要将其格式化?...
  14. PAT 考试是什么?
  15. android模拟器 vm版,怎样用vmware虚拟机安装android模拟器
  16. python特殊字符替换
  17. Android开发 人民币符号(¥)显示不一致的问题
  18. css中英文单词换行的问题
  19. word删除空格、修复“断行”
  20. 使用AlexNet训练自己的数据集

热门文章

  1. mysql原理~undo
  2. 海翰聚焦:专家一天话,价值八千八?
  3. 计算机图形软件---图形功能
  4. 此为太阳历的技术支持博客
  5. SpringMVC之源码分析--ViewResolver(四)
  6. 腾讯技术工程 | 腾讯数据平台部总监刘煜宏:这5大产品平台,展示了腾讯大数据的核心能力...
  7. RHEL 5服务篇—部署DNS域名解析服务(一)BIND软件
  8. TF-IDF与余弦相似性的应用
  9. 与其他CA合作签发证书 谷歌赛门铁克之争接近尾声
  10. 删除排序链表中的重复元素