在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在我们用 JSX 建立组件系统之前,我们先来用一个例子学习一下组件的实现原理和逻辑。这里我们就用一个轮播图的组件作为例子进行学习。轮播图的英文叫做 Carousel,它有一个旋转木马的意思。 上一篇文章《使用 JSX 建立 Markup 组件风格》中我们实现的代码,其实还不能称为一个组件系统,顶多是可以充当 DOM 的一个简单封装,让我们有能力定制 DOM。 要做这个轮播图的组件,我们应该先从一个最简单的 DOM 操作入手。使用 DOM 操作把整个轮播图的功能先实现出来,然后在一步一步去考虑怎么把它设计成一个组件系统。
因为是轮播图,那我们当然需要用到图片,所以这里我准备了 4 张来源于 Unsplash 的开源图片,当然大家也可以换成自己的图片。首先我们把这 4 张图片都放入一个 let gallery = [ 'https://source.unsplash.com/Y8lCoTRgHPE/1142x640', 'https://source.unsplash.com/v7daTKlZzaw/1142x640', 'https://source.unsplash.com/DlkF4-dbCOU/1142x640', 'https://source.unsplash.com/8SQ6xjkxkCo/1142x640', ]; 而我们的目标就是让这 4 张图可以轮播起来。 组件底层封装首先我们需要给我们之前写的代码做一下封装,便于我们开始编写这个组件。
这样我们就封装好我们组件的底层框架的代码,代码示例如下: function createElement(type, attributes, ...children) { // 创建元素 let element; if (typeof type === 'string') { element = new ElementWrapper(type); } else { element = new type(); } // 挂上属性 for (let name in attributes) { element.setAttribute(name, attributes[name]); } // 挂上所有子元素 for (let child of children) { if (typeof child === 'string') child = new TextWrapper(child); element.appendChild(child); } // 最后我们的 element 就是一个节点 // 所以我们可以直接返回 return element; } export class Component { constructor() { } // 挂載元素的属性 setAttribute(name, attribute) { this.root.setAttribute(name, attribute); } // 挂載元素子元素 appendChild(child) { child.mountTo(this.root); } // 挂載当前元素 mountTo(parent) { parent.appendChild(this.root); } } class ElementWrapper extends Component { // 构造函数 // 创建 DOM 节点 constructor(type) { this.root = document.createElement(type); } } class TextWrapper extends Component { // 构造函数 // 创建 DOM 节点 constructor(content) { this.root = document.createTextNode(content); } } 实现 Carousel接下来我们就要继续改造我们的 继承了 Component后,我们就要从 这里我们就可以正式开始开发组件了,但是如果每次都需要手动 webpack 打包一下,就特别的麻烦。所以为了让我们可以更方便的调试代码,这里我们就一起来安装一下 webpack dev server 来解决这个问题。 执行一下代码,安装 npm install --save-dev webpack-dev-server webpack-cli 看到上面这个结果,就证明我们安装成功了。我们最好也配置一下我们 webpack 服务器的运行文件夹,这里我们就用我们打包出来的 设置这个我们需要打开我们的 module.exports = { entry: './main.js', mode: 'development', devServer: { contentBase: './dist', }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'createElement' }]], }, }, }, ], }, }; 用过 Vue 或者 React 的同学都知道,启动一个本地调试环境服务器,只需要执行 npm 命令就可以了。这里我们也设置一个快捷启动命令。打开我们的 { "name": "jsx-component", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack serve" }, "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.12.3", "@babel/plugin-transform-react-jsx": "^7.12.5", "@babel/preset-env": "^7.12.1", "babel-loader": "^8.1.0", "webpack": "^5.4.0", "webpack-cli": "^4.2.0", "webpack-dev-server": "^3.11.0" }, "dependencies": {} } 这样我们就可以直接执行下面这个命令启动我们的本地调试服务器啦! npm start 开启了这个之后,当我们修改任何文件时都会被监听到,这样就会实时给我们打包文件,非常方便我们调试。看到上图里面表示,我们的实时本地服务器地址就是 这里要注意的一个点,我们把运行的目录改为了 dist,因为我们之前的 main.html 是放在根目录的,这样我们就在 localhost:8080 上就找不到这个 HTML 文件了,所以我们需要把 main.html 移动到 dist 目录下,并且改一下 main.js 的引入路径。 <!-- main.html 代码 --> <body></body> <script src="./main.js"></script> 打开链接后我们发现 Carousel 组件已经被挂載成功了,这个证明我们的代码封装是没有问题的。 接下来我们继续来实现我们的轮播图功能,首先要把我们的图片数据传进去我们的 Carousel 组件里面。 let a = <Carousel src={gallery}/>; 这样我们的 所以我们需要另外储存这个 src 上的数据,后面使用它来生成我们轮播图的图片展示元素。在 React 里面是用 因为我们需要储存进来的属性到 然后这个 attributes 是需要我们另外存储到类属性中,而不是挂載到我们元素节点上。所以我们需要在组件类中重新定义我们的 我们需要在组件渲染之前能拿到 src 属性的值,所以我们需要把 render 的触发放在 class Carousel extends Component { // 构造函数 // 创建 DOM 节点 constructor() { super(); this.attributes = Object.create(null); } setAttribute(name, value) { this.attributes[name] = value; } render() { console.log(this.attributes); return document.createElement('div'); } mountTo() { parent.appendChild(this.render()); } } 接下来我们看看实际运行的结果,看看是不是能够获得图片的数据。 接下来我们就去把这些图给显示出来。这里我们需要改造一下 render 方法,在这里加入渲染图片的逻辑:
class Carousel extends Component { // 构造函数 // 创建 DOM 节点 constructor() { super(); this.attributes = Object.create(null); } setAttribute(name, value) { this.attributes[name] = value; } render() { this.root = document.createElement('div'); for (let picture of this.attributes.src) { let child = document.createElement('img'); child.src = picture; this.root.appendChild(child); } return this.root; } mountTo(parent) { parent.appendChild(this.render()); } } 就这样我们就可以看到我们的图片被正确的显示在我们的页面上。 排版与动画首先我们图片的元素都是 img 标签,但是使用这个标签的话,当我们点击并且拖动的时候它自带就是可以被拖拽的。当然这个也是可以解决的,但是为了更简单的解决这个问题,我们就把 img 换成 div,然后使用 background-image。 默认 div 是没有宽高的,所以我们需要在组件的 div 这一层加一个 class 叫 // main.js class Carousel extends Component { // 构造函数 // 创建 DOM 节点 constructor() { super(); this.attributes = Object.create(null); } setAttribute(name, value) { this.attributes[name] = value; } render() { this.root = document.createElement('div'); this.root.addClassList('carousel'); // 加入 carousel class for (let picture of this.attributes.src) { let child = document.createElement('div'); child.backgroundImage = `url('${picture}')`; this.root.appendChild(child); } return this.root; } mountTo(parent) { parent.appendChild(this.render()); } } <!-- main.html --> <head> <style> .carousel > div { width: 500px; height: 281px; background-size: contain; } </style> </head> <body></body> <script src="./main.js"></script> 这里我们的宽是 500px,但是如果我们设置一个高是 300px,我们会发现图片的底部出现了一个图片重复的现象。这是因为图片的比例是 所以通过比例计算,我们可以得出这样一个高度: 500 ÷ 1900 × 900 = 281. x x x 500\div1900\times900 = 281.xxx 500÷1900×900=281.xxx。所以 500px 宽对应比例的高大概就是 281px。这样我们的图片就可以正常的显示在一个 div 里面了。 一个轮播图显然不可能所有的图片都显示出来的,我们认知中的轮播图都是一张一张图片显示的。首先我们需要让图片外层的 carousel div 元素有一个和它们一样宽高的盒子,然后我们设置
然后我们又有一个问题,轮播图一般来说都是左右滑动的,很少见是上下滑动的,但是我们这里图片就是默认从上往下排布的。所以这里我们需要调整图片的布局,让它们拍成一行。 这里我们使用正常流就可以了,所以只需要给 div 加上一个 <head> <style> .carousel { width: 500px; height: 281px; white-space: nowrap; overflow: hidden; } .carousel > div { width: 500px; height: 281px; background-size: contain; display: inline-block; } </style> </head> <body></body> <script src="./main.js"></script> 接下来我们来实现自动轮播效果,在做这个之前我们先给这些图片元素加上一些动画属性。这里我们用
<head> <style> .carousel { width: 500px; height: 281px; white-space: nowrap; overflow: hidden; } .carousel > div { width: 500px; height: 281px; background-size: contain; display: inline-block; transition: ease 0.5s; } </style> </head> <body></body> <script src="./main.js"></script> 实现自动轮播有了动画效果属性,我们就可以在 JavaScript 中加入我们的定时器,让我们的图片在每三秒钟切换一次图片。我们使用 但是我们怎么才能让图片轮播,或者移动呢?想到 HTML 中的移动,大家有没有想到 CSS 当中有什么属性可以让我们移动元素的呢? 对没错,就是使用 但是这样只能挪动一张图,所以如果我们需要挪动第二次,到达第三张图,我们就要让每一张图偏移 200%,以此类推。所以我们需要一个当前页数的值,叫做 class Carousel extends Component { // 构造函数 // 创建 DOM 节点 constructor() { super(); this.attributes = Object.create(null); } setAttribute(name, value) { this.attributes[name] = value; } render() { this.root = document.createElement('div'); this.root.classList.add('carousel'); for (let picture of this.attributes.src) { let child = document.createElement('div'); child.style.backgroundImage = `url('${picture}')`; this.root.appendChild(child); } let current = 0; setInterval(() => { let children = this.root.children; ++current; for (let child of children) { child.style.transform = `translateX(-${100 * current}%)`; } }, 3000); return this.root; } mountTo(parent) { parent.appendChild(this.render()); } } 这里我们发现一个问题,这个轮播是不会停止的,一直往左偏移没有停止。而我们需要轮播到最后一张的时候是回到一张图的。 要解决这个问题,我们可以利用一个数学的技巧,如果我们想要一个数是在 1 到 N 之间不断循环,我们就让它对 n 取余就可以了。在我们元素中,children 的长度是 4,所以当我们 current 到达 4 的时候, 4 ÷ 4 4\div4 4÷4 的余数就是 0,所以每次把 current 设置成 current 除以 children 长度的余数就可以达到无限循环了。
用这个逻辑来实现我们的轮播,确实能让我们的图片无限循环,但是如果我们运行一下看看的话,我们又会发现另外一个问题。当我们播放到最后一个图片之后,就会快速滑动到第一个张图片,我们会看到一个快速回退的效果。这个确实不是那么好,我们想要的效果是,到达最后一张图之后,第一张图就直接在后面接上。 那么我们就一起去尝试解决这个问题,经过观察其实在屏幕上一次最多就只能看到两张图片。那么其实我们就把这两张图片挪到正确的位置就可以了。 所以我们需要找到当前看到的图片,还有下一张图片,然后每次移动到下一张图片就找到再下一张图片,把下一张图片挪动到正确的位置。 讲到这里可能还是有点懵,但是不要紧,我们来整理一下逻辑。 获取当前图片 index 和 下一张图的 index
计算图片移动的距离,保持当前图片后面有一张图片等着被挪动过来
第二张图就位,就可以开始执行轮播效果
接下来我们把上面的逻辑翻译成 JavaScript: class Carousel extends Component { // 构造函数 // 创建 DOM 节点 constructor() { super(); this.attributes = Object.create(null); } setAttribute(name, value) { this.attributes[name] = value; } render() { this.root = document.createElement('div'); this.root.classList.add('carousel'); for (let picture of this.attributes.src) { let child = document.createElement('div'); child.style.backgroundImage = `url('${picture}')`; this.root.appendChild(child); } // 当前图片的 index let currentIndex = 0; setInterval(() => { let children = this.root.children; // 下一张图片的 index let nextIndex = (currentIndex + 1) % children.length; // 当前图片的节点 let current = children[currentIndex]; // 下一张图片的节点 let next = children[nextIndex]; // 禁用图片的动效 next.style.transition = 'none'; // 移动下一张图片到正确的位置 next.style.transform = `translateX(${-100 * (nextIndex - 1)}%)`; // 执行轮播效果,延迟了一帧的时间 16 毫秒 setTimeout(() => { // 启用 CSS 中的动效 next.style.transition = ''; // 先移动当前图片离开当前位置 current.style.transform = `translateX(${-100 * (currentIndex + 1)}%)`; // 移动下一张图片到当前显示的位置 next.style.transform = `translateX(${-100 * nextIndex}%)`; // 最后更新当前位置的 index currentIndex = nextIndex; }, 16); }, 3000); return this.root; } mountTo(parent) { parent.appendChild(this.render()); } } 如果我们先去掉 实现拖拽轮播一般来说我们的轮播组件除了这种自动轮播的功能之外,还有可以使用我们的鼠标进行拖动来轮播。所以接下来我们一起来实现这个手动轮播功能。 因为自动轮播和手动轮播是有一定的冲突的,所以我们需要把我们前面实现的自动轮播的代码给注释掉。然后我们就可以使用这个轮播组件下的 children (子元素),也就是所有图片的元素,来实现我们的手动拖拽轮播功能。 那么拖拽的功能主要就是涉及我们的图片被拖动,所以我们需要给图片加入鼠标的监听事件。如果我们根据操作步骤来想的话,就可以整理出这么一套逻辑: 我们肯定是需要先把鼠标移动到图片之上,然后点击图片。所以我们第一个需要监听的事件必然就是 this.root.addEventListener('mousedown', event => { console.log('mousedown'); }); this.root.addEventListener('mousemove', event => { console.log('mousemove'); }); this.root.addEventListener('mouseup', event => { console.log('mouseup'); }); 执行一下以上代码后,我们就会在 console 中看到,当我们鼠标放到图片上并且移动时,我们会不断的触发 所以我们需要把 mousemove 和 mouseup 两个事件,放在 mousedown 事件的回调函数当中,这样才能正确的在鼠标按住的时候监听移动和松开两个动作。这里还需要考虑,当我们 mouseup 的时候,我们需要把 mousemove 和 mouseup 两个监听事件给停掉,所以我们需要用函数把它们单独的存起来。 this.root.addEventListener('mousedown', event => { console.log('mousedown'); let move = event => { console.log('mousemove'); }; let up = event => { this.root.removeEventListener('mousemove', move); this.root.removeEventListener('mouseup', up); }; this.root.addEventListener('mousemove', move); this.root.addEventListener('mouseup', up); }); 这里我们在 mouseup 的时候就把 mousemove 和 mouseup 的事件给移除了。这个就是一般我们在做拖拽的时候都会用到的基础代码。 但是我们又会发现另外一个问题,鼠标点击拖动然后松开后,我们鼠标再次在图片上移动,还是会出发到我们的mousemove 事件。 这个是因为我们的 mousemove 是在 所以我们可以在 this.root.addEventListener('mousedown', event => { console.log('mousedown'); let move = event => { console.log('mousemove'); }; let up = event => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); 有了这个完整的监听机制之后,我们就可以尝试在 mousemove 里面去实现轮播图的移动功能了。我们一起来整理一下这个功能的逻辑: 要做这个功能,首先我们要知道鼠标的位置,这里可以使用 mousemove 中的 this.root.addEventListener('mousedown', event => { let children = this.root.children; let startX = event.clientX; let move = event => { let x = event.clientX - startX; for (let child of children) { child.style.transition = 'none'; child.style.transform = `translateX(${x}px)`; } }; let up = event => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); 好,到了这里我们发现了两个问题: 我们第一次点击然后拖动的时候图片的起始位置是对的,但是我们再点击的时候图片的位置就不对了。我们拖动了图片之后,当我们松开鼠标按钮,这个图片就会停留在拖动结束的位置了,但是在正常的轮播图组件中,我们如果拖动了图片超过一定的位置,就会自动轮播到下一张图的。 要解决这两个问题,我们可以这么计算,因为我们做的是一个轮播图的组件,按照现在一般的轮播组件来说,当我们把图片拖动在大于半个图的位置时,就会轮播到下一张图了,如果不到一半的位置的话就会回到当前拖动的图的位置。 按照这样的一个需求,我们就需要记录一个 首先当我们 mousemove 的时候,我们需要计算当前图片已经从起点移动了多远,这个就可以通过 N * 500 来计算,这里的 N 就是目前的图片的 this.root.addEventListener('mousedown', event => { let children = this.root.children; let startX = event.clientX; let move = event => { let x = event.clientX - startX; for (let child of children) { child.style.transition = 'none'; child.style.transform = `translateX(${x - current * 500}px)`; } }; let up = event => { let x = event.clientX - startX; current = current - Math.round(x / 500); for (let child of children) { child.style.transition = ''; child.style.transform = `translateX(${-current * 500}px)`; } document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); });
做到这里,我们就可以用拖拽来轮播我们的图片了,但是当我们拖到最后一张图的时候,我们就会发现最后一张图之后就是空白了,第一张图没有接着最后一张。 那么接下来我们就去完善这个功能。这里其实和我们的自动轮播是非常相似的,在做自动轮播的时候我们就知道,每次轮播图片的时候,我们最多就只能看到两张图片,可以看到三张图片的机率是非常小的,因为我们的轮播的宽度相对我们的页面来说是非常小的,除非用户有足够的位置去拖到第二张图以外才会出现这个问题。但是这里我们就不考虑这种因素了。 我们确定每次拖拽的时候只会看到两张图片,所以我们也可以像自动轮播那样去处理拖拽的轮播。但是这里有一个点是不一样的,我们自动轮播的时候,图片只会走一个方向,要么左要么右边。但是我们手动就可以往左或者往右拖动,图片是可以走任意方向的。所以我们就无法直接用自动轮播的代码来实现这个功能了。我们就需要自己重新处理一下轮播头和尾无限循环的逻辑。 我们可以从 mousemove 的回调函数开始改造需要找到当前元素在屏幕上的位置,我们给它 一个变量名叫
通过这个公式我们就可以取得上一张和下一张图片在数组里面的指针位置,这个时候我们就可以用这个指针获取到他们在节点中的对象,使用 CSSDOM 来改变他们的属性这里我们需要先把所有元素移动到当前图片的位置,然后根据 -1、0、1 这三个偏移的值对这个图片进行往左或者往右移动,最后我们要需要加上当前鼠标的拖动距离 我们已经把整个逻辑给整理了一遍,下来我们看看 mousemove 这个事件回调函数代码的应该怎么写: let move = event => { let x = event.clientX - startX; let current = position - Math.round(x / 500); for (let offset of [-1, 0, 1]) { let pos = current + offset; // 计算图片所在 index pos = (pos + children.length) % children.length; console.log('pos', pos); children[pos].style.transition = 'none'; children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)`; } };
最后还有一个小问题,在我们拖拽的时候,我们会发现上一张图和下一张有一个奇怪跳动的现象。 这个问题是我们的 我们只需要把这里的 这里其实还有比较多的问题的,我们还没有去改 mouseup 事件里面的逻辑。那么接下来我们就来看看 up 中的逻辑我们应该怎么去实现。 这里我们需要改的就是 children 中 for 循环的代码,我们要实现的是让我们拖动图片超过一定的位置就会自动轮播到对应方向的下一张图片。up 这里的逻辑其实是和 move 是基本一样的,不过这里有几个地方需要更改的: 首先我们的 transition 禁止是可以去掉了,改为 这个是因为我们的 Match.round() 的特性,在 250(500px 刚好一半的位置) 之间是有一定的误区,让我们无法判断图片需要往那个方向移动的,所以在计算往 Match.round 的值之后我们还需要加上 最终我们的代码就是这样的: let up = event => { let x = event.clientX - startX; position = position - Math.round(x / 500); for (let offset of [0, -Math.sign(Math.round(x / 500) - x + 250 * Math.sign(x))]) { let pos = position + offset; // 计算图片所在 index pos = (pos + children.length) % children.length; children[pos].style.transition = ''; children[pos].style.transform = `translateX(${-pos * 500 + offset * 500}px)`; } document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; 改好了 up 函数之后,我们就真正完成了这个手动轮播的组件了。 到此这篇关于使用JSX实现Carousel轮播组件的方法(前端组件化)的文章就介绍到这了,更多相关JSX实现Carousel轮播组件内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论