SegmentFault eveningwater最新的文章
2023-12-28T15:36:05+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
2023我的年终总结-很普通的一年
https://segmentfault.com/a/1190000044512286
2023-12-28T15:36:05+08:00
2023-12-28T15:36:05+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
2
<blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044433522">SegmentFault 思否 2023 年度有奖征文</a>活动,欢迎正在阅读的你也加入。</blockquote><h2>2023 年终总结</h2><p>又是这样匆匆忙忙的度过了一年,今年付出了很多,也收获了不少,接下来,我想盘点一下我今年所做的付出与收获。</p><h3>收集曾经的作品</h3><p>我收集了我曾经写的诗词,小说等作品,虽然写的不怎么样,但也是值得看看,留作纪念,网站地址<a href="https://link.segmentfault.com/?enc=XWpmtFnK7HvyTr4rO9h4Yg%3D%3D.dGurOCvMDHvJBR95fj5uCUU1fTb0WHeK2op3lvykkFRunuNjz8B%2Fsm6SYNBSJjP7" rel="nofollow">我的个人诗词</a>,国内可以访问<a href="https://link.segmentfault.com/?enc=OBxwN2bxlRMM7t9r9h8biA%3D%3D.IvoEWWyuYeufBCWujXEJXb0JlC%2Fx02WWkVBCanECPwhabh%2B5Q893YDUI2qmNxrXp" rel="nofollow">这个地址</a>。</p><p>如下图所示:</p><p><img src="/img/remote/1460000044512289" alt="poem-website.png" title="poem-website.png"></p><p>虽然是一个简单的网站,但其实也收获了不少知识点,总结如下:</p><ol><li>gh-pages 依赖可以帮助我们将打包后的代码一键部署到 github 的服务器上。</li><li>dumi 框架的使用。</li></ol><h3>写了一个收集资源网站</h3><p>这是一个自己使用 php+react 技术栈写的资源收集网站,当然还有很多功能未开发完成,只是完成了一个初版,网站地址<a href="https://link.segmentfault.com/?enc=PbNLOwWYtyMb2Q%2BbSx4IdA%3D%3D.itBjaP1qNxP2Q242EWKSRo7gjGD3gFiW%2FfkBtQm6rkv51KMHEINjyliAFHEmjA%2BP" rel="nofollow">资源网站</a>。如下图所示:</p><p><img src="/img/remote/1460000044512290" alt="resource-website.png" title="resource-website.png"></p><p>之所以要开发这个网站,起因在于,我看到很多推荐一些资源的网站地址,我就觉得应该写一个网站用于收藏这些资源网站地址,一个资源网站地址包含如下:</p><ol><li>网站名</li><li>网站分类</li><li>网站 url 地址</li><li>封面图</li><li>网站描述信息</li></ol><p>而我开发也正是按照这个来开发的,如下图所示:</p><p><img src="/img/remote/1460000044512291" alt="resource-website-1.png" title="resource-website-1.png"></p><blockquote>ps: 也希望诸位大佬可以分享你们收藏的一些资源网站。</blockquote><p>当然目前基本功能虽然完成了,但是还是有一些问题的,比如:</p><ol><li>封面图是必须要上传的。</li><li>图片上传之后是不能再次上传同名图片的,需要使用其它图片名替换。</li><li>如果翻页了,再搜索会有问题(这个应该是前端的问题,需要处理)。</li></ol><p>目前这个网站完成的功能有:</p><ol><li>图片上传</li><li>分页</li><li>局部搜索与全局搜索</li><li>资源网站数据的添加与修改(不支持删除)</li><li>图片列表的展示</li></ol><p>待完成的功能有很多,这里可以列一下我认为需要扩展的功能:</p><ol><li>新增分类的添加,删除,修改。</li><li>新增用户的添加,删除,修改以及权限。</li><li>图片支持不是必须上传,也支持删除封面图。</li></ol><p>这个网站是我在课余时间研发的,慢慢来。</p><h3>ts 代码段</h3><p>阅读了<a href="https://link.segmentfault.com/?enc=b10FRBa4Y7ETlTXJ330WYQ%3D%3D.xx6IEkpBqRR7awS1J%2FsZkNZub6qYH1HLANBAcM4pPCJscgg79g9BdwwfX8rylbtE9bBiAfU3lxntZXovPOSDvg%3D%3D" rel="nofollow">type-challenges</a>里的类型体操,然后动手实践,并总结到了我的个人<a href="https://link.segmentfault.com/?enc=2MnobtqvI1QLxdglez588w%3D%3D.NlM0gqSjdSYZc%2FvV0g4OovZe%2FKPnypn4omYAZJktwaoRNf1qXlbfOLofRGW1%2Fkns" rel="nofollow">代码段网站</a>中。如下图所示:</p><p><img src="/img/remote/1460000044512292" alt="code-segment-ts.png" title="code-segment-ts.png"></p><p>自己亲自写,也学到了很多 ts 的知识,比如条件类型,infer,keyof 等关键字,ts 中的递归,ts 的类型编程就好像 js 的值编程一样,比如实现数组的 slice 方法,这里应该叫截取数组元素,代码如下所示:</p><pre><code class="ts">// 第一步
type ToPositive<
N extends number,
Arr extends unknown[]
> = `${N}` extends `-${infer P extends number}` ? Slice<Arr, P>['length'] : N;
// 第二步
type InitialN<
Arr extends unknown[],
N extends number,
_Acc extends unknown[] = []
> = _Acc['length'] extends N | Arr['length']
? _Acc
: InitialN<Arr, N, [..._Acc, Arr[_Acc['length']]]>;
// 第三步
type Slice<
Arr extends unknown[],
Start extends number = 0,
End extends number = Arr['length']
> = InitialN<Arr, ToPositive<End, Arr>> extends [
...InitialN<Arr, ToPositive<Start, Arr>>,
...infer R
]
? R
: [];</code></pre><p>使用示例代码如下:</p><pre><code class="ts">type Arr = [1, 2, 3, 4, 5];
// basic
type SliceRes1 = Slice<Arr, 0, 1>; // [1]
type SliceRes2 = Slice<Arr, 0, 0>; // []
type SliceRes3 = Slice<Arr, 2, 4>; // [3, 4]
// optional args
type SliceRes4 = Slice<[]>; // []
type SliceRes5 = Slice<Arr>; // Arr
type SliceRes6 = Slice<Arr, 0>; // Arr
type SliceRes7 = Slice<Arr, 2>; // [3, 4, 5]
// negative index
type SliceRes8 = Slice<Arr, 0, -1>; // [1, 2, 3, 4]
type SliceRes9 = Slice<Arr, -3, -1>; // [3, 4]
// invalid
type SliceRes10 = Slice<Arr, 10>; // []
type SliceRes11 = Slice<Arr, 1, 0>; // []
type SliceRes12 = Slice<Arr, 10, 20>; // []</code></pre><p>我把以上的三步分成,第一步要将负数索引值转成正整数索引值,也就是 ToPositive 实现的含义,第二步就是根据正整数索引,截取数组中每一个元素到一个新数组中,这也就是 InitialN 类型的含义,最后再根据前面两者的实现即可推导出 Slice 的实现。</p><p>具体的详细解释,可以前往<a href="https://link.segmentfault.com/?enc=KOtKh4QKKQovw%2B53vzLJ8A%3D%3D.2NnR7GD2hk7IVmbdx%2BblzzCVVAlB17GsjwTu8LfKZn3rnsZh6l6zJKaamsxwl%2FgiKWsKsMtUPOc0dXWJsW8k%2BOVEBsm1rkY1v6CE70wbjSs%3D" rel="nofollow">这个地址</a>查看。</p><p>当然,js 代码段我也修改了一下,添加了很多示例,这里也不必多说。</p><h3>写了一个文档网站</h3><p>然后就是自己写了一个文档网站,网站地址<a href="https://link.segmentfault.com/?enc=rkrdusQOxegbfv2cZLOsWw%3D%3D.bb1Xqm8QFj9zfsHXSCijhpFBhENgJAI4Jd3Y9uXNFRUPQHuSj9hGZTM5zM%2FbKKAhA287l9UvjbS%2FE0q8bzA%2BeA%3D%3D" rel="nofollow">文档网站</a>。如下图所示:</p><p><img src="/img/remote/1460000044512293" alt="doc-website.png" title="doc-website.png"></p><p>并且仔细阅读了 vue.js 设计与实现,js 红宝书,css 世界,然后抄录在文档网站上,方便自己查看阅读,当然抄录仅供我自己参考学习,无任何商用传播的含义。</p><p>然后目前正在编写这个文档网站的实现文档,写完后再放出来。这个文档网站只使用 ts 实现,实现了很多插件,比如:</p><ol><li>弹出框插件</li><li>预览图插件</li><li>响应式图片插件</li><li>下拉框插件</li><li>抽屉插件</li><li>警告展示框插件</li><li>消息提示框插件</li></ol><p>对于消息提示框插件,我还单独写了一个仓库,地址<a href="https://link.segmentfault.com/?enc=JJZtDur2wA4QJgsu7G4IEw%3D%3D.phQObbeVk%2BnyEJMrJCPTt1dNZLeTEKvzqf412tJ%2BnBMcS%2Fy7y6173tVGdeDm%2F5A%2F" rel="nofollow">ew-message</a>。配套也有使用文档<a href="https://link.segmentfault.com/?enc=P8fva1Y1oP8szP%2BNeyEhug%3D%3D.ahfPTrOZC6DLgYOaZvsEhSlKpqmviNKesMl9qjhuoQ8LXZA4E1PVhlv1a9RV%2BUng" rel="nofollow">官网</a>。</p><p>对源码感兴趣的可以看看。</p><h3>文章方面</h3><p>今年写的文章也不多,一共 15 篇,分别如下:</p><ul><li><a href="https://link.segmentfault.com/?enc=hRPEZ33qjM42mfEuUMvwxw%3D%3D.uR2%2FdEHK4ZylZuQClo6kCuFJ%2BMrAGi%2Fj5OPmyleMSd7jW6rOYZxr0ZtZP8LmzgvK" rel="nofollow">国产工具好强大-一个可以允许小程序运行在任意 APP 的容器技术</a></li><li><a href="https://link.segmentfault.com/?enc=PWL9ldw8Tlm8YHV8pvGSJQ%3D%3D.%2FerIrq7QWO%2BNPeEjRNLJr3MaSP89dXDK7SaEHzSGYoGg0VEoyR77AhYQQmpQmCkY" rel="nofollow">作兔器——手写一个可爱的兔兔相册展示器</a></li><li><a href="https://link.segmentfault.com/?enc=ZMDQk%2Fj1clBBtGixpmV05g%3D%3D.8R7oXK6dEFfLVkTQM762j1lIgRzM6rgRtEm4%2FovmhJP7sDAMai7CldeYennknLwl" rel="nofollow">抖音两个旋转小球的 loading 实现</a></li><li><a href="https://link.segmentfault.com/?enc=blvfrICAeHSftJeP0jooZw%3D%3D.e21SCWRQYMQqaBLv%2FQdHjy9JZXcF7A0HjRZU4GPbKJT4wzpaPWxrondMx2q%2BtPtA" rel="nofollow">原来 typescript 也可以这么有趣</a></li><li><a href="https://link.segmentfault.com/?enc=gMhLPDbnePP88EyaQRRm4A%3D%3D.%2BdfdqlbSkX%2FQq4CDCs2kOh0c00pIvUkE3fwhLy%2FWR1xhmL5MuuHMC4EW45uxPD8v" rel="nofollow">10 个 效果不错的值得收藏的 css 代码片段</a></li><li><a href="https://link.segmentfault.com/?enc=04KVuIHUe%2Fe%2FZf8KBW5l3Q%3D%3D.%2B2I9%2FEWEI95GsnRamJJzrvchDZLmD0aQQtjmOvMe6sljJ9HgWo9dxV1%2BsCOjNGVg" rel="nofollow">强大的 css 计数器,你确定不来看看?</a></li><li><a href="https://link.segmentfault.com/?enc=8H7ieHXOdLxsdAenEcRpgg%3D%3D.4%2F6y43cqNnFW3cLnyVbi2PWM1x0uVcnaJQJURxNiopKx8aHDwbDYnJ1DazSseb6P" rel="nofollow">奇怪,奇怪,真的好奇怪的 javascript</a></li><li><a href="https://link.segmentfault.com/?enc=rCGCpJMnK%2F3XLvGqxVxp6A%3D%3D.Nm8P8cTGV7OyyipB0rEaASRvISxZ0Kpv77rSPzEmJ%2BJelRvWvhoXtZ%2FBfMU6XWUK" rel="nofollow">ew-message 一个基于 typescript 封装的消息提示框插件</a></li><li><a href="https://link.segmentfault.com/?enc=pdl867SQRFxgYkQ9g3W19g%3D%3D.8oxv%2Fk3F1RiCoaY3TujU2jHU4rLn1cDFlGXfSKDQDvfPJRJQ4pawVPo01UBVSaw4" rel="nofollow">难以置信,一个小小的需求让我捣鼓出一个提效的网站来</a></li><li><a href="https://link.segmentfault.com/?enc=wCKXTYPSm75IGvLhhk1Obw%3D%3D.IdVfAFtD6A3hgjounr%2Bt5ylV7qQi%2BgGnkgd5GlrqtIpBD6azyzk1GKB1u7hEioy5" rel="nofollow">有关安卓软键盘遮挡输入框的问题分析及处理方案</a></li><li><a href="https://link.segmentfault.com/?enc=AkPLI46JfrLUr%2Bzm8Qqaeg%3D%3D.8GNXL9j7IBVf1Iu70ilUlhoOlV2unDKQPkOQ5pNqwkTC%2FqcbdPmLUW3TtDCqr2%2Fu" rel="nofollow">隐藏元素 display:none 与 visibility:hidden 的区别你真的知道吗?</a></li><li><a href="https://link.segmentfault.com/?enc=WRA2XQdEedVj0yd98fAp9g%3D%3D.DDMUxQcjOOijz7Cc5o2ayKfESrUyzgF3UvNAmUdLhstYVILT8U8YBlBfVy4pOHUB" rel="nofollow">这么有趣的 ts 类型,你确定不来看看?</a></li><li><a href="https://link.segmentfault.com/?enc=9GNEK7LjTI0r%2FpxBw06YCA%3D%3D.FV9MOqQNsrKyYUzWN6bmtFzjTJBp2aA0U9CylrjiXwHmraw9%2FnIv0mbYBGUWH9NV" rel="nofollow">进来看看,剖析正则表达式数组去重原理,没用你找我</a></li><li><a href="https://link.segmentfault.com/?enc=Hha48cocqHzWr7EDusxxDQ%3D%3D.TiU6CymWmet%2FBJqNWvHtzebBDNf4yGuKHnbgYjhjxZLA9YbXgxgfvtm4Oca3l4Uj" rel="nofollow">20 个值得收藏点赞的 ts 代码段</a></li><li><a href="https://link.segmentfault.com/?enc=LCiHJ4zwDSBU08cCzWYMDQ%3D%3D.gw0wxCgTkrSTz1PB2hFvImhzEc2v1vrPhEtcjYoLGeZZKi486h5BqVMFqmt4WkaX" rel="nofollow">一个小小的备忘录,让我熟悉了 vue3 全家桶</a></li></ul><blockquote>ps: 可能有些文章有点水吧,见谅见谅。</blockquote><p>个人觉得值得一看的就这几篇文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=ZFL6TBUZEA1vV5ogICx7LA%3D%3D.Toe2545ZBf6Uxb5Hx2zC4R3BRP4AsHBBmu9S4NSFj9tbrcIovizt1PsmbEqVTmk1" rel="nofollow">作兔器——手写一个可爱的兔兔相册展示器</a></li><li><a href="https://link.segmentfault.com/?enc=7lz2iZrJbr6Njmm1ORD3gw%3D%3D.49r68sKZnAdjHjezwTIINhYlft04%2BJaK9SvqkRAcRA1GqM1cdiL6ezclW0pHGAr7" rel="nofollow">抖音两个旋转小球的 loading 实现</a></li><li><a href="https://link.segmentfault.com/?enc=PfgRNbrA2yiJFGrtxlpoPA%3D%3D.IxKcsWJ7sZ5PyUUd66tJAPCew7FsFXIRoYDEuWMIQvn%2BIYUms%2BUdg2h%2BkBAw9xbS" rel="nofollow">20 个值得收藏点赞的 ts 代码段</a></li><li><a href="https://link.segmentfault.com/?enc=ROJRVOVtvQyUYXBOVC8EEg%3D%3D.VrddytwOr7i77Hj2LtEgLX3RGHvXS7fM8Mk%2BUAokvA5GwdzTVGMYLYbi3r9udhXJ" rel="nofollow">一个小小的备忘录,让我熟悉了 vue3 全家桶</a></li><li><a href="https://link.segmentfault.com/?enc=kyhCPZ9TrONDbxYmk5auyw%3D%3D.N5MvoRULKJhp%2FBmRimSGpYVR7xpI8DKlGyZ4TBvyP5r0S8MM0uW7AjCXxKyPJuAY" rel="nofollow">难以置信,一个小小的需求让我捣鼓出一个提效的网站来</a></li><li><a href="https://link.segmentfault.com/?enc=jdfhSkTo47m8gQRgJWGvzw%3D%3D.eyGviScny5ExxSScScr8vxBsPerNmvoKlNpe9AxZa18Pv7q6LlebDNEmg8z1GClz" rel="nofollow">10 个 效果不错的值得收藏的 css 代码片段</a></li></ul><p>这几篇文章点赞量也不错,我也很满意,没什么可说的。</p><h3>工作与生活</h3><p>工作方面吧,马马虎虎,可能算是得过且过吧,其实工作方面也让我学到了不少,我觉得我今年相比过去,应该是变成熟了不少,生活方面有了一笔小小的存款,也算是对自己的告诫吧。希望下一年再接再厉,努力积极向上。</p><h3>未来展望</h3><p>我还是希望明年自己能够坚持将自己的文档网站实现文档给写完,然后希望能完成一本技术书《ts 项目实战》。</p><h3>最后</h3><p>最后感谢大家阅读,如果本文有什么不好的地方,感谢批评指正,愿我们都能积极努力,实现自己的梦想。</p>
这么有趣的ts类型,你确定不来看看?
https://segmentfault.com/a/1190000044387852
2023-11-14T16:07:58+08:00
2023-11-14T16:07:58+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
6
<blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044350890">SegmentFault 思否写作挑战赛</a>活动,欢迎正在阅读的你也加入。</blockquote><h2>前言</h2><p>事情是这样的,有这样一道 ts 类型题,代码如下所示:</p><pre><code class="ts">type Union = "Mon" | "Tue" | "Wed";
// 补充这里的类型代码
type Mapping<T extends Union, Value extends string> = any;
type Res = Mapping<Union, "周一" | "周二" | "周三">;
// 以下是输出结果
// {
// mon: "周一";
// Tue: "周二";
// Wed: "周三";
// }</code></pre><p>观察题目,其实就是将两个联合类型的值组合成接口,其中第一个联合类型的值作为属性,第二个联合类型的值则作为属性值,并且两者的属性顺序是一一对应的。下面跟着我一起来分析,通过这道题,我们能理解到 ts 的不少知识点,不信继续往下看。</p><h2>分析</h2><p>实际上,在 ts 当中,想要保证 ts 的顺序是很困难的,这与 ts 编译器有关,不过这不影响我们对这道题的分析,那么这道题如何解决呢?思路就是想办法将 2 个联合类型构造成数组,然后就可以根据数组项一一对应来转成对象了,那么这道题的难点在于如何转成数组。</p><p>转成数组的前提就是我们将联合类型的每一项取出来然后添加到数组中,那么如何提取呢?下面让我们一步一步来实现。</p><h2>将并集转成交集</h2><p>联合类型我们也可以叫做并集,如: <code>1 | 2 | 3</code>,而要实现添加的第一步,我们需要将并集转成交集,那么如何进行交集的转换呢?</p><p>其实我们可以将并集的每一项使用函数来推断,在这里我们需要理解 ts 中的 2 个关键字用法,如下:</p><ol><li>extends: 既可以表示类的继承,也可以表示条件判断(相当于 js 的全等)。</li><li>infer: 该关键字用于推导某个类型。</li></ol><p>根据以上分析,我们就可以实现并集转成交集,代码如下:</p><pre><code class="ts">// X | Y | Z ==> X & Y & Z
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;</code></pre><p>以上类型就实现了并集对交集的转换,理解起来也很容易,就是判断给定的泛型参数是否是任意类型 any,如果是则构造成函数参数为该类型,然后使用 infer 关键字去推导参数类型,如果能推导出来,则返回推导出来的结果,否则返回 never。</p><h3>any 与 never 与 void 类型</h3><p>这个 ts 类型也涉及到了 3 个类型,即任意类型 any,从不类型 never,和 void 类型。</p><h4>any 类型</h4><p>其中 any 用来表示允许赋值为任意类型。在 ts 中,如果是一个普通类型,在赋值过程中改变类型是不被允许的。例如:</p><pre><code class="ts">let a: string = "123";
a = 2; // error TS2322: Type 'number' is not assignable to type 'string'</code></pre><p>以上定义 a 变量的类型是 string,因此修改变量值为数值,则 ts 编译会出错,但如果是赋值为任意值类型,则以上操作不会报错,如下所示:</p><pre><code class="ts">let a: any = "123";
a = 2; // 允许修改,因为是任意值类型</code></pre><p>我们也可以访问任意类型的属性和方法,如下所示:</p><pre><code class="ts">let b: any = "b";
console.log(b.name); // ts编译不会报错
console.log(b.setName("a")); // ts编译不会出错</code></pre><p>也就是说,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。</p><p>在 ts 中,一个未声明类型的变量,也会被推导成任意类型,如:</p><pre><code class="ts">let a;
a = "a";
a = 2;
a.setName("b");
// 以上操作在ts中都不会报错</code></pre><h4>never 类型</h4><p>never 类型表示从不存在的类型,比如一个函数抛出异常,它的返回类型就是 never,如:</p><pre><code class="ts">const fn = (msg: string) => throw new Error(msg); // never</code></pre><h4>void 类型</h4><p>void 类型表示没有返回值,通常用在没有任何返回值的函数中。如:</p><pre><code class="ts">const fn = (): void => {
alert(123);
};</code></pre><p>以上类型是包装成函数类型推导,对于函数有没有返回值没有任何意义,因此这里只需要使用 void 来代表返回值即可。</p><h2>将联合类型转换成重载函数类型</h2><p>下一步,我们就需要将联合类型转换成重载函数类型,例如:</p><pre><code class="ts">X | Y ==> ((x: X)=>void) & ((y:Y)=>void)</code></pre><p>我们要如何实现呢?其实就是将泛型参数包装成函数类型,然后再调用用前面的并集转交集类型,如下:</p><pre><code class="ts">type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;</code></pre><p>做这一步的目的是方便将联合类型中的每一项提取出来,因此需要这个类型,接下来我们就需要将联合类型的每一项取出来,我们叫做 PopUnion 类型。</p><h2>从联合类型中取出每一个类型</h2><p>有了前面 2 个类型的铺垫,取出联合类型中的每一个类型就很容易,我们只需要包装成重载函数类型,然后使用 infer 推断函数参数类型,返回参数类型即可。代码如下:</p><pre><code class="ts">type PopUnion<U> = UnionToOvlds<U> extends (f: infer A) => void ? A : never;</code></pre><p>能够取出联合类型的每一个类型,那么构造成数组就很容易了,不过接下来还需要一个类型,那就是判断是否是联合类型,为此我们需要先实现这个类型,即 IsUnion 类型。</p><h2>判断是否是联合类型</h2><p>判断是否是联合类型比较简单,就是将类型构造成一个数组,然后使用两个数组比较,不过我们需要比较的是原始泛型参数构造成数组和转成交集构造成数组是否相等,相等则返回 false,否则返回 true。代码如下:</p><pre><code class="ts">type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;</code></pre><h2>联合类型转数组</h2><p>接下来就是联合类型转数组类型的实现,首先我们需要用到上一节提到的判断是否是联合类型,如果是联合类型,则使用 PopUnion 类型提取联合类型的每一项,注意这里是需要递归的提取剩余项的,直到不是剩余项不是联合类型为止,因此这里我们没提取一项,都需要使用 Exclude 类型将联合类型中提取的排除掉,这样就得到了剩余的联合类型,然后我们使用第二个参数来存储结果,如果不是联合类型就直接添加到数组中。代码如下所示:</p><pre><code class="ts">type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];</code></pre><p>以上代码我们使用泛型 A 是一个未知类型的数组,并默认赋值为空数组来存储结果,相当于我们是从联合类型当中一项一项的提取出来然后添加到 A 结果数组中,最终返回的结果就是一个由联合类型每一项组成的数组。</p><h3>Exclude 类型</h3><p>其中 Exclude 类型是 ts 内置类型,不过要实现还是比较简单的,简单来说就是如果两个参数相等,则不返回类型,否则返回原类型。代码如下:</p><pre><code class="ts">type Exclude<T, U> = T extends U ? never : T;</code></pre><h2>获取数组的长度</h2><p>接下来我们还需要比较两个联合类型提取出来的数组长度是否相同,为此我们需要先实现如何获取一个数组类型的长度,观察发现数组是存在一个 length 属性的,因此我们可以判断如果存在 length 属性,并使用 infer 推断具体值,能够推断出来就返回这个推断的值,否则返回 never,代码如下:</p><pre><code class="ts">type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? L
: never;</code></pre><p>这个代码有一个类型即 ReadonlyArray 类型,它也是 ts 的一个内置类型,表示数组项只读的数组,那么这个类型是如何实现的呢?</p><h4>ReadonlyArray 数组类型</h4><p>这个类型的实现还是很简单的,就是只读数组只有一个 at 方法,数组 at 方法的作用就是获取一个整数值并返回该索引处的项目,允许参数是正整数和负整数,负整数从数组的最后一项开始倒数。因此我们需要先实现这个只有 at 方法的接口,代码如下:</p><pre><code class="ts">interface RelativeIndexable<T> {
at(index: number): T | undefined;
}</code></pre><p>而只读数组类型 ReadonlyArray 只需要继承这个接口就行了,代码如下:</p><pre><code class="ts">interface ReadonlyArray<T> extends RelativeIndexable<T> {}</code></pre><h2>实现比较两个数组长度的类型</h2><p>有了能够获取数组长度的类型,接下来比较两个数组长度的类型就很简单了,代码如下:</p><pre><code class="ts">type CompareLength<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>
> = Length<T> extends Length<U> ? true : false;</code></pre><p>简单来说,就是两个数组长度一样就返回 true,否则返回 false,这也限制了我们最终实现的类型 2 个参数的联合类型最终提取出来的元素一定要一样。</p><h2>将属性构造成接口</h2><p>接下来我们要实现将属性构造成接口,要想构造成接口,那就需要属性和属性值,因此这个类型的实现是有 2 个参数的,可以看到我们最终实现的 Mapping 就是有 2 个参数,第一个参数作为属性,第二个参数作为属性值。而由于接口属性类型有限制,即只能是 PropertyKey 类型,因此我们是需要判断的,同理,为了实现属性和属性值一一对应有值的情况下,我们也需要对第二个参数做判断,只有满足 2 个参数类型都是 PropertyKey 类型,才能构造成接口,并且构造成接口我们可以使用 Record 类型。</p><p>根据以上分析,我们的最终代码就实现如下:</p><pre><code class="ts">// 不一定要叫Callback,也可以叫名字
type Callback<T, U> = T extends PropertyKey
? U extends PropertyKey
? Record<T, U>
: never
: never;</code></pre><p>以上还涉及到了 ts 的两个内置类型,第一个是 PropertyKey 类型,第二个则是<code>Record<T,U></code>类型,下面我们来一一看下这 2 个类型的实现。</p><h3>PropertyKey 类型</h3><p>第一个 PropertyKey 类型非常简单,它表示对象的属性类型,我们只需要知道 js 对象的属性只能是字符串或者数字或者符号就可以知道这个类型的实现,代码如下:</p><pre><code class="ts">type PropertyKey = string | number | symbol;</code></pre><p>可以看到这就是一个联合类型,属性的类型只能是字符串或者数值或者符号。</p><h3>Record 类型</h3><p>Record 类型表示构造一个构造一个具有类型 T 的一组属性 U 的类型,我们只需要使用 in 操作符即可实现,因为这个类型的第一个参数是要作为接口属性的,而第二个参数则是作为对应的属性值。代码如下:</p><pre><code class="ts">type Record<T extends keyof any, U> = {
[K in T]: U;
};</code></pre><p>这就是 Record 类型的实现,这其中还设计到了 ts 的一个关键字,即 keyof,它表示提取类型的属性,这个关键字通常用来提取接口的属性,最后会返回组成属性的联合类型。例如:</p><pre><code class="ts">type Test = {
a: string;
1: number;
};
type TestKey = keyof Test; // 'a' | 1</code></pre><h2>将两个数组类型构造成接口</h2><p>有了前面的几个类型的实现,接下来我们需要实现一个根据 2 个参数数组构造成接口的类型,为此我们需要定义第三个参数,第三个参数应该是一个接口对象,用来当作最终返回的结果,默认是一个空对象,而前面 2 个参数就是我们的属性组成的只读数组。结构如下所示:</p><pre><code class="ts">type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = any;</code></pre><p>接下来第一步,首先我们需要比较 2 个参数数组长度应该是一样的,不一样,我们就直接返回 never。如下所示:</p><pre><code class="ts">type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = CompareLength<T, U> extends true ? any : never;</code></pre><blockquote>ps: 以上包括后面用 any 表示我们还没有实现,起一个占位符作用,方便我们理解实现思路。</blockquote><p>紧接着第二步,我们需要判断是否是空数组,只需要判断其中一个即可,因为我们已经判断了两个数组长度是否相等,如果其中一个是空数组,那么另一个必定也是空数组,如果是空数组,直接返回结果即可,此时默认值就是空对象,直接返回结果也合理,代码如下:</p><pre><code class="ts">type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = CompareLength<T, U> extends true ? (T extends [] ? R : any) : never;</code></pre><p>接下来第三步,我们还需要做判断,那就是如果 2 个数组都只有一个数组项,那么我们只需要将第一个数组项提取出来,这里当然是使用 infer 关键字来推导数组项,然后使用 Callback 类型构造成接口并与 R 结果取并集即可。代码如下所示:</p><pre><code class="ts">type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = CompareLength<T, U> extends true
? T extends []
? R
: T extends [infer F1]
? U extends [infer F2]
? R & Callback<F1, F2>
: never
: any
: never;</code></pre><p>第四步就是如果数组有多个数组项,则我们需要递归的取并集。代码如下所示:</p><pre><code class="ts">type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = CompareLength<T, U> extends true
? T extends []
? R
: T extends [infer F1]
? U extends [infer F2]
? R & Callback<F1, F2>
: never
: T extends [infer F1, ...infer T1]
? U extends [infer F2, ...infer T2]
? Zip<T1, T2, R & Callback<F1, F2>>
: never
: never
: never;</code></pre><p>虽然这个类型的实现代码比较长,但其实我们逐一拆分下来理解起来还是比较容易的。</p><h2>实现 Mapping 类型</h2><p>有了前面几个类型的实现,最终我们就可以解答这道题了,我们只需要将 2 个联合类型构造成 2 个数组,然后使用 Zip 类型将 2 个类型组成的数组转成接口即可,代码如下:</p><pre><code class="ts">type Mapping<T extends Union, Value extends string> = Zip<
UnionToArray<T>,
UnionToArray<Value>
>;</code></pre><p>以上代码很好理解,我们将 2 个联合类型使用 UnionToArray 构造成 2 个类型数组,然后使用 Zip 类型构造成接口。</p><h2>优化</h2><p>不过以上代码的实现还不算完美,因为我们最终的结果是使用 Record 类型展示的,并不直观,因此最后一步,我们还需要将 Record 类型转成可以直观看到的接口类型,很简单,只需要读取每一个接口属性即可,和 Record 类型实现原理很类似。代码如下:</p><pre><code class="ts">type ToObj<T> = {
[K in keyof T]: T[K];
};</code></pre><h2>最终实现版本</h2><p>将优化后的代码与前面的实现合并,就得到了我们的最终实现,代码如下:</p><pre><code class="ts">type Mapping<T extends Union, Value extends string> = ToObj<
Zip<UnionToArray<T>, UnionToArray<Value>>
>;</code></pre><p>下面,我们将以上所有实现代码整理到一起,代码如下:</p><pre><code class="ts">// 第一步
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// 第二步
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// 第三步
type PopUnion<U> = UnionToOvlds<U> extends (f: infer A) => void ? A : never;
// 第四步
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
// 第五步
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
// 第六步
type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? L
: never;
// 第七步
type CompareLength<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>
> = Length<T> extends Length<U> ? true : false;
// 第八步
type Callback<T, U> = T extends PropertyKey
? U extends PropertyKey
? Record<T, U>
: never
: never;
// 第九步
type Zip<
T extends ReadonlyArray<any>,
U extends ReadonlyArray<any>,
R extends Record<string, any> = {}
> = CompareLength<T, U> extends true
? T extends []
? R
: T extends [infer F1]
? U extends [infer F2]
? R & Callback<F1, F2>
: never
: T extends [infer F1, ...infer T1]
? U extends [infer F2, ...infer T2]
? Zip<T1, T2, R & Callback<F1, F2>>
: never
: never
: never;
// 第十步
type ToObj<T> = {
[K in keyof T]: T[K];
};
// 最终
type Mapping<T extends Union, Value extends string> = ToObj<
Zip<UnionToArray<T>, UnionToArray<Value>>
>;</code></pre><h2>总结</h2><p>下面我们来总结一下这道题中我们学到的知识点:</p><ol><li>extends 关键字用于条件判断。</li><li>infer 关键字用于推导类型。</li><li>keyof 关键字用于获取对象接口属性。</li><li>ts 类型递归。</li><li>ts 中的 3 个基本类型的含义,即 any,never,void 的含义。</li><li>ts 中内置类型的实现,如: ReadonlyArray,Exclude,PropertyKey,Record。</li></ol><p>以上的知识点,在 ts 类型体操当中将会经常用到,所以需要理解深刻。</p><h2>最后</h2><p>只是一道题目,我们就学到了 ts 的很多类型体操的知识,ts 类型这么有趣,难道不是吗?</p>
隐藏元素 display:none 与 visibility:hidden 的区别你真的知道吗?
https://segmentfault.com/a/1190000044346780
2023-10-30T11:20:14+08:00
2023-10-30T11:20:14+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044317599">1024 程序员节</a>活动,欢迎正在阅读的你也加入。</blockquote><h2>隐藏元素 display:none 与 visibility:hidden 的区别你真的知道吗?</h2><p>有关隐藏元素的方式 display: none 与 visibility:hidden 的区别,这可以说是 css 面试题当中最常见的一道题了。相信大多数开发者被问到的第一答案就是:<br>display: none 不占据空间,visibility: hidden 占据空间。但实际上两者之间的区别并不只是不占据空间这么简单,且听我娓娓道来。</p><h3>区别之一: visibility 拥有继承性</h3><p>什么意思呢? 我们知道,如果一个元素的 visibility 设置为 hidden,它的子元素也会被隐藏,那么你知道子元素被隐藏的原理是什么吗?就是继承性。简单来说,就是子元素也继承了 visibility:hidden 的特性。换句话说,如果我们将子元素修改成 visibility: visible,你会发现子元素不再是隐藏,而是显示。来看一个例子感受一下:</p><p>html 代码如下:</p><pre><code class="html"><div class="vh-parent">
<div class="vh-inherit-child">这是继承子元素的hidden隐藏</div>
<div class="vv-child">子元素设置了visibility: visible之后又显示了</div>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.vh-parent {
visibility: hidden;
}
.vv-child {
visibility: visible;
}</code></pre><p>实际运行效果如下图所示:</p><p><img src="/img/remote/1460000044346783" alt="1.png" title="1.png"></p><h3>区别之二: visibility 不会影响 css 计数器</h3><p>visibility:hidden 不会影响计数器的计数,这和 display:none 完全不一样。举个例子,如下 CSS 和 HTML 代码:</p><pre><code class="html"><ol>
<li>列表</li>
<li class="dn">列表</li>
<li>列表</li>
<li>列表</li>
</ol>
<ol>
<li>列表</li>
<li class="vh">列表</li>
<li>列表</li>
<li>列表</li>
</ol></code></pre><pre><code class="css">.vh {
visibility: hidden;
}
.dn {
display: none;
}
ol {
border: 1px solid;
margin: 1em 0;
counter-reset: test;
}
li:after {
counter-increment: test;
content: counter(test);
}</code></pre><p>实际运行效果如下图所示:</p><p><img src="/img/remote/1460000044346784" alt="2.png" title="2.png"></p><p>可以看到,visibility:hidden 虽然让其中一个列表不可见了,但是其计数效果依然存在。相比之下,设置 display:none 的列表就完全没有参与计数运算。</p><h3>区别之三: visibility 过渡效果有效,而 display 则无效</h3><p>CSS3 transition 支持的 CSS 属性中有 visibility,但是并没有 display。如以下示例:</p><pre><code class="css">/* 过渡效果无效 */
.test {
display: none;
position: absolute;
opacity: 0;
transition: opacity 0.25s;
}
.test:hover {
display: block;
opacity: 1;
}</code></pre><pre><code class="css">/* 过渡效果无效 */
.test {
position: absolute;
opacity: 0;
transition: opacity 0.25s;
visibility: hidden;
}
.test {
visibility: visible;
opacity: 1;
}</code></pre><p>由于 transition 可以延时执行,因此,和 visibility 配合可以使用纯 CSS 实现 hover 延时显示效果,由此提升我们的交互体验。来看如下一个示例:</p><pre><code class="html"><table>
<tr>
<td>数据1</td>
<td>数据2</td>
<td>
<a href>操作▾</a>
<div class="list">
<a href>编辑</a>
<a href>删除</a>
</div>
</td>
</tr>
<tr>
<td>数据1</td>
<td>数据2</td>
<td>
<a href>操作▾</a>
<div class="list">
<a href>编辑</a>
<a href>删除</a>
</div>
</td>
</tr>
</table></code></pre><pre><code class="css">td {
padding: 5px 10px;
border: 0;
background: #fff;
font-size: 14px;
}
td a {
display: block;
}
.list {
width: 80px;
position: absolute;
visibility: hidden;
border: 1px solid #ccc;
background: #fff;
}
td:hover .list {
visibility: visible;
transition: visibility 0s 0.2s;
}
.list a {
padding: 5px 10px;
color: #333;
}
.list a:hover {
background-color: #f5f5f5;
}</code></pre><p>效果如下图所示:</p><p><img src="/img/remote/1460000044346785" alt="3.png" title="3.png"></p><p>以上是一个很常见的 hover 悬浮显示列表效果,而且有多个触发点相邻,对于这种 hover 交互,如果在显示的时候增加一定的延时,可以避免不经意触碰导致覆盖目标元素的问题。如果没有增加延时效果,则会存在如下情况:我本来想去 hover 第二行的“操作”文字,但是由于鼠标光标移动路径不小心经过了第一行的“操作”,结果把第二行本来 hover 的“操作”覆盖了,必须重新移出去,避开干扰元素,重新 hover 才行。如此一来,对用户体验就不好了。而恰好 visibility 就可以处理这个问题。</p><h3>区别之四: visibility 可以获得元素的尺寸位置,而 display 则无法获取</h3><p>在实际开发中,我们会遇到这样的场景:我们需要对隐藏元素进行尺寸和位置的获取,以便对交互布局进行精准定位。这时候如果使用 display:none 来隐藏元素,我们获取到元素的尺寸位置则是 0,但是 visibility 则不会。js 代码如下:</p><pre><code class="js">console.log('clientWidth: ' + element.clientWidth);
console.log('clientHeight: ' + element.clientHeight);
console.log('clientLeft: ' + element.clientLeft);
console.log('clientTop: ' + element.clientTop);
console.dir(element.getBoundingClientRect());</code></pre><p>因此面对以上的场景,我们更应该选择 visibility 来隐藏元素。</p><h3>区别之五: visibility 在无障碍访问这一块比 display 更友好</h3><p>视觉障碍用户对页面的状态变化都是通过声音而非视觉感知的,因此有必要告知其准确信息。</p><p>通过本文,我们了解到了 visibility 与 display 的详细区别,在面试的时候我们也不会只回答的片面了,知晓以上的区别能给我们的面试加分。</p><h2>隐藏元素方式</h2><p>最后分享一些隐藏元素的方式:</p><ol><li>如果希望元素不可见,同时不占据空间,辅助设备无法访问,同时不渲染,可以使用<code><script></code>标签隐藏。例如:</li></ol><pre><code class="html"><script type="text/html">
<img src="1.jpg" />
</script></code></pre><p><code><script></code>标签是不支持嵌套的,因此,如果希望在<code><script></code>标签中再放置其他不渲染的模板内容,可以试试使用<code><textarea></code>元素。例如:</p><pre><code class="html"><script type="text/html">
<img src="1.jpg" />
<textarea style="display:none;">
<img src="2.jpg">
</textarea
>
</script></code></pre><ol start="2"><li>如果希望元素不可见,同时不占据空间,辅助设备无法访问,但资源有加载,DOM 可 访问,则可以直接使用 display:none 隐藏。例如:</li></ol><pre><code class="css">.hidden {
display: none;
}</code></pre><ol start="3"><li>如果希望元素不可见,同时不占据空间,辅助设备无法访问,但显隐的时候可以有 transition 淡入淡出效果,则可以使用:</li></ol><pre><code class="css">.hidden {
position: absolute;
visibility: hidden;
}</code></pre><ol start="4"><li>如果希望元素不可见,不能点击,辅助设备无法访问,但占据空间保留,则可以使用 visibility:hidden 隐藏。例如:</li></ol><pre><code class="css">.hidden {
visibility: hidden;
}</code></pre><ol start="5"><li>如果希望元素不可见,不能点击,不占据空间,但键盘可访问,则可以使用 clip 剪裁隐藏。例如:</li></ol><pre><code class="css">.clip {
position: absolute;
clip: rect(0 0 0 0);
}
.out {
position: relative;
left: -999em;
}</code></pre><ol start="6"><li>如果希望元素不可见,不能点击,但占据空间,且键盘可访问,则可以试试 relative 隐藏。例如,如果条件允许,也就是和层叠上下文之间存在设置了背景色的父元素,则也可以使用更友好的 z-index 负值隐藏。例如:</li></ol><pre><code class="css">.lower {
position: relative;
z-index: -1;
}</code></pre><ol start="7"><li>如果希望元素不可见,但可以点击,而且不占据空间,则可以使用透明度。例如:</li></ol><pre><code class="css">.opacity {
position: absolute;
opacity: 0;
filter: Alpha(opacity=0);
}</code></pre><ol start="8"><li>如果单纯希望元素看不见,但位置保留,依然可以点可以选,则直接让透明度为 0。例如:</li></ol><pre><code class="css">.opacity {
opacity: 0;
filter: Alpha(opacity=0);
}</code></pre><ol start="9"><li>在标签受限的情况下希望隐藏某元素文字,例如:</li></ol><pre><code class="css">.hidden {
text-indent: -120px;
}</code></pre><ol start="10"><li>如果希望显示的时候可以加一个 transition 动画,可以使用 max-height 进行隐藏。</li></ol><pre><code class="css">.hidden {
max-height: 0;
overflow: hidden;
}</code></pre><blockquote>注: 以上内容阅读《css世界》而整理。</blockquote>
有关安卓软键盘遮挡输入框的问题分析及处理方案
https://segmentfault.com/a/1190000044339632
2023-10-26T22:26:45+08:00
2023-10-26T22:26:45+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044317599">1024 程序员节</a>活动,欢迎正在阅读的你也加入。</blockquote><p>经过多番查阅资料,发现只通过前端是无法完美处理该问题的。主要原因在于:</p><ol><li>js没有相关接口可以准确获取安卓手机软键盘的高度,这样也就无法处理当软键盘弹出时,能够将输入框顶上去,并且与软键盘完美的贴合,比如输入框在最底部的场景,此时页面滚动高度不够,因此无法准确的滚动到底,使得软键盘将输入框顶上去并与之完美的贴合。</li><li>js并不能监听软键盘关闭的事件,比如点击折叠按钮关闭软键盘,此时输入框焦点还在,无法触发输入框的blur事件,有人说会触发resize事件,但是经过我的测试,resize事件并未触发。</li></ol><p>以下是我的解决方案。首先封装一个获取页面视图宽高的工具函数,代码如下:</p><pre><code class="ts"> const getViewSize = ():{ width:number;height:number } {
if (window.innerWidth) {
return {
width: window.innerWidth,
height: window.innerHeight
};
} else if (document.compatMode === 'CSS1Compat') {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
};
} else {
return {
width: document.body.clientWidth,
height: document.body.clientHeight
};
}
}</code></pre><p>其次,监听页面resize事件,同时也监听输入框的focus和blur事件。如下:</p><pre><code class="ts">input.addEventListener('focus',this.onSetScrollHandler.bind(this,true));
input.addEventListener('blur',this.onSetScrollHandler.bind(this,false));
window.addEventListener('resize',this.onSetScrollHandler.bind(this,false));</code></pre><p>接下来是onSetScrollHandler函数的处理,首先我们要考虑2种情况,第一那就是如果页面滚动高度足够,滚动到输入框时,页面的剩余滚动高度刚好可以大于等于软键盘的高度,此时就可以做到完美贴合输入框。那如果滚动高度不够,我们是需要将页面根元素高度给增大的,而我们是通过设置style的height来将根元素高度增大的。因此在触发blur事件或者是触发resize事件,高度变动时,就需要将根元素的高度恢复原样,因此我们需要先缓存页面的高度,以及是否存在高度的设置。如下:</p><pre><code class="ts">const originHeight = getViewSize().height;
const originBodyHeight = document.body.style.height;</code></pre><p>我们可以看到onSetScrollHandler是添加了一个布尔值参数的,用来做判断的,当然此时因为会触发resize事件,我们还需要单独计算状态。</p><p>此外由于这个问题是安卓手机出的,为了避免我们加的代码影响到ios手机又或者其它设备默认是实现的软键盘弹起顶上去的功能,我们需要添加环境的判断。如下:</p><pre><code class="ts">type envReturnType = {
isBrowser: boolean;
isServer: boolean;
isMobile: boolean;
isAndriod: boolean;
isIos: boolean;
canUseWorkers: boolean;
canUseEventListeners: boolean;
canUseViewport: boolean;
}
const getEnv = (): envReturnType => {
const inBrowser = Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
const isMobileVailable = (reg: string | RegExp): boolean => Boolean(navigator.userAgent.match(reg));
const getEnvObject = [
isBrowser: inBrowser
isMobile: isMobileVailable(/(iPhoneliPod]Androidlios)/i)Test Regex...
isAndriod: isMobileVailable(/(android)/i),
isIos: isMobileVailable(/(iPhoneliPodlios)/i),
isServer: !inBrowser,
canUseWorkers: typeof Worker !== undefined!
canUseEventListeners: inBrowser && Boolean(window.addEventListener)
canUseViewport: inBrowser && Boolean(window.screen)
];
return Object.assign(0bject.values(getEnvObject),getEnvObject);
}</code></pre><p>因此在onSetScrollHandler函数内部首先要做的就是判断是否是安卓手机。如下:</p><pre><code class="ts">const onSetScrollHandler = (status: boolean) => {
const { isAndriod } = getEnv();
if(!isAndriod){
return;
}
// 后续代码
}</code></pre><p>接着我们获取body元素的scrollTop,然后获取到需要被顶上去的输入框的scrollTop加上它的高度就是我们要滚动的距离,然后为了保证兼容性,我们需要设置document.body.scrollTop和document.documentElement.scrollTop,当然在部分安卓手机上,可能这2个设置都不会生效,这时候需要调用元素的scrollIntoView方法。同时我们还需要修改body元素的高度,由于无法获取到软键盘的准确高度,这时候我们需要修改body元素高度为2个屏幕高度,这样才能达到让剩余滚动高度足够大于软键盘高度。最终版本代码如下:</p><pre><code class="ts">const onSetScrollHandler = (status: boolean) => {
const { isAndriod } = getEnv();
if(!isAndriod){
return;
}
const { height: resizeHeight } = getViewSize().height;
// 这里主要是为了还原
const { scrollTop } = document.body || document.documentElement;
if(status || resizeHeight < originHeight){
// 撑大根元素高度,方便滚动
document.body.style.height = `${originHeight + resizeHeight}px`;
const top = input.offsetHeight + input.scrollTop;
document.body.scrollTop = document.documentElement.scrollTop = top;
// 确保兼容性,还得调用scrollIntoView方法还原
input.scrollIntoView();
}else {
// 如果不存在原始高度,则移除height属性,否则重新设置height
if(!originBodyHeight){
document.body.style.removeProperty('height');
}else{
document.body.style.height = `${originBodyHeight}px`;
}
// 还原scrollTop
document.body.scrollTop = document.documentElement.scrollTop = scrollTop;
// 确保兼容性,还得调用scrollIntoView方法还原
document.body.scrollIntoView();
}
}</code></pre><p>一个小小的软键盘遮挡问题,竟然要写出这么多的兼容性代码,兼容真的好蛋疼。</p><p>以上方案还并不算完美的处理掉了这个问题,正如开头提到的2个问题限制,因此当出现开头提到的场景,以上的代码就不能解决,也可以算作是体验问题。最完美的方案还得是端上做配合才行。</p>
难以置信,一个小小的需求让我捣鼓出一个提效的网站来
https://segmentfault.com/a/1190000044332220
2023-10-24T21:24:05+08:00
2023-10-24T21:24:05+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
1
<h2>难以置信,一个小小的需求让我捣鼓出一个提效的网站来</h2><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044317599">1024 程序员节</a>活动,欢迎正在阅读的你也加入。</blockquote><h3>需求介绍</h3><p>事情是这样的,有个群友在业务当中碰到一个小小的需求,需求是这样的: 页面当中存在多个输入框,输入框的 value 值是一个数值组成的字符串(盲猜应该是身份证号码),这个字符串的位数是 15 位或者是 18 位,例如:'621848063680370'(15 位)和'621848063688370808'(18 位),然后默认的值是这样的,现在问题来了,需求希望在这些数值中插入空白符号,比如 15 位的数字就按照 6 + 6 + 3 的格式分隔,分隔的时候需要使用空白符号。比如'621848063680370'分隔后应该变成'621848 063680 370',也就是数字位数到了第 6 位就加个空白符号分隔,...依次类推,而 18 位数字的分割规则则是:6 + 4 + 4 + 4。比如'621848063688370808'应该分隔成'621848 0636 8837 0808'。</p><p>这个需求就是对字符串的处理,提到分隔替换,那么我们就可以想到强大的字符串替换方法 replace,这个方法可以接受 2 个参数,一个参数通常是一个正则表达式,第二个参数则是一个回调函数,用于定义替换后返回字符串。因此,我的第一个想法就是使用正则表达式去处理,如何处理呢?</p><h3>原理分析</h3><p>首先我们需要去理解这个规则,从需求我们可以发现,不同的位数,规则就会有所不同,因此我们可以提前用一个数据来表示这种规则,为了保持良好的扩展性,我设计了如下字段:</p><pre><code class="ts">type spaceRule = {
digit: number; // 位数
rule: RegExp; //规则
symbolNumber: number; // 插入符号数量
symbolName: string; // 插入符号
};</code></pre><p>可以看到,我设计了四个参数,正如注释所说,每一个参数都有具体的含义,为什么要如此设计参数呢?还是看需求,我们需求首先是限定了数字的位数,只可能是 15 位或者是 18 位,那如果存在 19 位又或者 20 位的场景呢?因此我们需要设计一个位数的参数,然后是每一个位数对应的规则是不一样的,因此我们也需要设计一个 rule 参数,然后是插入符号数量,也许会存在 1 个空白,2 个空白等等场景,或者我们不一定插入空白符号,也有可能是其它符号例如"-"等等,因此就设计 symbolNumber 和 symbolName 参数。</p><p>既然规则是类似 6 + 6 + 3 这样的规则,因此我们想到使用正则表达式来完成这个功能是可以的,我们将其拆分开来,分成 3 个分组,第一个分组匹配 6 个数字,第二个分组匹配 6 个数字,第三个分组匹配 3 个数字,然后针对分组之间插入特定的符号即可。</p><p>正则表达式中匹配数字可以使用'\d'来表示,然后匹配位数位 6,我们就可以使用量词'{6,}'来表示,因此我们的 6 + 6 + 3 规则就可以写成如下:</p><pre><code class="ts">const rule = /(\d{6,})(\d{6,})(\d{3,})/g;</code></pre><h3>replace方法核心参数</h3><p>接下来根据 <a href="https://link.segmentfault.com/?enc=OXswuKRXA%2F9dU%2FvdEKLGhQ%3D%3D.2LMHVXw1e4uN0Zk3Kg5%2BxMGLv%2FhPVV1owPaDV9CHpfEaVrFLyJBOJnWP1K0U9URgxwQAstqbgOLgvVoGIGYtB6Fx90ryZnZO0oY%2B1TAx1mQkF3EIXaDEtSv3GJoQ8OVN" rel="nofollow">mdn</a> 对 replace 第二个参数回调函数参数的介绍,我们就知道,如果匹配到了正则表达式,则回调函数的参数会是如下所示:</p><pre><code class="ts">function replacer(match, p1, p2, /* …, */ pN, offset, string, groups) {
return replacement;
}</code></pre><p>其中 p1,p2...pN 就是我们这里需要用到的匹配分组,有个专业的名词叫做捕获组,前面 9 个捕获组对应的就是正则表达式实例对象的$1....$9 属性。</p><p>然后其返回值就会用作字符串被替代的部分,因此这里我们可以使用展开运算符将中间的捕获组截取出来,然后利用 join 方法,传入需要插入的符号即可转成符合需求的字符串。</p><blockquote>ps: 由于这里经过我对谷歌浏览器的测试,replacer 的倒数第 3 个参数不存在,因此我这里截取结束索引值就是 args.length - 2。</blockquote><p>因此,我们可以写出如下代码:</p><pre><code class="ts">const spaceRule = {
digit: 15,
rule: /(\d{6,})(\d{6,})(\d{3,})/g,
symbolNumber: 1,
symbol: " ",
};
const allInputs = document.querySelectorAll("input");
allInputs.forEach((item) => {
const v = item.value;
const { symbolNumber, symbol, rule } = spaceRule;
item.value = v.replace(rule, (...args) =>
args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber))
);
});</code></pre><p>这样就达到了将输入框中 15 位数字中间插入符号的需求,并且满足 6 + 6 + 3 的规则。如果是 18 位数字,规则也变成了 6 + 4 + 4 + 4,我们就只需要修改 digit 和 rule 值即可,如下:</p><pre><code class="ts">const spaceRule = {
digit: 18,
rule: /(\d{6,})(\d{4,})(\d{4,})(\d{4,})/g,
symbolNumber: 1,
symbol: " ",
};
const allInputs = document.querySelectorAll("input");
allInputs.forEach((item) => {
const v = item.value;
const { symbolNumber, symbol, rule } = spaceRule;
item.value = v.replace(rule, (...args) =>
args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber))
);
});</code></pre><p>可以看到,我们核心的替换逻辑是没有变动的,变动的只是我们定义的规则而已,哪怕是用在 vue 和 react 框架当中,我们也只是修改一些框架特定的语法,但其实核心替换逻辑还是不会变动,比如 vue2 代码如下:</p><pre><code class="ts">const spaceRule = {
digit: 18,
rule: "6 + 4 + 4 + 4",
symbolNumber: 1,
symbol: " ",
};
export default {
methods: {
onFormatValue(item) {
const { symbolNumber, symbol, rule } = spaceRule;
const regExp = new RegExp(
`${rule
.split("+")
.map((item) => `(\\d{${Number(item)},})`)
.reduce((res, item) => ((res += item), res), "")}`,
"g"
);
const formatValue = item.replace(regExp, (...args) =>
args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber))
);
return formatValue;
},
},
};</code></pre><p>vue3 代码如下:</p><pre><code class="ts">const spaceRule = {
digit: 18,
rule: "6 + 4 + 4 + 4",
symbolNumber: 1,
symbol: " ",
};
const onFormatValue = computed(() => (item) => {
const { symbolNumber, symbol, rule } = spaceRule;
const regExp = new RegExp(
`${rule
.split("+")
.map((item) => `(\\d{${Number(item)},})`)
.reduce((res, item) => ((res += item), res), "")}`,
"g"
);
const formatValue = item.replace(regExp, (...args) =>
args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber))
);
return formatValue;
});</code></pre><p>react 代码如下:</p><pre><code class="ts">const spaceRule = {
digit: 18,
rule: "6 + 4 + 4 + 4",
symbolNumber: 1,
symbol: " ",
};
const FormatInput = () => {
const onFormatValue = React.useCallback((value) => {
const { symbolNumber, symbol, rule } = spaceRule;
const regExp = new RegExp(
`${rule
.split("+")
.map((item) => `(\\d{${Number(item)},})`)
.reduce((res, item) => ((res += item), res), "")}`,
"g"
);
const formatValue = value.replace(regExp, (...args) =>
args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber))
);
return formatValue;
}, []);
return <input type="text" value={onFormatValue("621848063688370808")} />;
};
export default FormatInput;</code></pre><p>纵观以上的代码,我们可以发现核心的 js 逻辑是没有变动的,变动的只是一些框架有的概念而已,例如 vue2 中,我们使用方法结合双向绑定指令 v-model 来修改,而 react 也是同理,vue3 我们则是使用计算属性来表示。</p><h3>网站介绍</h3><p>基于以上的分析,接下来,就是我们这个提效网站实现的雏形,首先来看一下网站,如下图所示:</p><p><img width="723" height="325" src="/img/bVdaaZX" alt="" title=""></p><p>截图截的不全,更详细可以点<a href="https://link.segmentfault.com/?enc=njlF40zKaUgIP6iotkhbgg%3D%3D.pHmcb3GW0OB5jcp2lGYaM%2FUWPkRT0fD1umZUxn15TIWn8ZbxSsyEG34p514nvaMVU1tMkrBTpqc7NkR00wbnWw%3D%3D" rel="nofollow">这里</a>查看。</p><p>通过以上的网站展示,我们已经初步构思好了整个网站的构架:</p><ol><li>创建规则的表单部分。</li><li>预览效果部分。</li><li>代码展示部分。</li></ol><p>其中代码展示部分又提供了不同框架和原生版本的展示以及复制,同样的还提供了在线示例的下载,其它就是一些额外展示功能组件,没什么可说的,比如底部链接展示,头部组件,还有就是需求介绍展示组件。</p><p>核心原理我们已经知道了,接下来无非就是写好页面架构,技术选型上我们使用的是 vue3 + vite + naive-ui 组件库。</p><h3>重点代码分析</h3><p>核心页面我们也不必要介绍,这里只重点提一下一些重要功能的实现点,首先是代码压缩包的下载,我们采用的是 file-saver 和 jszip 库,代码很简单,如下所示:</p><pre><code class="ts">const zip = new JSZip();
zip.file(
`${codeTypeValue.value}-demo.html`,
htmlTemplate(
renderTemplateCode.value.html,
renderTemplateCode.value.js,
codeTypeValue.value
)
);
// 调用zip的generateAsync生成一个blob文件
const content = await zip.generateAsync({ type: "blob" });
// saveAs 方法实现下载
saveAs(content, `${codeTypeValue.value}-demo.zip`);</code></pre><p>其实这里的 htmlTemplate 就是构造一个下载代码模板,如下所示:</p><pre><code class="ts">import { CodeTemplateKey } from "./code";
export const getScriptTemplate = (type: CodeTemplateKey) => {
if (type.includes("vue")) {
const src =
type === "vue2"
? "https://cdn.bootcdn.net/ajax/libs/vue/2.6.7/vue.min.js"
: "https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.global.min.js";
return `<script src="${src}"></script>`;
} else if (type === "react") {
return `<script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.22.17/babel.min.js"></script>`;
} else {
return "";
}
};
export const htmlTemplate = (
htmlContent: string,
jsContent: string,
type: CodeTemplateKey
) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>输入框生成插入符号&nbsp;${type}&nbsp;demo</title>
<style>
body {
margin: 0;
}
input {
padding: 8px 24px;
border: 0;
border-radius: 15px;
background-color: #fefefe;
color: rgba(0, 0, 0, .85);
margin: 8px 0;
border: 1px solid #232323;
min-width: 250px;
}
</style>
</head>
<body>
<div id="app">${htmlContent}</div>
${getScriptTemplate(type)}
<script type="${
type === "react" ? "text/babel" : "text/javascript"
}">${jsContent}</script>
</body>
</html>`;</code></pre><p>代码模板如下:</p><pre><code class="ts">export const codeTemplate = {
js: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((item) => `<input type="text" value="${item}"/>\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: /${options.rule
.split("+")
.map((item) => `(\\d{${Number(item)},})`)
.reduce((res, item) => ((res += item), res), "")}/g,
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
const allInputs = document.querySelectorAll('input');
allInputs.forEach(item => {
const v = item.value;
const { symbolNumber, symbol, rule } = spaceRule;
item.value = v.replace(rule, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
})`,
}),
vue2: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" v-model="onFormatValue('${_}')" />\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
export default {
methods:{
onFormatValue(item){
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = item.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
}
}
}
`,
}),
vue3: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" :value="onFormatValue('${_}')" />\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
const onFormatValue = computed(() => (item) => {
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = item.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
})
`,
}),
react: (options: IFormValue) => ({
html: "",
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
const FormatInput = () => {
const onFormatValue = React.useCallback((value) => {
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = value.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
},[])
return (
${Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" value={onFormatValue('${_}')} />`)
.join("")}
)
}
export default FormatInput;
`,
}),
};
export const demoCodeTemplate = {
js: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((item) => `<input type="text" value="${item}"/>\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: /${options.rule
.split("+")
.map((item) => `(\\d{${Number(item)},})`)
.reduce((res, item) => ((res += item), res), "")}/g,
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
const allInputs = document.querySelectorAll('input');
allInputs.forEach(item => {
const v = item.value;
const { symbolNumber, symbol, rule } = spaceRule;
item.value = v.replace(rule, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
})`,
}),
vue2: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" v-model="onFormatValue('${_}')" />\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
new Vue({
el:"#app",
methods:{
onFormatValue(item){
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = item.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
}
}
});
`,
}),
vue3: (options: IFormValue) => ({
html: Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" :value="onFormatValue('${_}')" />\n`)
.join(""),
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
Vue.createApp({
setup() {
const onFormatValue = Vue.computed(() => (item) => {
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = item.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
})
return {
onFormatValue
}
}
}).mount('#app')
`,
}),
react: (options: IFormValue) => ({
html: "",
js: `
const spaceRule = {
digit: ${options.digit},
rule: '${options.rule}',
symbolNumber: ${options.symbolNumber},
symbol: '${options.symbol}'
};
const FormatInput = () => {
const onFormatValue = React.useCallback((value) => {
const { symbolNumber,symbol,rule } = spaceRule;
const regExp = new RegExp(\`\${rule.split('+').map(item => \`(\\\\d{\$\{Number(item)\},})\`).reduce((res, item) => (res += item, res), '')}\`, 'g');
const formatValue = value.replace(regExp, (...args) => args?.slice(1, args.length - 2)?.join(symbol.repeat(symbolNumber)));
return formatValue;
},[])
return (
${Array.from({ length: options.inputNumber })
.map((_, i) => options.inputContent[i])
.map((_) => `<input type="text" value={onFormatValue('${_}')} />`)
.join("")}
)
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
root.render(<FormatInput />);
`,
}),
};
export type CodeTemplateKey = keyof typeof codeTemplate;
// 代码key列表
export const codeTypeList = Object.keys(codeTemplate) as CodeTemplateKey[];
// 代码版本下拉列表
export const selectCodeTypeList = codeTypeList.map((item) => ({
label: item,
value: item,
}));</code></pre><p>然后就是我们的 copy 复制代码功能函数的实现,原理就是利用了 navigator.clipboard api,如果浏览器不支持,我们就使用 document.execCommand api,工具函数代码如下所示:</p><pre><code class="ts">export const copyHandler = (str: string, dialog?: DialogApi) => {
const confirm = (title = "温馨提示", content = "已复制到剪切板") => {
dialog?.success({
title: title,
content: content,
positiveText: "确定",
});
};
const baseCopy = (copyText: string) =>
new Promise<void>((resolve, reject) => {
// 判断是否存在clipboard并且是安全的协议
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(copyText)
.then(() => {
resolve();
})
.catch(() => {
reject(new Error("复制失败"));
});
} else {
// 否则使用被废弃的execCommand
const input = document.createElement("input") as HTMLInputElement;
input.value = copyText;
// 使input不在viewport,同时设置不可见
input.style.position = "absolute";
input.style.left = "-9999px";
input.style.top = "-9999px";
document.body.append(input);
input.focus();
input.select();
// 执行复制命令并移除文本框
if (document.execCommand) {
document.execCommand("copy");
resolve();
} else {
reject(new Error("复制失败"));
}
input.remove();
}
});
baseCopy(str)
.then(() => confirm())
.catch(() => confirm("温馨提示", "复制失败"));
};</code></pre><h3>遇到的有意思的问题分析</h3><p>除此之外,其它都是一些很好理解的基础代码,因此不需要讲解,这里讲一个让我觉得有意思的问题,也是在源码当中有备注,那就是被代理的对象会被污染,可以看到我们的 config.ts 里面写了 2 个最基础的表单配置对象:</p><pre><code class="ts">export const defaultFormValue = {
digit: 15,
rule: "6 + 6 + 3",
symbol: " ",
symbolNumber: 1,
inputNumber: 1,
inputContent: ["621848063680370"],
};
export const baseDefaultFormValue = {
digit: 15,
rule: "6 + 6 + 3",
symbol: " ",
symbolNumber: 1,
inputNumber: 1,
inputContent: ["621848063680370"],
};</code></pre><p>用来设置表单的初始值对象,我在监听用户修改输入框值之后去改变绑定的初始值,发现绑定的初始值被修改污染了,哪怕我采用了复制对象副本(使用 JSON 和展开运算符来复制),都会修改原始配置对象。我们的表单配置对象是这样的:</p><pre><code class="ts">const formValue = ref({ ...defaultFormValue });</code></pre><p>也就是说我对 formValue 的修改会影响到 defaultFormValue,这就让我感觉很奇怪,所以我想创建一个 baseDefaultFormValue 的方式去解决这个问题,这样我在重置表单数据的时候,就能够重置为最初始的数据,如下:</p><pre><code class="ts">const handleResetClick = () => {
formRef.value?.restoreValidation();
// 不重新写一个defaultFormValue已经被污染了
formValue.value = {
...baseDefaultFormValue,
};
emit("on-submit", formValue.value);
};</code></pre><p>这个问题,目前我还没有分析出原因来,如果有感兴趣的大佬,可以通过参考<a href="https://link.segmentfault.com/?enc=o7B8i0Us4ep%2BmrH5L7xfkQ%3D%3D.ZjLuZIaVBoxNo%2Fqz50AVWL8aiNLGe2gF5oA%2Fr%2Box%2FE1iFgp2%2Fk4DJXlgfnz18x5QOjcdVU8eztJExRmcdYyEv9WcKFB%2B7QaNv6eCkLmFDgs%3D" rel="nofollow">源码</a>调试看看问题,我就没有时间去研究这个问题呢。</p><p>这些点是我觉得值得分析的地方,其它就没啥了,感谢阅读到这里,如果觉得有帮助可以点赞收藏,顺带可以帮我的项目点个 star,感激不尽。</p>
ew-message 一个基于typescript封装的消息提示框插件
https://segmentfault.com/a/1190000044276076
2023-10-04T11:25:46+08:00
2023-10-04T11:25:46+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<h2>ew-message</h2><p>一个基于 typescript 而封装的消息提示框插件,可用于不使用ui组件库的中小型网站中。</p><h3>安装与使用</h3><h4>安装</h4><pre><code>npm install ew-message --save-dev//或者 yarn add ew-message
</code></pre><h4>引入</h4><pre><code class="js"><script src="./dist/ew-message.min.js"></script></code></pre><p>消息提示框插件如下:</p><pre><code class="js">const msg1 = ewMessage('默认消息提示框');
const msg1 = ewMessage({
content: '默认消息提示框'
});</code></pre><p>option 配置对象有如下参数:</p><pre><code class="ts">interface ewMessageOption {
content: string; //消息提示框内容,必传参数
center?: boolean; //消息提示框内容是否居中
type?: string; //消息提示框类型,有四种: info,success,warning,error
duration?: number; //消息提示框消失时间
showClose?: boolean; //是否显示关闭按钮
stylePrefix?: string; //消息提示框样式前缀,注意插件有检测如果导入了样式文件,则这个配置无效
showTypeIcon?: boolean; // 是否显示类型图标,默认为true
typeIcon?: string; // 自定义类型图标
closeIcon?: string; // 自定义关闭按钮图标
container?: string | HTMLElement; // 挂载元素
immediate?: boolean; // 是否立即渲染消息提示框
removeClassName?: string; // 移除消息提示框动画类名,目前内置动画类名值: fadeOut与scaleDown
removeClassNameSymbol?: string; // 指定多个移除动画类名并且分隔符不是空白时传入
}</code></pre><blockquote>ps: 推荐使用导入样式文件的方式。</blockquote><h3>cdn 引入</h3><pre><code class="js">//样式引入
// CDN:https://www.unpkg.com/ew-message/dist/ew-message.min.css
// CDN:https://www.unpkg.com/ew-message/dist/ew-message.min.js</code></pre><h3>在组件中使用</h3><pre><code class="js">import ewMessage from 'ewMessage';
// 导入样式
import 'ew-message/dist/ew-message.min.css';
const msg = ewMessage(option); //option为配置对象,详情见前述</code></pre><p>当然也可以不引入样式,插件检测了如果不导入样式文件,则会自动添加样式,并且也可以给样式添加类名前缀自定义样式。</p><h2>参数接口</h2><p>这里主要介绍函数的参数类型。</p><h3>字符串参数</h3><p>默认参数可以是一个字符串,用作消息提示框的内容,如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');</code></pre><h3>配置对象</h3><p>配置对象主要有 6 个属性,分别如下:</p><h4>content</h4><p>content 是一个字符串,用作消息提示框的内容,默认为空,如果在开发环境下(即导入的是非生产模式的文件 ewMessage.js),则会在控制台给出警告,如:</p><pre><code class="ts">const msg = ewMessage({ content: '这是一个默认的消息提示框' });</code></pre><h4>center</h4><p>center 属性是一个布尔值,表示是否让消息提示框的内容剧中,默认是 false。如:</p><pre><code class="ts">// 消息提示框内容居中
const msg = ewMessage({ content: '这是一个默认的消息提示框', center: true });</code></pre><h4>type</h4><p>type 的值虽然是一个字符串,但只支持"info" | "success" | "warning" | "error",表示消息提示框的类型,默认是值是"info"。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个成功的消息提示框',
center: true,
type: 'success' // 成功消息提示框
});</code></pre><h4>duration</h4><p>该字段的值是一个数值,表示消息提示框关闭的时间,默认值是 6s,可以自定义修改。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
center: true,
type: 'success',
duration: 2000 // 消息提示框将在2s后关闭
});</code></pre><p>需要注意的是,插件内部做了一个小小的规范化,如果传入的时间即 duration 的值小于 1000,则会自动乘以 10。</p><blockquote><p>ps: 0.0.7 版本做了一个优化,如果 duration 不是一个数值,则会在开发环境下提示警告,如果小于等于 0,则会取 1000 作为默认值,默认值也由 6s 变成了 1s。</p><p>ps: 不建议将该值设置的过大。</p></blockquote><h4>maxDuration (0.0.7 版本新增)</h4><p>该字段的值也是一个数值,表示消息提示框的最大关闭时间,插件内部会将该值与传入的 duration 值做比较,取两者之间的最小值,默认值是 10s。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
center: true,
type: 'success',
duration: 120000,
maxDuration: 10000 // 最大关闭时间设置的是10s,因此消息提示框将在10s后关闭,而不是12s后关闭
});</code></pre><blockquote><p>ps: 如果传入的值不是一个数值,也会在开发环境下提示警告,如果小于等于 duration,则会取 duration 作为默认值。</p><p>ps: 不建议设置该值。</p></blockquote><h4>showClose</h4><p>该字段的值是一个布尔值,表示是否显示消息提示框的关闭按钮,默认值是 true,可以将值设置为 false。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
showClose: false
});</code></pre><blockquote>ps: 需要注意的是如果将 duration 的值设置为 0,showClose 设置为 false,则在开发环境下会提供警告,然后自动开启消息提示框的关闭按钮。</blockquote><h4>stylePrefix</h4><p>该属性是一个字符串,表示样式类名的前缀,默认是'ew-',可以自定义类名,方便自己修改样式。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
stylePrefix: 'el-'
});</code></pre><p>以上代码将会给消息提示框的样式类名前缀变成 el-,比如原本是 ew-message 类名将变成 el-message。</p><p>通常来讲这个属性应该是用不到的,只有在确实需要自定义样式的时候可以用到。</p><h4>showTypeIcon (0.0.8)新增</h4><p>该属性是一个布尔值,表示是否显示图标,会根据相关的类型来匹配相应的图标,比如 info 类型就是 info 类型的图标,默认值是 true。如下所示:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
showTypeIcon: true
});</code></pre><h4>typeIcon(0.0.8 新增)</h4><p>该属性用于自定义图标,需要将 showTypeIcon 设置为 true 才行,如果默认的图标不符合需求,可以自己传入一个 img 标签或者 svg 标签自定义一个图标,或者是任意元素,不过需要自己调整图标样式,可以定义<code>${prefix}-message-${type}-icon</code>相关的类名来使用默认的样式,其中 prefix 的值为前缀名默认是 ew,type 为消息提示框类型,如 info。如下所示:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
showTypeIcon: true,
typeIcon:
'<svg t="1695191942528" class="ew-message-icon ew-message-info-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7731" id="mx_n_1695191942529"><path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM448 448v384h128V448H448z m0-256v128h128V192H448z" fill="#1677ff" p-id="7732"></path></svg>'
});</code></pre><h4>closeIcon(0.0.8 新增)</h4><p>该属性用于自定义关闭按钮的图标,需要将 showClose 设置为 true 才行,如果默认的关闭按钮图标不符合需求,可以自己传入一个 img 标签或者 svg 标签自定义一个图标,或者是任意元素,不过需要自己调整图标样式,可以定义<code>${prefix}-message-close-icon</code>相关的类名来使用默认的样式,其中 prefix 的值为前缀名,默认是 ew。如:</p><pre><code class="ts">// 不推荐替换默认的图标
const msg = ewMessage({
content: '这是一个默认的消息提示框',
showTypeIcon: true,
closeIcon:
'<svg t="1695191942528" class="ew-message-close-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7731" id="mx_n_1695191942529"><path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM448 448v384h128V448H448z m0-256v128h128V192H448z" fill="#1677ff" p-id="7732"></path></svg>'
});</code></pre><h4>container(0.0.9 新增)</h4><p>该属性用于设置消息提示框挂载的元素,可以传入一个 dom 元素字符串或者是 dom 元素,不满足则会在开发环境下给出警告提示,默认值是 body 元素。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
container: '#test'
});
// 将消息提示框挂载到页面当中id为test的元素中</code></pre><h4>immediate(0.0.9 新增)</h4><p>该属性用于是否立即渲染消息提示框,如果设置为 false,则需要手动调用 render,render 方法可以传也可以不传参数,方法来渲染消息提示框,默认值是 true。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
immediate: false
});
msg.render(); // 内部会自动获取配置对象并渲染</code></pre><h4>removeClassName(0.0.9 新增)</h4><p>该属性用于卸载消息提示框时添加的动画类名,目前内置有 fadeOut 和 scaleDown 动画效果,如指定了正确的动画类名值,当点击关闭或者是自动关闭消息提示框的时候,会有相应的动画效果,默认值是空字符串,即没有动画效果。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
removeClassName: 'fadeOut' // 或者也可以传入ew-message-fadeOut
});</code></pre><p>也可以传入多个类名,多个类名之间可以使用任意值或者空白来隔开,如果不是使用空白隔开,则需要指定 removeClassNameSymbol 的值,例如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
removeClassName: 'fadeOut scaleDown' // 或者也可以传入ew-message-fadeOut ew-message-scaleDown
// 这里是使用空白隔开,因此不需要指定 removeClassNameSymbol
});</code></pre><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
removeClassName: 'fadeOut,scaleDown' // 或者也可以传入ew-message-fadeOut ew-message-scaleDown
removeClassNameSymbol: ','
// 这里是使用逗号隔开
});</code></pre><p>我们可以结合<a href="https://link.segmentfault.com/?enc=H6PctnKmlfRKa2CLWu7V7Q%3D%3D.EMqQGO%2BxY4ZS%2Bngd%2BwchYDhgmol4ge9oIVCo1THPgWM%3D" rel="nofollow">animate.css</a>来为移除消息提示框添加对应的动画效果,如果页面当中引入了该样式文件,则可以传入相应的类名,如:</p><pre><code class="ts">// 假设页面引入了animate.css文件
const msg = ewMessage({
content: '这是一个默认的消息提示框',
removeClassName: 'animate__animated animate__bounce'
});</code></pre><h4>removeClassNameSymbol(0.0.9 新增)</h4><p>用于指定多个 removeClassName 的分隔符,如果 removeClassName 指定的是单个类名或者是多个以空白隔开的类名,则不需要传入该值。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
removeClassName: 'fadeOut+scaleDown' // 或者也可以传入ew-message-fadeOut ew-message-scaleDown
removeClassNameSymbol: '+'
// 这里是使用+隔开
});</code></pre><h4>messageZIndex(0.1.0 新增)</h4><p>用于自定义消息提示框的显示层级,是一个 number 值,用于覆盖默认得到 z-index:1000,无默认值。如:</p><pre><code class="ts">const msg = ewMessage({
content: '这是一个默认的消息提示框',
messageZIndex: 2000
});
// 设置消息提示框的显示层级为2000</code></pre><h2>实例方法接口</h2><p>除了属性接口之外,我们也可以使用插件的一些实例方法,总结如下。</p><h3>实例属性</h3><h4>options</h4><p>消息提示框的配置对象。如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');
console.log(msg.options); // { content:"这是一个默认的消息提示框",... }</code></pre><h4>el</h4><p>消息提示框的根元素。</p><h4>closeBtnEl</h4><p>消息提示框的关闭元素。</p><h3>实例方法</h3><h4>destroy (0.0.6 版本新增)</h4><p>该方法将会立即销毁消息提示框实例。如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');
msg.destroy(); // 页面将不再看到消息提示框</code></pre><h4>validateHasStyle</h4><p>该方法验证消息提示框是否含有样式,如果通过 link 标签导入样式,则该方法返回 true,否则返回 false。如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');
msg.validateHasStyle(); // 假设页面引入了消息提示框的样式,则返回true</code></pre><h4>normalizeOptions</h4><p>该方法规范化传入的参数,并和默认配置对象合并,最终返回一个消息提示框的配置对象。</p><h4>getMessageType</h4><p>该方法返回消息提示框支持的类型值。</p><h4>getDefaultOption</h4><p>该方法用于获取消息提示框的默认配置对象。</p><h4>addMessageStyle</h4><p>该方法用于添加消息提示框的样式,如果传入了第二个参数,将会将第二个参数的样式作为最终样式添加到页面中,两个参数都是一个字符串。不同的是,第一个参数代表样式前缀名,第二个参数代表一个样式字符串。如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');
msg.addMessageStyle('ew-', `body { background: #2396ef; }`); // 第一个参数实际上就没有什么作用了,页面body元素的背景是蓝色
msg.addMessageStyle('el-'); // 会添加消息提示框的默认样式,但类名前缀是el-</code></pre><h4>render</h4><p>这个方法就是渲染消息提示框的核心方法,不需要使用,感兴趣可参看<a href="https://link.segmentfault.com/?enc=qN4PGkMTHykvY%2F8rPbkAzQ%3D%3D.vgYj41Lbygv2T%2Bx7RRgGTlmor7RYCsJZuylkHiZUj7S4cqkDlwAE%2BKYYhFMgDj8mjxxq%2F%2BZBJFPnlL8IVfovNfCdxL9FN8HLlTVIeANY4do%3D" rel="nofollow">源码</a>。</p><h4>create</h4><p>该方法表示创建一个消息提示框的元素,返回消息提示框的根元素,不需要使用,感兴趣可参看<a href="https://link.segmentfault.com/?enc=erzSOXFCVFOQSkIAnwxkEA%3D%3D.MmtkgR6dQcyXgoTC8TqIcDvdUGbja9U2cCmy3t%2Bz041af1uV89iRzlkJjzR%2Fcjug3QQtjmpYUKRZ5srjDnmbl5oijltjgZTYlvV42xQd%2Bu4%3D" rel="nofollow">源码</a>。</p><h4>setTop</h4><p>该方法用于设置消息提示框的 top 偏移量,不需要使用,感兴趣可参看<a href="https://link.segmentfault.com/?enc=M0E4WemXj5obifV9EbVElQ%3D%3D.NGu88kOPF%2FIH9fXCafjoYfe7emcq0mmMUzYXYtbBf9TdO%2Fw1oburjW6g1CJTHRbNA5GTyitd7mpzZSAPFikAfQAyNNzw5DCRZEsRzaAPIOE%3D" rel="nofollow">源码</a>。</p><h4>close</h4><p>该方法用于销毁一个 dom 元素,传入 2 个参数,第一个参数是一个 dom 元素或者是 dom 元素集合,第二个参数是销毁的时间,是一个数值。如:</p><pre><code class="ts">const msg = ewMessage('这是一个默认的消息提示框');
msg.close(msg.el, 1000); // 将在1s后移除元素</code></pre><h2>内置工具函数接口</h2><p>插件内部提供了一些工具函数可供调用,该工具函数定义在 ewMessage 的静态属性 util 上,我们可以通过如下代码来获取:</p><pre><code class="ts">const util = ewMessage.util;
// 以下的util均指这里</code></pre><h3>isFunction</h3><p>该工具函数表示判断是否是一个函数,传入一个需要判断的参数。如:</p><pre><code class="ts">const testFn = function () {
console.log('这是一个函数');
};
util.isFunction(testFn); // true
util.isFunction(null); // false</code></pre><h3>isDom</h3><p>该工具函数表示判断是否是一个 dom 元素,传入一个需要判断参数。如:</p><pre><code class="ts">const div = document.querySelector('#app');
util.isDom(div); // true;
util.isDom(1); // false;</code></pre><h3>isObject</h3><p>该工具函数表示判断是否是一个对象,传入一个需要判断的参数。如:</p><pre><code class="ts">util.isObject({}); // true;
util.isObject([]); // true;
util.isObject(function () {
console.log(1);
}); // false;
util.isObject(111); // false;</code></pre><h3>isString</h3><p>该工具函数表示判断是否是一个字符串,传入一个需要判断的参数。如:</p><pre><code class="ts">util.isString('111'); // true;
util.isString(111); // false</code></pre><h3>isNumber(0.0.7 版本新增)</h3><p>该工具函数表示判断是否是一个数值,传入一个需要判断的参数。如:</p><pre><code class="ts">util.isNumber(123); // true;
util.isNumber(NaN); // false;
util.isNumber('1111'); // false</code></pre><h3>warn</h3><p>该函数表示在控制台打印一些警告信息,传入一个需要打印的信息字符串。如:</p><pre><code class="ts">util.warn('warning: this is not a function');</code></pre><h3>hasOwn</h3><p>该工具函数表示某个属性是否在某个对象上,传入 2 个参数,第一个参数是一个对象,第二个参数是一个属性名。如:</p><pre><code class="ts">const obj = { name: 'eveningwater' };
util.hasOwn(obj, 'name'); // true;
util.hasOwn(obj, 'age'); // false;</code></pre><h3>toArray</h3><p>该工具函数用于将伪数组转换成真正的数组,传入一个需要转换的值。如:</p><pre><code class="ts">// 假设页面有多个<div class="list-item"></div>的元素
const listItems = document.querySelectorAll('.list-item');
util.toArray(listItems); // 一个包含多个div元素的数组</code></pre><h3>$</h3><p>该工具函数用于获取 dom 元素,有两个参数,第一个参数是一个字符串,第二个参数是一个 dom 元素。如:</p><pre><code class="ts">const app = util.$('#app');
util.$('.child', app);</code></pre><h3>$$</h3><p>该工具函数用于获取 dom 元素集合即 NodeList,参数同$方法。如:</p><pre><code class="ts">const app = util.$('#app');
util.$$('.child', app);
util.$$('.child');</code></pre><h3>createElement(0.0.8 新增)</h3><p>该工具函数用于根据模板字符串创建一个 dom 元素,模板字符串可以带入子元素,如:</p><pre><code class="ts">const html = util.createElement(`<div><span></span></div>`);
console.log(html); // 返回div节点</code></pre><h3>addClass(0.0.9 新增)</h3><p>该工具函数用于给 dom 元素添加类名,需要传入 2 个参数,如:</p><pre><code class="ts">const html = util.addClass(`app`, util.$('#app'));
// 给id为app的元素添加app类名</code></pre><h3>off(0.0.9 新增)</h3><p>该方法用于给移除一个事件监听器,传入四个参数,第一个为 DOM 元素,第二个为事件名,第三个参数为事件监听器,第四个参数为一个布尔值。如:</p><pre><code class="ts">const test = document.getElementById('test');
const handler = e => console.log(e.target.tagName);
util.off(test, 'click', handler);</code></pre><h3>on(0.0.9 新增)</h3><p>该方法用于添加一个事件监听器,参数等同 off 方法。如:</p><pre><code class="ts">const test = document.getElementById('test');
const handler = e => console.log(e.target.tagName);
util.on(test, 'click', handler);</code></pre><p>更多详情参阅文档官网介绍<a href="https://link.segmentfault.com/?enc=YcK2Dy5rX6vtdI%2FR0t7U4A%3D%3D.H%2BKHjSIpPCrBzCJCDWJi%2F2q%2FkPOQNYGkuNw0QP2U9cJECW7iiDCE9fF5%2FFvUfmnl" rel="nofollow">ew-message</a>;</p><h2>更新日志</h2><ul><li>0.0.1 \~ 0.0.4: 添加了消息提示框的基本功能。</li><li>0.0.5: 修改了 ts 类型导入。</li><li>0.0.6: 消息提示框添加了销毁 destroy 方法。</li><li>0.0.7: 完善了 ts 类型,新增了最大关闭时间属性 maxDuration,修改了默认关闭时间。</li><li>0.0.8: 新增了 showTypeIcon 和 typeIcon 属性以及 closeIcon,用于配置图标,新增了工具方法 createElement。</li><li>0.0.9: 新增了 container、immediate、removeClassName、removeClassNameSymbol 配置属性,新增了 on,off,addClass 工具方法。</li></ul><p>以下为示例代码:</p><p><a href="https://link.segmentfault.com/?enc=5HRS6eUCREFHV34IQq7bog%3D%3D.iPXiGIh8wcDljla%2BwGJjoPtY3uYgu45NZUQNRPL7QLDWSDKUq0UAw9yIx89ugqD2" rel="nofollow">jcode</a></p>
奇怪,奇怪,真的好奇怪的javascript
https://segmentfault.com/a/1190000044193432
2023-09-07T22:11:25+08:00
2023-09-07T22:11:25+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
3
<h2>奇怪的 javascript</h2><blockquote>ps: 本文的题目解答都是以谷歌浏览器为准。</blockquote><p>本文就从四道题来分析 javascript 的奇怪行为,首先我们来看第一道题,如下所示:</p><h3>题目 1</h3><pre><code class="js">let a = 1;
function fn() {
let a = 2;
// 这里写代码,使得最后的打印是3
}
fn();
console.log(a); // 3</code></pre><p>问题很简单,先使用 let 在全局中定义了一个变量 a,并定义初始值为 1,然后定义了一个函数,在函数的内部又定义了一个同样的变量 a,然后调用这个函数,在调用函数之后打印变量 a,问题就是在函数内部添加一些代码使得最终打印变量 a 的结果是 3。</p><h3>思路分析</h3><p>首先我们知道如果没有特殊的办法,那么最外层的打印将始终打印的是 a 变量最初的值,那就是 1。函数内部如果没有定义 a 变量,那么我们是可以访问到 a 变量的,而有了 a 变量,那么我们只能在函数访问到 2,这就导致我们在函数内部似乎没有任何办法访问到外部的变量 1,因此我们无法修改外部的变量 a。要解决这道题,我们可以从 2 个方向入手,第一个方向就是如何在函数内部访问到外部的变量 a,从而修改变量 a,第二个方向则是从 console.log 函数入手。</p><p>我们先看第一个方向,如何在函数内部访问到外部的变量 a,这听起来似乎很不可思议,但我们确实可以做到,使用 eval 函数就可以了,eval 函数支持传一个字符串当做参数,我想这大多数开发者都知道,但是很少有人知道 eval 函数的调用方式分为直接调用和间接调用,什么是直接调用,什么又是间接调用。我们来看两段代码:</p><pre><code class="js">eval('console.log(1)'); // 直接调用</code></pre><pre><code class="js">(0, eval)('console.log(1)'); // 间接调用</code></pre><p>看了以上代码我们就知道了,直接调用就是直接调用 eval 方法即可,而间接调用,是让 eval 函数像自调用函数(当然这里并不是自调用函数)那样去调用,这里用到了逗号操作符,逗号操作符可以用来在一条语句中执行多个操作,如下所示:</p><pre><code class="js">let num1 = 1,
num2 = 2,
num3 = 3;</code></pre><p>不过,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:</p><pre><code class="js">let num = (5, 1, 4, 8, 0); // num 的值为 0</code></pre><p>因此这里的逗号操作符前面的第一个操作数 0 其实没有什么意义,用 1 也可以,2 也行,这里我们主要搞清楚间接调用和直接调用的区别就行了,直接调用 eval 执行的环境就是当下的环境,那么访问到的也就是当下环境中的变量,而间接调用可以让 eval 执行的环境暴露在全局环境中。比如:</p><pre><code class="js">let a = 1;
function fn() {
let a = 2;
console.log(eval('a')); // 2
}
fn();</code></pre><pre><code class="js">let a = 1;
function fn() {
let a = 2;
console.log((0, eval)('a')); // 1
}
fn();</code></pre><p>看了以上两段代码的结果就不难看出直接调用和间接调用的区别了,有了间接调用,我们就可以修改在全局环境下的 a 变量,这样也就能解答本题了。如下:</p><pre><code class="js">let a = 1;
function fn() {
let a = 2;
(0, eval)('a+2'); // 或者 (0,eval)('a = 3')
}
fn();
console.log(a); // 3</code></pre><p>有了 eval 函数,那么我们同样想到了可以使用 Function 来模拟 eval 函数的功能,如下所示;</p><pre><code class="js">const equalEval = str => new Function('return ' + str)();</code></pre><p>因此以上的代码还可以这么解答:</p><pre><code class="js">const equalEval = str => new Function('return ' + str)();
let a = 1;
function fn() {
let a = 2;
equalEval('a+2'); // 或者 equalEval('a = 3')
}
fn();
console.log(a); // 3</code></pre><p>以上是我们说的第一个方向,接下来我们来谈谈第二个方向,那就是修改 console.log 函数,很简单,如下所示:</p><pre><code class="js">let a = 1;
function fn() {
let a = 2;
const originLog = console.log;
console.log = v => {
originLog(v + 2);
};
}
fn();
console.log(a); // 3</code></pre><p>可以看到我们使用一个变量缓存 console.log 方法,然后改写 console.log,让最终的打印值加 2 就可以得到 3。</p><p>javascript 是真的好奇怪,这些莫名其妙的特性总是让人难以理解,并烦恼,正常谁会想到这样的解答?</p><h3>题目 2</h3><pre><code class="js">window.eval = () => {
throw new Error('eval is not allowed');
};
window.Function = () => {
throw new Error('Function is not allowed');
};
//这里写代码使得后续的打印返回注释的结果
console.log(eval); // eval(){[native code]}
console.log(eval('1 + 2')); // 3</code></pre><h3>思路分析</h3><p>正如第一题那样,我们改写了 javascript 的 console.log 方法,这第 2 题由于改写了 javascript 的 eval 和 Function 方法,让我们想要恢复原来的方法就显得比较困难。因此这道题的办法就是怎么样才能够恢复 eval 方法,这时候我们就可以想到内联框架,内联框架也有一个 window 对象,说明同样也有 eval 方法,因此我们可以获取到内联框架的 eval 方法然后恢复 eval 方法的定义,如下所示:</p><pre><code class="js">window.eval = () => {
throw new Error('eval is not allowed');
};
window.Function = () => {
throw new Error('Function is not allowed');
};
const iframe = document.createElement('iframe'); // 创建一个iframe对象
document.body.appendChild(iframe); //注意一定要把iframe添加到dom中
const eval = iframe.contentWindow.eval; //获取内联框架下的eval函数
document.body.removeChild(iframe); // 获取到了之后从dom中移除iframe元素
console.log(eval); // eval(){[native code]}
console.log(eval('1 + 2')); // 3</code></pre><p>如此一来,本题就轻松的解决了。</p><p>javascript 就是这么奇怪,居然允许我们修改内置函数的定义,你就说它奇不奇怪?</p><h3>题目 3</h3><pre><code class="js">let a; // a = ? 这里写代码使得后续的打印返回注释的结果;
if (!a) {
console.log(a + 1); // 2
}</code></pre><h3>思路分析</h3><p>这道题也是很有意思的,这道题的难点在于既要满足条件是 false,又要满足 a 变量经过转换后的值一定是 1,否则不可能得到结果为 2。如果大家可以想到 valueOf 这个方法,离解答这道题就不远了,valueOf 方法返回任意数据的原始值,也就是说,如果我们修改变量 a 的原始值,那么 a 最终会以原始值参与 + 1 的计算,然后得到 2。也就是说,我们只需要这样:</p><pre><code class="js">a.valueOf = () => 1; // 将a的原始值设置为1</code></pre><p>也许有人说,那好,这里的 a = 0 即可,记住这里的 a 不能为原始数据类型,因为原始数据类型的原始值就相当于是它本身调用 Number 方法得到的一个数字 0,也就是说:</p><pre><code class="js">let a = false; // 或者 a = '' a = 0</code></pre><p>以上都是错误的写法,并不能得到 a 的原始值为 1,因此我们需要将 a 设置为对象,而能够是 false 值的对象只有 document.all,它的返回值在除 ie 浏览器上都是 false,因此也就满足了既是 false,又修改原始值,能够让变量 a 读取到修改后的原始值。因此最终的解答就是:</p><pre><code class="js">let a = document.all;
a.valueOf = () => 1;
if (!a) {
console.log(a + 1); // 2
}</code></pre><p>那么问题来了,谁会想到 document.all 的返回值是 false?javascript 好奇怪,明明这里是返回 document 的整个集合,为什么在谷歌浏览器上将它转成布尔值的时候,是 false 而不是 true。</p><h3>题目 4</h3><pre><code class="js">let a; // a = ? 这里写代码使得后续的打印返回注释的结果;
console.log(typeof a); // number
console.log(1 + a === 1); // false
console.log(2 + a === 2); // true</code></pre><h3>思路分析</h3><p>这道题咋一看,有这样的数字吗?既要满足 1 + a = 1,又要满足 2 + a = 2,还别说,翻阅了文档,还真能找到这个数字,这个数字就是 Number.EPSILON,这是一个啥玩意儿,估计很少有人知道。mdn 文档上是这样说的:</p><p>Number.EPSILON 属性表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。EPSILON 属性的值接近于 2.2204460492503130808472633361816E-16,或者 2^-52。这玩意儿加 1 的结果是:1.0000000000000002,加 2 的结果就是 2,你就说它奇不奇怪。因此本题的答案就是:</p><pre><code class="js">let a = Number.EPSILON; // 或者 let a = 2.2204460492503130808472633361816E-16;或者let a = Math.pow(2,-52)
console.log(typeof a); // number
console.log(1 + a === 1); // false
console.log(2 + a === 2); // true</code></pre><p>从以上的四道题,我们可以看到 javascript 的奇怪之处,你就说 javascript 奇不奇怪?</p><blockquote>ps: 如果各位大佬还有这四道题的其它答案,欢迎评论区留言。</blockquote>
作兔器——手写一个可爱的兔兔相册展示器
https://segmentfault.com/a/1190000044189531
2023-09-06T20:52:58+08:00
2023-09-06T20:52:58+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
1
<h2>作兔器——手写一个可爱的兔兔相册展示器</h2><p>兔岁既来,为迎兔岁,更以为兔兔甚是可怜,故作一兔相册展示器,以藏兔图。而今先顾此效,如下所示:</p><p><img width="723" height="554" src="/img/bVc9zSv" alt="" title=""></p><blockquote>自译: 兔年既然来了,为了表示欢迎兔年的到来,而且我也认为兔兔很是可爱,令人喜欢,因此写了一个兔兔相册展示器,用来收藏可爱的兔兔图片,现在让我们先来看看这个相册展示器的效果吧,正如下面所展示的:</blockquote><p>据上而知,此器可分三成,一则为头部,头部含一按钮,点此可随意见兔之图,二则为图区,图区不限,此例乃九,每一区见一兔,鼠标移之可得一词以述兔也,所述则同分为二,其一乃题,其二乃文,文即一词。</p><blockquote>自译: 根据上图我们可以知道,这个兔兔相册展示器可以分成三个部分,第一个部分就是一个标题栏头部,标题栏头部有一个按钮,点击这个按钮可以随机的展现兔兔的图片,第二个部分则是兔兔的图片展示区域,这个展示区域可以分成多个部分,在这个例子当中是九个图区,每一个图区就会展示一个兔兔,鼠标移上去可以看到一首词来描述兔兔。这个描述同样分成了两部分,第一部分就是标题,第二部分就是内容,内容就是一首诗。</blockquote><p>三则为细品兔图,鼠标击之,可得一黑盒,黑盒中位,即为兔图,鼠标复击之,即可关之。由之可见,其有动效,如何实之?需有二者,其一为定时器,其二乃以透明度为效,如是娓娓道来。</p><blockquote>自译: 第三个部分则是详细的预览图片,通过使用鼠标点击图片,就可以得到一个黑色的背景遮罩,在黑色背景遮罩的中间位置,就是兔兔的图片,鼠标再次点击就可以关闭详细预览。从这里我们可以看到,这个关闭和打开都是有动画效果的,那么我们如何实现这个动画效果呢?第一部分很显然我们需要用到定时器,第二部分就是通过修改透明度达到效果,接下来让我们一一来看每一部分的实现吧。</blockquote><p>其一,构之 HTML 元素:</p><blockquote>自译: 第一步,当然是编写 html 元素。</blockquote><pre><code class="html"><div class="rabbit-box">
<header class="rabbit-box-header">
<button type="button" class="rabbit-box-header-btn">换一换</button>
</header>
<div class="rabbit-box-album"></div>
</div></code></pre><p>如上所示,应知外有容器元素,内有标题元素与兔图容器元素,其内更有一按钮元素,使之可更兔图,兔图容器之内,乃元素抽成,如之奈何?</p><blockquote>自译: 正如上面所展示的,我们应该知道最外层有一个容器元素,里面又有一个标题元素以及一个装满兔子图片的容器元素,在标题元素中还有一个按钮元素,可以让它来随机更换兔子图片,兔子图片内部的元素正好是动态生成的元素,我们应该怎么做呢?</blockquote><p>使我等估之如下:</p><blockquote>自译: 让我们猜测 html 元素结构应该如下所示:</blockquote><pre><code class="html"><div class="rabbit-box-album-item" data-title="..." data-content="...">
<img src="..." alt="图片加载中" class="rabbit-box-album-item-img" />
</div></code></pre><p>甚易,构之大成,因而画之美样,但其乃易,故可径直赏码矣。</p><blockquote>自译: 太简单了,构建元素大功告成,因此我们只需要用 css 样式美化一下即可,不过因为这也同样简单,因此我们只需要直接查看代码即可。</blockquote><pre><code class="css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
height: 100%;
overflow: hidden;
}
body {
overflow-y: auto;
padding: 2em;
background: linear-gradient(135deg, #f184e8 10%, #e209e9 90%);
}
.rabbit-box {
width: 100%;
max-width: 1200px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: auto;
}
.rabbit-box-header {
width: 100%;
background: linear-gradient(135deg, #ffc5e7 10%, #fa6cc4 90%);
height: 60px;
border-radius: 10px 10px 0 0;
text-align: center;
line-height: 60px;
}
.rabbit-box-header-btn {
outline: none;
border-radius: 4px;
padding: 0.4rem 1rem;
border: none;
transition: all 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
display: inline-block;
color: #fff;
cursor: pointer;
background: linear-gradient(135deg, #f173bd 10%, #e71b92 90%);
letter-spacing: 2px;
font-size: 20px;
font-family: '微软雅黑', '楷体';
}
.rabbit-box-header-btn:hover,
.rabbit-box-header-btn:active {
background: linear-gradient(135deg, #f060b4 10%, #e60f8c 90%);
}
.rabbit-box-album {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
}
.rabbit-box-album-item {
width: 33.3333%;
height: 300px;
overflow: hidden;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.5);
cursor: pointer;
position: relative;
}
.rabbit-box-album-item::before,
.rabbit-box-album-item::after {
position: absolute;
color: #fff;
font-size: 15px;
width: 100%;
height: 50%;
display: flex;
justify-content: center;
align-items: center;
left: 0;
transition: transform 0.4s ease-in-out;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.rabbit-box-album-item::before {
content: attr(data-title);
top: 0;
background-color: rgba(0, 0, 0, 0.85);
transform: translateY(-100%) scale(0);
}
.rabbit-box-album-item::after {
content: attr(data-content);
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
border-top: 1px solid #fff;
transform: translateY(100%) scale(0);
}
.rabbit-box-album-item:hover.rabbit-box-album-item::before,
.rabbit-box-album-item:hover.rabbit-box-album-item::after {
transform: translateY(0) scale(1);
}
.rabbit-box-album-item-img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
@media screen and (max-width: 900px) {
.rabbit-box-album-item {
width: 100%;
height: 200px;
}
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
width: 4px;
height: 4px;
background-color: #ee4f9e;
}</code></pre><p>甚易,皆凡样也,故无需多言以述之,使我等望功码。</p><blockquote>自译: 太简单了,都是一些常规的布局样式,因此没有必要说太多来解释,让我们来看功能代码的实现吧。</blockquote><p>其一,有一构造函数,函数一对参,参内二属,一为 resources,二为 el,resources 乃对组,对内有三属,分为 src,title,content,顾名思义,src 为兔图之源,title 为标题,content 为内容,el 则为容器元素,值乃字符,顾可得如下:</p><blockquote>自译: 首先我们会创建一个构造函数,构造函数有一个对象参数,对象参数里面是二个属性,第一个是 resources,第二个则是 el,resources 为一个对象数组,对象里面分别有三个属性,分别是 src,title,content,从名字就能想到其中的含义,src 代表兔兔图片的路径,title 就是标题,content 就是内容,el 则代表是容器元素,是一个字符串,因此我们就可以得到如下代码:</blockquote><pre><code class="js">const resources = [
{
src:"./images/rabbit-1.png",
title:"可爱的兔兔-1",
content:"雄兔脚扑朔,雌兔眼迷离。"
},
{
src:"./images/rabbit-2.png",
title:"可爱的兔兔-2",
content:"兔走乌驰人语静,满溪红袂棹歌初。"
},
{
src:"./images/rabbit-3.png",
title:"可爱的兔兔-3",
content:"白兔捣药成,问言与谁餐?"
},
{
src:"./images/rabbit-4.png",
title:"可爱的兔兔-4",
content:"兔丝附蓬麻,引蔓故不长。"
},
{
src:"./images/rabbit-5.png",
title:"可爱的兔兔-5",
content:"白兔捣药秋复春,嫦娥孤栖与谁邻。"
},
{
src:"./images/rabbit-6.png",
title:"可爱的兔兔-6",
content:"吴质不眠倚桂树,露脚斜飞湿寒兔。"
},
{
src:"./images/rabbit-7.png",
title:"可爱的兔兔-7",
content:"茕茕白兔,东走西顾。"
},
{
src:"./images/rabbit-8.png",
title:"可爱的兔兔-8",
content:"可笑常娥不了事,走却玉兔来人间。"
},
{
src:"./images/rabbit-9.png",
title:"可爱的兔兔-9",
content:"兔走乌驰人语静,满溪红袂棹歌初。"
}
];
const rabbit = new RabbitAlbum({
resources: ,
el:".rabbit-box-album"
})</code></pre><p>使我等见构造函数如下:</p><blockquote>自译: 让我们来看看构造函数的实现如下:</blockquote><pre><code class="js">class RabbitAlbum {
constructor(options) {
this.animation = Object.create(null);
this.initAnimation();
this.container = RabbitAlbum.$(options.el) || document.body;
this.imageList = options.resources || [];
this.createAlbum();
}
}</code></pre><p>如上初始所属,内有二方,其一为 initAnimation,意即初始动效,其二为 createAlbum,意即生兔图也,三属分为 animation,container,imgList,即动效,容器元素,兔图对组。</p><blockquote>自译: 以上我们初始化所有的属性,里面有二个方法,第一个方法就是 initAnimation,意思就是初始化动画,第二个方法就是 createAlbum,意思就是生成兔子图片,三个属性分别是 animation,container,imgList,也就是动画效果,容器元素,兔兔图片对象数组。</blockquote><p>有一$,乃是静方,使我等见之如下:</p><blockquote>自译: 有一个$方法,就是一个静态方法,让我们来看看如下所示:</blockquote><pre><code class="js">RabbitAlbum.$ = (selector, el = document) => el.querySelector(selector);</code></pre><p>甚易,其乃 document.querySelector 方包耳!。另有二静方,为 getCss,\_create,使我等见之如下:</p><blockquote>自译: 太简单了,就是 document.querySelector 方法的包装罢了。另外还有二个静态方法,那就是 getCss,\_create,让我们来看看如下所示:</blockquote><pre><code class="js">RabbitAlbum.getCss = (el, prop) => window.getComputedStyle(el, null)[prop];
RabbitAlbum._create = name => document.createElement(name);</code></pre><p>getCss 为 window.getComputedStyle 方包耳!,\_create 为 document.createElement 方包耳!甚易,如视 createAlbum 方,后见 initAnimation 方。</p><blockquote>自译: getCss 就是 window.getComputedStyle 方法的包装罢了,\_create 就是 document.createElement 方法的包装罢了,接下来我们先来看 createAlbum 方法,最后再看 initAnimation 的实现。</blockquote><pre><code class="js">createAlbum(){
const randomSort = this.imageList.sort((a,b) => Math.random() > 0.5 ? -1 : 1);
let template = '';
randomSort.forEach(item => {
template += `
<div class="rabbit-box-album-item" data-title="${item.title}" data-content="${item.content}">
<img src="${item.src}" alt="图片加载中" class="rabbit-box-album-item-img">
</div>
`;
});
this.container.innerHTML = template;
this.container.addEventListener('click',e => {
if(e.target.className.includes('rabbit-box-album-item')){
if(e.target.firstElementChild){
const src = e.target.firstElementChild.getAttribute('src');
this.preview(src);
}
}
})
}</code></pre><p>其一,乱序兔图对组,编之生兔图元素,入 container 为子元素,为 container 元素增击事,断之乃图元素,而取 src 属,调 preview 方也。</p><blockquote>自译: 首先将兔图对象数组打乱顺序,遍历这个数组生成兔图元素,添加到 container 容器元素中作为子元素,然后给 container 元素添加点击事件,判断是否是图片元素,然后获取 src 属性,调用 preview 方法。</blockquote><p>使我等见 preview 方如下:</p><blockquote>自译: 让我们来看看 preview 方法如下:</blockquote><pre><code class="js">preview(src,time = 600){
const imgTag = RabbitAlbum._create('img'),
imgMask = RabbitAlbum._create('div');
imgMask.style.cssText += 'position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;';
imgTag.style.cssText += 'position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:auto;height:auto;border-radius:5px;';
imgMask.id = 'previewMask';
imgTag.src = src;
imgMask.appendChild(imgTag);
const mask = RabbitAlbum.$("#previewMask");
if (!mask) {
document.body.appendChild(imgMask);
this.animation.fadeIn(imgMask, time);
} else {
document.body.replaceChild(imgMask, mask);
this.animation.fadeIn(imgMask, time);
}
imgMask.addEventListener('click', e => {
const el = e.target.tagName.toLowerCase().indexOf('img') > -1 ? e.target.parentElement : e.target;
this.animation.fadeOut(el, time);
}, false)
}</code></pre><p>其一生 img 元素与 div 元素,分为增样,后入 body 元素,调 fadeIn 方见之,为 div 元素增击事,因而隐之。</p><blockquote>自译: 第一步生成 img 和 div 元素,分别添加样式,然后添加到 body 元素中,然后调用 fadeIn 方法显示它们,并且为 div 元素添加点击事件,从而达到隐藏它们。</blockquote><p>何知 fadeIn 方与 fadeOut 方焉? 如之奈何?使我等观之。其一生时管器,如下:</p><blockquote>自译: 是不是有些熟悉 fadeIn 方法和 fadeOut 方法?如何实现呢?让我们一起来看看吧,首先是需要创建一个时间管理器,如下:</blockquote><pre><code class="js">class TimerManager {
constructor() {
this.timers = [];
this.args = [];
this.isTimerRun = false;
}
add(timer, args) {
this.timers.push(timer);
this.args.push(args);
this.timerRun();
}
timerRun() {
if (!this.isTimerRun) {
const timer = this.timers.shift(),
args = this.args.shift();
if (timer && args) {
this.isTimerRun = true;
timer(args[0], args[1]);
}
}
}
next() {
this.isTimerRun = false;
this.timerRun();
}
}
TimerManager.makeTimerManage = element => {
if (
!element.TimerManage ||
element.TimerManage.constructor !== TimerManager
) {
element.TimerManage = new TimerManager();
}
};
TimerManager.runNext = element => {
if (element.TimerManage && element.TimerManage.constructor === TimerManager) {
element.TimerManage.next();
}
};</code></pre><p>其效如何? 生组而存定时器,后一一取出调之,甚易,使我等观 fadeIn 方与 fadeOut 方,二者所似,知其一便知其二也,皆以定时器增减 opacity 矣。</p><blockquote>自译: 这个时间管理器的用处是什么呢?也就是创建一个数组存储定时器,然后每次都从这个数组当中取出一个,然后调用它,这很简单,让我们来说看看 fadeIn 与 fadeOut 方法的实现。两个方法的实现原理都很类似,知道其中一个方法的实现便知道另一个方法的实现,它们都是通过定时器增加或者是减少 opacity 即可。</blockquote><pre><code class="js">fadeIn(element,time){
element.style.transition = "opacity" + time + " ms";
if (!Number(RabbitAlbum.getCss(element, 'opacity')) || !parseInt(RabbitAlbum.getCss(element, 'opacity')) <= 0) {
element.style.display = "none";
element.style.opacity = 0;
let curAlpha = 0,
addAlpha = 1 * 100 / (time / 10),
timer = null;
let handleFade = function () {
curAlpha += addAlpha;
if (element.style.display === 'none'){
element.style.display = "block";
}
element.style.opacity = (curAlpha / 100).toFixed(2);
if (curAlpha >= 100) {
if (timer) clearTimeout(timer);
element.style.opacity = 1;
TimerManager.runNext(element);
} else {
timer = setTimeout(handleFade, 10);
}
}
handleFade();
} else {
TimerManager.runNext(element);
}
}</code></pre><p>甚易,后使我等观 initAnimation 方,如下:</p><blockquote>很简单,最后让我们来看看 initAnimation 方法 的实现,如下所示:</blockquote><pre><code class="js">initAnimation(){
const _self = this;
if(typeof this.animation['fadeIn'] !== 'function'){
this.animation['fadeIn'] = function(element,time = 400){
TimerManager.makeTimerManage(element);
element.TimerManage.add(_self.fadeIn, arguments);
return this;
}
}
if(typeof this.animation['fadeOut'] !== 'function'){
this.animation['fadeOut'] = function(element,time = 400){
TimerManager.makeTimerManage(element);
element.TimerManage.add(_self.fadeOut, arguments);
return this;
}
}
}</code></pre><p>甚易,初始 fadeIn 方与 fadeOut 方,后入元素矣。</p><blockquote>自译: 很简单,就是初始化 fadeIn 和 fadeOut 方法,把它们添加到元素当中就行了。</blockquote><p>综上,可得作兔器也,使我等观之。</p><blockquote>自译: 综上所述,我们的兔兔相册管理器就实现了,让我们来看看最后的效果吧。</blockquote><p><a href="https://link.segmentfault.com/?enc=Jwm7BPNI9AS23pzVSEsMPg%3D%3D.uB7H62YjO6MDSBuGzfuHrZR7iqSeXz0De3nVX18TUSKc%2BK6lHHGNr0rdPMJdwLFq" rel="nofollow">在线示例</a></p>
强大的css计数器
https://segmentfault.com/a/1190000044185526
2023-09-05T22:37:20+08:00
2023-09-05T22:37:20+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
4
<h2>强大的 css 计数器</h2><p>css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。</p><p>css 计数器主要有 3 个关键点需要掌握。如下:</p><ol><li>首先需要一个计数器的名字,这个名字由使用者自己定义。</li><li>计数器有一个计数规则,比如是 1,2,3,4...这样的递增方式,还是 1,2,1,2...这样的连续递增方式。</li><li>计数器的使用,即定义好了一个计数器名字和计数规则,我们就需要去使用它。</li></ol><p>以上 3 个关键点分别对应的就是 css 计数器的 counter-reset 属性,counter-increment 属性,和 counter()/counters()方法。下面我们依次来介绍这三个玩意儿。</p><h3>counter-reset 属性</h3><p>counter-reset 属性叫做计数器重置,对应的就是创建一个计数器名字,如果可以,顺便也可以告诉计数器的计数起始值,也就是从哪个值开始计数,默认值是 0,注意是 0,而不是 1。例如以下一个示例:</p><p>html 代码如下:</p><pre><code class="html"><p>开始计数,计数器名叫counter</p>
<p class="counter"></p></code></pre><p>css 代码如下:</p><pre><code class="css">.counter {
counter-reset: counter;
}
.counter::before {
content: counter(counter);
}</code></pre><p>在浏览器中运行以上示例,你会看到如下图所示:</p><p><img width="330" height="158" src="/img/bVc9yPG" alt="" title=""></p><p>可以看到计数器的初始值就是 0,现在我们修改一下 css 代码,如下所示:</p><pre><code class="css">.counter {
counter-reset: counter 1;
}</code></pre><p>在浏览器中运行以上示例,你会看到如下图所示:</p><p><img width="360" height="122" src="/img/bVc9yPH" alt="" title=""></p><p>这次我们指定了计数器的初始值 1,所以结果就是 1,计数器的初始值同样也可以指定成小数,负数,如-2,2.99 之类,只不过 IE 和 FireFox 浏览器都会认为是不合法的数值,当做默认值 0 来处理,谷歌浏览器也会直接显示负数,如下图所示:</p><p><img width="317" height="126" src="/img/bVc9yPI" alt="" title=""></p><p>低版本谷歌浏览器处理小数的时候是向下取整,比如 2.99 则显示 2,最新版本则当成默认值 0,来处理,如下图所示:</p><p><img width="294" height="126" src="/img/bVc9yPJ" alt="" title=""></p><blockquote>ps: 当然不推荐指定初始值为负数或者小数。</blockquote><p>你以为到这里就完了吗?还没有,计数器还可以指定多个,每一个计数器之间用空格隔开,比如以下代码:</p><pre><code class="css">.counter {
counter-reset: counter1 1 counter2 2;
}
.counter::before {
content: counter(counter1) counter(counter2);
}</code></pre><p>在浏览器中运行以上示例,你会看到如下图所示:</p><p><img width="293" height="155" src="/img/bVc9yPK" alt="" title=""></p><p>除此之外,计数器名还可以指定为 none 和 inherit,也就是取消计数和继承计数器,这没什么好说的。</p><h3>counter-increment</h3><p>顾名思义,该属性就是计数器递增的意思,也就是定义计数器的计数规则,值为计数器的名字,可以是一个或者多个,并且也可以指定一个数字,表示计数器每次变化的数字,如果不指定,默认就按照 1 来变化。比如以下代码:</p><pre><code class="css">.counter {
counter-reset: counter 1;
counter-increment: counter;
}</code></pre><p>得到的结果就是: 1 + 1 = 2。如下图所示:</p><p><img width="326" height="133" src="/img/bVc9yPL" alt="" title=""></p><p>再比如以下代码:</p><pre><code class="css">.counter {
counter-reset: counter 2;
counter-increment: counter 3;
}</code></pre><p>得到的结果就是: 2 + 3 = 5,如下图所示:</p><p><img width="344" height="128" src="/img/bVc9yPM" alt="" title=""></p><p>由此可见,计数器的规则就是: 计数器名字唯一,每指定一次计数规则,计数器就会加一,每指定二次计数规则,计数器就会加二,……以此类推。</p><p>计数规则不仅可以创建在元素上,也可以创建在使用计数器的元素上,比如以下代码:</p><pre><code class="css">.counter {
counter-reset: counter;
counter-increment: counter;
}
.counter::before {
content: counter(counter);
counter-increment: counter;
}</code></pre><p>我们不仅在类名为 counter 元素上创建了一个计数器规则,同样的也在 before 伪元素上创建了一个计数器规则,因此最后的结果就是: 0 + 1 + 1 = 2。如下图所示:</p><p><img width="306" height="126" src="/img/bVc9yPN" alt="" title=""></p><p>总而言之,无论位置在何处,只要有 counter-increment,对应的计数器的值就会变化, counter()只是输出而已!计数器的数值变化遵循 HTML 渲染顺序,遇到一个 increment 计数器就变化,什么时候 counter 输出就输出此时的计数值。</p><p>除此之外,计数器规则也可以和计数器一样,创建多个计数规则,也是以空格区分,比如以下示例代码:</p><pre><code class="css">.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 2 counter2 3;
}
.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 4 counter2 5;
}</code></pre><p>此时的结果就应该是计数器 1: 1 + 2 + 4 = 7,计数器 2: 2 + 3 + 5 = 10。如下图所示:</p><p><img width="379" height="140" src="/img/bVc9yPO" alt="" title=""></p><p>同样的,计数器规则的值也可以是负数,也就是递减效果了,比如以下代码:</p><pre><code class="css">.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 -1 counter2 -3;
}
.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 2 counter2 5;
}</code></pre><p>此时的结果就应该是计数器 1: 1 - 1 + 2 = 2,计数器 2: 2 - 3 + 5 = 4。如下图所示:</p><p><img width="367" height="125" src="/img/bVc9yPP" alt="" title=""></p><p>同样的计数规则的值也可以是 none 或者 inherit。</p><h3>counter</h3><p>counter 方法类似于 calc,主要用于定义计数器的显示输出,到目前为止,我们前面的示例都是最简单的输出,也就是如下语法:</p><pre><code class="css">counter(name); /* name为计数器名 */</code></pre><p>实际上还有如下的语法:</p><pre><code class="css">counter(name,style);</code></pre><p>style 参数和 list-style-type 的值一样,意思就是不仅可以显示数字,还可以显示罗马数字,中文字符,英文字母等等,值如下:</p><pre><code class="css">list-style-type: disc | circle | square | decimal | lower-roman | upper-roman |
lower-alpha | upper-alpha | none | armenian | cjk-ideographic | georgian |
lower-greek | hebrew | hiragana | hiragana-iroha | katakana | katakana-iroha |
lower-latin | upper-latin | simp-chinese-informal;</code></pre><p>比如以下的示例代码:</p><pre><code class="css">.counter {
counter-reset: counter;
counter-increment: counter;
}
.counter::before {
content: counter(counter, lower-roman);
}</code></pre><p>结果如下图所示:</p><p><img width="307" height="113" src="/img/bVc9yPQ" alt="" title=""></p><p>再比如以下的示例代码:</p><pre><code class="css">.counter {
counter-reset: counter;
counter-increment: counter;
}
.counter::before {
content: counter(counter, simp-chinese-informal);
}</code></pre><p>结果如下图所示:</p><p><img width="464" height="196" src="/img/bVc9yPT" alt="" title=""></p><p>同样的 counter 也可以支持级联,也就是说,一个 content 属性值可以有多个 counter 方法,如:</p><pre><code class="css">.counter {
counter-reset: counter;
counter-increment: counter;
}
.counter::before {
content: counter(counter) '.' counter(counter);
}</code></pre><p>结果如下图所示:</p><p><img width="332" height="143" src="/img/bVc9yPU" alt="" title=""></p><h3>counters</h3><p>counters 方法虽然只是比 counter 多了一个 s 字母,但是含义可不一样,counters 就是用来嵌套计数器的,什么意思了?我们平时如果显示列表符号,不可能只是单单显示 1,2,3,4...还有可能显示 1.1,1.2,1.3...前者是 counter 做的事情,后者就是 counters 干的事情。</p><p>counters 的语法为:</p><pre><code class="css">counters(name, string);</code></pre><p>name 就是计数器名字,而第二个参数 string 就是分隔字符串,比如以'.'分隔,那 string 的值就是'.',以'-'分隔,那 string 的值就是'-'。来看如下一个示例:</p><p>html 代码如下:</p><pre><code class="html"><div class="reset">
<div class="counter">
javascript框架
<div class="reset">
<div class="counter">&nbsp;angular</div>
<div class="counter">&nbsp;react</div>
<div class="counter">
vue
<div class="reset">
<div class="counter">
vue语法糖
<div class="reset">
<div class="counter">&nbsp;@</div>
<div class="counter">&nbsp;v-</div>
<div class="counter">&nbsp;:</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.reset {
counter-reset: counter;
padding-left: 20px;
}
.counter::before {
content: counters(counter, '-') '.';
counter-increment: counter;
}</code></pre><p>结果如下图所示:</p><p><img width="415" height="273" src="/img/bVc9yPV" alt="" title=""></p><p>这种计数效果在模拟书籍的目录效果时非常实用,比如写文档,会有嵌套标题的情况,还有一个比较重要的点需要说明一下,就是显示 content 计数值的那个 DOM 元素在文档流中的位置一定要在 counter-increment 元素的后面,否则是没有计数效果的。</p><p>总而言之,content 计数器是非常强大的,以上都只是很基础的用法,真正掌握还需要大量的实践以及灵感还有创意。</p>
10 个 效果不错的值得收藏的 css 代码片段
https://segmentfault.com/a/1190000044180853
2023-09-04T21:38:04+08:00
2023-09-04T21:38:04+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
2
<h2>10 个 css 代码片段</h2><p>以下这 10 个常用的 css 代码片段值得收藏,都可以用于平常的业务代码当中。</p><h3>1. 点点点加载中效果</h3><p>这是一个兼容性不错的用户体验很棒的点点点加载效果,实现思路如下:</p><ul><li>使用自定义的标签元素 dot。</li><li>将 dot 元素设置为内联元素(display:inline-block),并设置溢出隐藏(overflow:hidden),高度设置为 1em。</li><li>使用:before 伪元素结合\AUnicode 字符插入内容,并且使用 white-space:pre-wrap 保留换行效果,使用 css 动画。</li><li>使用 transform 和 translate 为...添加动画效果。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="loading">正在加载中<dot>...</dot></div></code></pre><p>css 代码如下:</p><pre><code class="css">.loading {
/**这里写自己自定义的样式 */
}
.loading > dot {
height: 1em;
overflow: hidden;
display: inline-block;
text-align: left;
vertical-align: -0.25em;
line-height: 1;
}
/* 核心代码 */
.loading > dot:before {
display: block;
/* 这行代码最重要 */
content: '...\A..\A.';
/* 值是Pre也是一样的效果 */
white-space: pre-wrap;
animation: dot 3s infinite step-start both;
}
@keyframes dot {
33% {
transform: translateY(-2em);
}
66% {
transform: translateY(-1em);
}
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/dot-loading.html"></iframe></p><h3>2. 对话框</h3><p>创建一个顶部带有三角形的内容容器对话框,实现思路如下:</p><ul><li>使用 :before 和 :after 伪元素创建两个三角形。</li><li>两个三角形的颜色应分别与容器的边框颜色和容器的背景颜色相同。</li><li>一个三角形的边框宽度 (:before) 应该比另一个三角形 (:after) 宽 1px,以便充当边框。</li><li>较小的三角形 (:after) 应位于较大三角形 (:before) 右侧 1px 处,以允许显示其左边框。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="container">Border with top triangle</div></code></pre><p>css 代码如下:</p><pre><code class="css">.container {
--borderColor--: #ddd;
--bgColor--: #fff;
position: relative;
background-color: var(--bgColor--);
padding: 15px;
margin-top: 20px;
border: 1px solid var(--borderColor--);
}
.container:before,
.container:after {
content: '';
position: absolute;
bottom: 100%;
left: 19px;
border: 11px solid transparent;
border-bottom-color: var(--borderColor--);
}
.container:after {
left: 20px;
border: 10px solid transparent;
border-bottom-color: var(--bgColor--);
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/border-with-top-triangle.html"></iframe></p><h3>3. 弹跳加载效果</h3><p>创建一个弹跳加载器动画,实现思路如下:</p><ul><li>使用 @keyframes 定义弹跳动画,使用 opacity 和 transform 属性。 在 transform: translate3d() 上使用单轴平移来获得更好的动画性能。</li><li>为弹跳圆创建一个父容器 .bouncing-loader。 使用 display: flex 和 justify-content: center 将它们定位在中心。</li><li>给三个弹跳的圆形 <code><div></code> 元素设置相同的宽度和高度以及 border-radius: 50% 以使它们成为圆形。</li><li>将 bouncing-loader 动画应用于三个弹跳圆圈中的每一个。</li><li>为每个圆圈和动画方向使用不同的动画延迟:交替以创建适当的效果。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="bouncing-loader">
<div class="bouncing-loader-item"></div>
<div class="bouncing-loader-item"></div>
<div class="bouncing-loader-item"></div>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
.bouncing-loader {
display: flex;
justify-content: center;
width: 150px;
}
.bouncing-loader-item {
width: 16px;
height: 16px;
margin: 3rem 0.2rem;
background-color: #0b16f1;
border-radius: 50%;
animation: bouncingLoader 0.6s infinite alternate;
}
.bouncing-loader-item:nth-child(2) {
animation-delay: 0.2s;
}
.bouncing-loader-item:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bouncingLoader {
to {
opacity: 0.1;
transform: translate3d(0, -16px, 0);
}
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/bouncing-loader.html"></iframe></p><h3>4. 动画边框按钮</h3><p>在悬停时创建边框动画,实现思路如下:</p><ul><li>使用 :before 和 :after 伪元素创建两个 24px 宽的盒子,在盒子的上方和下方彼此相对。</li><li>使用 :hover 伪类在悬停时将这些元素的宽度扩展到 100% 并使用过渡动画更改。</li></ul><p>html 代码如下:</p><pre><code class="html"><button class="animated-border-button">Submit</button></code></pre><p>css 代码如下:</p><pre><code class="css">.animated-border-button {
outline: none;
background-color: #2396ef;
padding: 12px 40px 10px;
border: none;
position: relative;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.animated-border-button::before,
.animated-border-button::after {
content: '';
position: absolute;
border: 0 solid transparent;
height: 0;
width: 24px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.animated-border-button::before {
border-top: 2px solid #2396ef;
top: -4px;
right: 0;
}
.animated-border-button::after {
border-bottom: 2px solid #2396ef;
bottom: -4px;
left: 0;
}
.animated-border-button:hover::before,
.animated-border-button:hover::after {
width: 100%;
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/button-border-animation.html"></iframe></p><h3>5. border 等高布局</h3><p>使用 border 实现等高布局,实现思路如下:</p><ul><li>给盒子元素设置一个左边框,边框宽度由子元素的宽度决定,这里是 150px。</li><li>给盒子元素的伪类设置清除浮动,这里不能使用 overflow:hidden 来清除。</li><li>给盒子元素的左边导航元素设置左浮动,并设置宽度和左负间距,间距值等于宽度值。</li><li>给盒子元素的右边内容元素设置 overflow:hidden。</li><li>导航子元素设置行高和右边子元素设置行高。</li></ul><p>html 代码如下:</p><pre><code class="html"><section class="box">
<nav class="box-nav">
<div class="box-nav-item">导航1</div>
</nav>
<section class="box-content">
<div class="box-content-module">模块1</div>
</section>
</section></code></pre><p>css 代码如下:</p><pre><code class="css">.box {
border-left: 150px solid #232425;
background-color: #eeeded;
}
.box::after {
content: '';
clear: both;
display: block;
}
.box-nav {
width: 150px;
margin-left: -150px;
float: left;
}
.box-nav-item {
line-height: 40px;
color: #fff;
text-align: center;
}
.box-content-module {
line-height: 40px;
text-align: center;
color: #c40dd4;
}
.box-content {
overflow: hidden;
}</code></pre><p>javascript 代码如下所示:</p><pre><code class="js">const navMore = document.getElementById('addNav'),
moduleMore = document.getElementById('addContent');
if (navMore && moduleMore) {
const nav = document.querySelector('.box-nav'),
section = document.querySelector('.box-content');
let navIndex = nav.children.length,
sectionIndex = 1;
let rand = () => 'f' + (Math.random() + '').slice(-1);
navMore.onclick = function () {
navIndex++;
nav.insertAdjacentHTML(
'beforeEnd',
'<div class="box-nav-item">导航' + navIndex + '</div>'
);
};
moduleMore.onclick = function () {
sectionIndex++;
section.insertAdjacentHTML(
'beforeEnd',
'<div class="box-content-module" style="background:#' +
[rand(), rand(), rand()].join('') +
'">模块' +
sectionIndex +
'</div>'
);
};
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/border-contour.html"></iframe></p><h3>6. 自定义复选框</h3><p>创建带有状态更改动画的样式复选框,实现思路如下:</p><ul><li>使用 <code><svg></code> 元素创建检查 <code><symbol></code> 并通过 <code><use></code> 元素将其插入以创建可重用的 SVG 图标。</li><li>创建一个 .ew-checkbox-group 并使用 flex box 为复选框创建适当的布局。</li><li>隐藏 <code><input></code> 元素并使用与其关联的标签来显示复选框和提供的文本。</li><li>使用 stroke-dashoffset 在状态更改时为检查符号设置动画。</li><li>通过 CSS 动画使用 transform: scale(0.9) 创建缩放动画效果。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="ew-checkbox-group">
<label class="ew-checkbox">
<svg class="ew-checkbox-symbol">
<symbol id="ew-check" viewbox="0 0 12 10">
<polyline
points="1.5 6 4.5 9 10.5 1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
></polyline>
</symbol>
</svg>
<input type="checkbox" class="ew-checkbox-input" />
<span class="ew-checkbox-item">
<svg class="ew-checkbox-icon" width="12px" height="10px">
<use xlink:href="#ew-check"></use>
</svg>
</span>
<span class="ew-checkbox-item"> Apples </span>
</label>
<label class="ew-checkbox">
<input type="checkbox" class="ew-checkbox-input" />
<span class="ew-checkbox-item">
<svg class="ew-checkbox-icon" width="12px" height="10px">
<use xlink:href="#ew-check"></use>
</svg>
</span>
<span class="ew-checkbox-item"> Oranges </span>
</label>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.ew-checkbox-group {
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
height: 64px;
display: flex;
flex-wrap: row wrap;
justify-content: center;
align-items: center;
}
.ew-checkbox-group .ew-checkbox-symbol {
width: 0;
height: 0;
position: absolute;
pointer-events: none;
user-select: none;
}
.ew-checkbox-group * {
box-sizing: border-box;
}
.ew-checkbox-input {
position: absolute;
visibility: hidden;
}
.ew-checkbox {
user-select: none;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s ease-in-out;
display: flex;
}
.ew-checkbox:not(:last-of-type) {
margin-right: 6px;
}
.ew-checkbox:hover {
background-color: rgba(0, 120, 255, 0.06);
}
.ew-checkbox .ew-checkbox-item {
vertical-align: middle;
transform: translate3d(0, 0, 0);
}
.ew-checkbox .ew-checkbox-item:first-of-type {
position: relative;
flex: 0 0 18px;
width: 18px;
height: 18px;
border-radius: 4px;
transform: scale(1);
border: 1px solid #cdcdfd;
transition: all 0.4s ease;
}
.ew-checkbox .ew-checkbox-icon {
position: absolute;
top: 3px;
left: 2px;
fill: none;
stroke: #fff;
stroke-dasharray: 16px;
stroke-dashoffset: 16px;
transition: all 0.4s ease;
transform: translate3d(0, 0, 0);
}
.ew-checkbox .ew-checkbox-item:last-of-type {
padding-left: 8px;
line-height: 18px;
}
.ew-checkbox:hover .ew-checkbox-item:first-of-type {
border-color: #2396ef;
}
.ew-checkbox-input:checked + .ew-checkbox-item:first-of-type {
animation: zoom-in-out 0.3s ease;
background-color: #2396ef;
border-color: #2396ef;
}
.ew-checkbox-input:checked + .ew-checkbox-item .ew-checkbox-icon {
stroke-dashoffset: 0;
}
@keyframes zoom-in-out {
50% {
transform: scale(0.9);
}
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/custom-checkbox.html"></iframe></p><h3>7. 自定义单选框</h3><p>创建一个带有状态更改动画的样式单选按钮,实现思路如下:</p><ul><li>创建一个 .radio-container 并使用 flex box 为单选按钮创建适当的布局。</li><li>重置 <code><input></code> 上的样式并使用它来创建单选按钮的轮廓和背景。</li><li>使用 ::before 元素创建单选按钮的内圈。</li><li>使用 transform: scale(1) 和 CSS transition 在状态变化时创建动画效果。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="radio-container">
<input type="radio" class="radio-input" id="male" name="sex" />
<label for="male" class="radio">男</label>
<input type="radio" class="radio-input" id="female" name="sex" />
<label for="female" class="radio">女</label>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.radio-container {
box-sizing: border-box;
background-color: #fff;
color: #545355;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
flex-flow: row wrap;
}
.radio-container * {
box-sizing: border-box;
}
.radio-input {
appearance: none;
background-color: #fff;
width: 16px;
height: 16px;
border: 1px solid #cccfdb;
margin: 0;
border-radius: 50%;
display: grid;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in;
}
.radio-input::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
transform: scale(0);
transition: 0.3s transform ease-in-out;
box-shadow: inset 6px 6px #fff;
}
.radio-input:checked {
background-color: #2396ef;
border-color: #2396ef;
}
.radio-input:checked::before {
transform: scale(1);
}
.radio {
cursor: pointer;
padding: 6px 8px;
}
.radio:not(:last-child) {
margin-right: 6px;
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/custom-radio.html"></iframe></p><h3>8. 打字效果</h3><p>创建打字机效果动画,实现思路如下:</p><ul><li>定义两个动画,键入动画字符和闪烁动画插入符号。</li><li>使用 ::after 伪元素将插入符号添加到容器元素。</li><li>使用 JavaScript 设置内部元素的文本并设置包含字符数的 --characters 变量。 此变量用于为文本设置动画。</li><li>使用 white-space: nowrap 和 overflow: hidden 使内容在必要时不可见。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="typewriter-effect">
<div class="text" id="typewriter-text"></div>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.typewriter-effect {
display: flex;
justify-content: center;
font-family: monospace;
font-size: 25px;
color: #535455;
font-weight: 500;
}
.text {
max-width: 0;
animation: typing 3s steps(var(--characters--)) infinite;
white-space: nowrap;
overflow: hidden;
}
.typewriter-effect::after {
content: ' |';
animation: blink 1s infinite;
animation-timing-function: step-end;
}
@keyframes typing {
75%,
100% {
max-width: calc(var(--characters--) * 1ch);
}
}
@keyframes blink {
0%,
75%,
100% {
opacity: 1;
}
25% {
opacity: 0;
}
}</code></pre><p>javascript 代码如下:</p><pre><code class="js">const typeWriter = document.getElementById('typewriter-text');
const createWriter = (text = 'Lorem ipsum dolor sit amet.') => {
typeWriter.innerHTML = text;
typeWriter.style.setProperty('--characters--', text.length);
};
createWriter();</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/typewriter-effect.html"></iframe></p><h3>9. 高度过渡效果</h3><p>当元素的高度未知时,将元素的高度从 0 转换为 auto,实现思路如下:</p><ul><li>使用 transition 来指定对 max-height 的更改应该被过渡。</li><li>使用 overflow:hidden 来防止隐藏元素的内容溢出其容器。</li><li>使用 max-height 指定初始高度为 0。</li><li>使用 :hover 伪类将 max-height 更改为 JavaScript 设置的 --max-height 变量的值。</li><li>使用 Element.scrollHeight 和 CSSStyleDeclaration.setProperty() 将 --max-height 的值设置为元素的当前高度。</li><li>注意:在每个动画帧上导致重排,如果在高度过渡的元素下方有大量元素,则会出现延迟。</li></ul><p>html 代码如下:</p><pre><code class="html"><div class="trigger">
Hover me to see a height transition.
<div class="el">Additional content</div>
</div></code></pre><p>css 代码如下:</p><pre><code class="css">.trigger {
cursor: pointer;
border-bottom: 1px solid #2396ef;
}
.el {
transition: max-height 0.4s;
overflow: hidden;
max-height: 0;
}
.trigger:hover > .el {
max-height: var(--max-height--);
}</code></pre><p>javascript 代码如下:</p><pre><code class="js">const el = document.querySelector('.el'),
height = el.scrollHeight;
el.style.setProperty('--max-height--', height + 'px');</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/height-transition.html"></iframe></p><h3>10. 开关切换</h3><p>仅使用 CSS 创建一个切换开关小组件,实现思路如下:</p><ul><li>使用 for 属性将 <code><label></code> 与复选框 <code><input></code> 元素相关联。</li><li>使用 <code><label></code> 的 ::after 伪元素为开关创建一个圆形旋钮。</li><li>使用 :checked 伪类选择器更改旋钮的位置,使用 transform: translateX(20px) 和开关的背景颜色。</li><li>使用 position: absolute 和 left: -9999px 在视觉上隐藏 <code><input></code> 元素。</li></ul><p>html 代码如下:</p><pre><code class="html"><input type="checkbox" id="toggle" class="offscreen checkbox" />
<label for="toggle" class="switch"></label></code></pre><p>css 代码如下:</p><pre><code class="css">.offscreen {
position: absolute;
left: -9999px;
}
.checkbox:checked + .switch::after {
transform: translateX(20px);
}
.checkbox:checked + .switch {
background-color: #7983ff;
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.25);
transition: all 0.3s;
cursor: pointer;
}
.switch::after {
content: '';
position: absolute;
width: 18px;
height: 18px;
border-radius: 18px;
background-color: #fff;
top: 1px;
left: 1px;
transition: all 0.3s;
}</code></pre><p>效果如下所示:</p><p><iframe src="https://eveningwater.github.io/code-segment/codes/css/html/toggle-switch.html"></iframe></p><blockquote>ps: 以上代码段来自各大网络上收集的,目前总结在本人的代码段网站<a href="https://link.segmentfault.com/?enc=adXUDg%2BKVlhIYisnM6tPFg%3D%3D.0SGNsYfQpNZ%2Bo4YIOf45g9Flz%2Fdywg5H88tJBSgVSrkZfQEJC%2F8XQU8iyprZjnbz" rel="nofollow">代码段</a>上。</blockquote>
抖音两个旋转小球的loading实现
https://segmentfault.com/a/1190000044177481
2023-09-03T23:15:45+08:00
2023-09-03T23:15:45+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
1
<p>抖音的小圆球加载效果相信大家都见识过,也对其中的实现原理应该有一定的好奇心吧,下面就让我带大家来探索一下小圆球加载效果的实现原理吧。</p><p>要实现两个小圆球,我们可以思考两种方案的实现,第一种就是css方案,画两个小圆球,然后使用css动画来实现,而第二种则是使用canvas方案。我们首先来尝试第一种方案,首先肯定是要画出两个小圆球,这不就是相当于画两个圆嘛,所以使用宽高加圆角属性即可实现。</p><p>html代码如下:</p><pre><code><div class="rotate-ball">
<div class="small-ball small-left-ball"></div>
<div class="small-ball small-right-ball"></div>
</div></code></pre><p>首先是一个旋转的容器元素,接着就是左右两个小圆球,我的思路也很简单,既然两个小球是互相旋转的,那也就是说我给它们的父元素旋转不就达到了两个小球互相旋转的效果吗?</p><p>接下来我们来看样式代码:</p><pre><code>.small-ball {
width: 11px;
height: 11px;
border-radius: 50%;
}
.small-ball.small-left-ball {
background-color: #e94359;
}
.small-ball.small-right-ball {
background-color: #74f0ed;
}
.rotate-ball {
width: 22px;
animation: rotate 5s ease-in infinite forwards;
transition: 2s;
display: flex;
align-items: center;
justify-content: space-between;
}
@keyframes rotate {
0% {
transform: rotateY(0deg);
}
100% {
transform: rotateY(360deg);
}
}</code></pre><p>样式代码很简单,就是设置两个小圆球的宽高和圆角,然后分别设置不同的背景色,然后给父元素添加旋转动画,这看起来似乎很容易就实现了,接下来我们来看效果。</p><p><a href="https://link.segmentfault.com/?enc=NZLP18IUYu6anwiUQULwKg%3D%3D.1eedmb%2ByIMdLgo7jtO9Hf8RlTfB7Tu1dsAeDIsqDeUICID09hwuGiEwrYq9jFp98" rel="nofollow">https://code.juejin.cn/pen/7231520565935210500</a></p><p>嗯大功告成,等等,这个效果差的太远了吧,没那么简单,好吧很显然这个方案不太合适,让我们换一种方式来实现,也就是第二种方案canvas方案。</p><p>使用canvas方案实现我们主要分为两个步骤,第一步即使用canvas画出两个小圆球,第二步则是让两个小圆球进行翻转,也就是添加翻转动画。</p><p>首先第一步当然是画小圆球,每个小圆球我们都可以看作是一个类,我们把它叫做ball,好的,接下来我们来看代码如下:</p><pre><code>class Ball {
// 这里写核心代码
}</code></pre><p>既然小圆球是一个类,那么我们小圆球就会有属性,思考一下,我们会有哪些属性呢?总结如下:</p><ul><li>圆心X坐标</li><li>圆心Y坐标</li><li>半径</li><li>开始角度</li><li>结束角度</li><li>顺时针,逆时针方向指定</li><li>是否描边</li><li>是否填充</li><li>缩放X比例</li><li>缩放Y比例</li></ul><p>首先小圆球有一个圆心坐标,既x和y坐标,其次还有半径,然后旋转的角度会有开始和结束,并且还会有旋转的方向,然后就是画小圆球是否有描边,是否能够填充,最后就是缩放比例(主要用于小球运动时,我们根据实际效果可以看到小球旋转的时候明显有缩放效果,所以这里需要一个缩放比例的属性)。</p><p>分析了属性之后,很显然我们第一步要做的就是初始化这些属性,代码如下:</p><pre><code>class Ball {
x: number;
y: number;
r: number;
startAngle: number;
endAngle: number;
anticlockwise: boolean;
stroke: boolean;
fill: boolean;
scaleX: number;
scaleY: number;
lineWidth: number;
fillStyle: string | CanvasGradient | CanvasPattern;
strokeStyle: string | CanvasGradient | CanvasPattern;
constructor(o: AnyObj) {
this.x = 0; // 圆心X坐标
this.y = 0; // 圆心Y坐标
this.r = 0; // 半径
this.startAngle = 0; // 开始角度
this.endAngle = 0; // 结束角度
this.anticlockwise = false; // 顺时针,逆时针方向指定
this.stroke = false; // 是否描边
this.fill = false; // 是否填充
this.scaleX = 1; // 缩放X比例
this.scaleY = 1; // 缩放Y比例
this.init(o);
}
init(o: AnyObj): void {
Object.keys(o).forEach(k => (this[k] = o[k]));
}
}</code></pre><p>初始化属性之后我们要干什么?那当然是渲染小圆球啦,写一个render方法就可以了。</p><pre><code>class Ball {
// 以上代码以省略
render(){
// 渲染小圆球代码
}
}</code></pre><p>如何画小圆球?也就是canvas画圆的步骤,最核心的就是canvas的arc方法,总的说来,我们主要分为设置原点坐标,设置缩放,调用arc方法画圆,设置线宽,填充颜色,以及描边这几步,然后我们返回小圆球实例,因而代码如下:</p><pre><code>class Ball {
// 以上代码已省略
render(ctx: CanvasRenderingContext2D | null): Ball | void {
if (!ctx) {
return;
}
ctx.save();
ctx.beginPath();
ctx.translate(this.x, this.y); // 设置原点的位置
ctx.scale(this.scaleX, this.scaleY); // 设定缩放
ctx.arc(0, 0, this.r, this.startAngle, this.endAngle); // 画圆
if (this.lineWidth) {
// 线宽
ctx.lineWidth = this.lineWidth;
}
if (this.fill) {
// 是否填充
this.fillStyle ? (ctx.fillStyle = this.fillStyle) : null;
ctx.fill();
}
if (this.stroke) {
// 是否描边
this.strokeStyle ? (ctx.strokeStyle = this.strokeStyle) : null;
ctx.stroke();
}
ctx.restore();
return this;
}
}</code></pre><p>如此一来,我们画小圆球这一步就算是完成了,接下来我们要做的就是让小圆球动起来。要让小圆球动起来,那么就需要用到定时器,然而我这里并没有使用setInterval函数,这是为什么呢?</p><p>如果我们把定时器每次执行一次看作是一个任务,那么setInterval就相当于是按照一定的时间间隔来执行任务,而一个任务的开始时间和结束时间我们是无法保证它们之间的时间间隔的,也就是说有时候我们的循环定时任务会被跳过,而setTimeout因为是在条件满足的时候会自动停止,所以我们可以使用setTimeout来避免这个问题,因此,接下来我要说的就是我们会使用setTimeout来模拟实现setInterval函数。</p><p>那么如何实现呢?</p><p>我们把每次setTimeout执行也看作是一个任务,然后我们通过一个对象来存储每一次执行的任务,这样我们每次执行的任务都可以通过在对象当中找到,因此,我们要清除任务同样也可以从对象当中取出任务来清除。</p><p>也就是说,我们存储每一个setTimeout任务的延迟id,这个函数返回一个数值型的延迟id,我们把这个值记录到对象当中,方便后面从对象当中取出然后清除任务。</p><p>实现这个函数主要分成两部分,第一部分当然还是模拟实现执行定时任务,第二部分就是模拟实现一个清除定时任务的函数,即clearInterval函数的模拟实现。</p><p>模拟实现定时任务我们可以使用递归来实现,这个应该还是比较好理解,这里我们还有一点,那就是存储在对象当中的延迟id,我们需要一个属性名,对象不就是一种含有属性名属性值的键值对数据类型吗?在这里属姓名我们可以使用Symbol类型,为什么使用这个数据类型?因为这个数据类型确保了唯一性。</p><p>最后还有一点,那就是如果要写ts类型,那么定时器任务的回调函数应该是任意类型的函数,因此这里需要编写类型代码。如下:</p><pre><code>type AnyFunction = (...args: any) => any;</code></pre><p>通过以上分析,我们的模拟函数代码就很好理解了,代码如下:</p><pre><code>export const defineSetInterval = (): {
setInterval: (fn: AnyFunction, time: number) => symbol;
clearInterval: (k: symbol) => void;
} => {
const timeWorker = {};
const key = Symbol();
const defineInterval = (handler: AnyFunction, interval: number) => {
const executor = (fn: AnyFunction, time: number) => {
timeWorker[key] = setTimeout(() => {
fn();
executor(fn, time);
}, time);
};
executor(handler, interval);
return key;
};
const defineClearInterval = (k: symbol):void => {
if (k in timeWorker) {
clearTimeout(timeWorker[k] as number);
delete timeWorker[k];
}
};
return {
setInterval: defineInterval,
clearInterval: defineClearInterval
};
};</code></pre><p>以下是该函数的使用示例代码:</p><pre><code>const { setInterval, clearInterval } = defineSetInterval();
const timeId = setInterval(() => alert('hello,world!'), 1000);
// 取消定时器// clearInterval(timeId);</code></pre><p>让我们继续下一步,下一步我们当然是创建这两个小圆球,然后暴露出一个start方法和一个clear方法,顾名思义,就是在这个函数当中我们创建小圆球,然后默认不执行动画,将执行动画的逻辑包装在start方法中,而之所以留下一个clear方法,那就是如果需要实现暂停效果,也就是清除定时器了,那么我们就需要调用clear方法清除定时器,暂停执行动画,如果需要重新执行动画,那么我们也就重新调用start方法即可。因此这个函数的结构我们可以定义如下:</p><pre><code>export interface CreateBallReturnType {
clear: () => void;
start: (time?: number) => void;
}
export interface AnyObj {
[prop: string]: unknown
}
export const createBall = (
el: HTMLElement | string,
leftBallConfig?: AnyObj,
rightBallConfig?: AnyObj
): CreateBallReturnType => {
// 这里写核心代码
}</code></pre><p>这个函数有3个参数,第一个参数是一个dom元素,也就是说,我们需要将两个小圆球渲染到canvas元素上,再将这个canvas元素添加到一个容器元素当中,这个el参数就是代表传入一个容器元素中,如果不传,那么我们默认就添加到body元素中,第二个和第三个参数分别是两个小圆球的配置属性对象,其实这里我们直接采用默认的就好,不需要传入这两个参数,因此这两个参数是可选的,虽然这里定义的是任意对象,但实际上根据前面小圆球类含有哪些属性的分析结果来看,这两个参数很明显传入的就是初始化的那些属性,如果有特别需求,可以传入这些属性进行更改。</p><p>在实现该函数的核心之前,我们这里会涉及到一个计算缩放比例的公式,代码如下:</p><pre><code>export const computedScale = (val: number, dir: number, dis: number): number =>
(val * 1000 + dir * (dis * 1000)) / 1000;</code></pre><p>这里就不多分析这个公式的原理了,只要记住它是一个公式就可以了。</p><p>接下来我们看该函数的核心实现,我们主要也还是分成2个部分,第一个部分渲染两个小圆球并添加到容器元素中,定义动画函数,并封装到start函数当中,然后暴露出start和clear函数。这里需要注意的一点,那就是小圆球的宽高以及canvas元素的宽高不会太大,然后小圆球移动有个边界,因此x坐标和y坐标有个最小值和最大值,我们定义成一个一维数组即可。</p><p>接下来,我们按照相应的分析步骤去实现每一步骤的代码就可以了,每一步在代码当中也有所注明,所以我们只需要看完整代码即可。</p><pre><code>export const createBall = (
el: HTMLElement | string,
leftBallConfig?: AnyObj,
rightBallConfig?: AnyObj
): CreateBallReturnType => {
const container = (typeof el === 'string' ? document.querySelector(el) : el) || document.body;
const canvas = document.createElement('canvas');
canvas.width = 34;
canvas.height = 20;
container.appendChild(canvas);
const w = canvas.width;
const h = canvas.height;
const ctx = canvas.getContext('2d');
const xArr = [10, 22];
const yArr = [10, 10];
const leftBall = new Ball({
x: xArr[0],
y: yArr[0],
r: 6,
startAngle: 0,
endAngle: 2 * Math.PI,
fill: true,
fillStyle: '#E94359',
lineWidth: 1.2,
...leftBallConfig
}).render(ctx);
const rightBall = new Ball({
x: xArr[1],
y: yArr[1],
r: 6,
startAngle: 0,
endAngle: 2 * Math.PI,
fill: true,
fillStyle: '#74F0ED',
lineWidth: 1.2,
...rightBallConfig
}).render(ctx);
const a = 1.04; // 加速度
let dir = 1; // 方向
let dis = 1; // X轴移动初始值
const move = (): void => {
if (!ctx || !leftBall || !rightBall) {
return;
}
// 清理画布
ctx.clearRect(0, 0, w, h);
// 通过加速度计算移动值
dis *= a;
// 更改x轴位置
leftBall.x += dir * dis;
rightBall.x -= dir * dis;
// 计算缩放比例
leftBall.scaleX = computedScale(-dir, 0.005, leftBall.scaleX);
leftBall.scaleY = computedScale(-dir, 0.005, leftBall.scaleY);
rightBall.scaleX = computedScale(dir, 0.005, rightBall.scaleX);
rightBall.scaleY = computedScale(dir, 0.005, rightBall.scaleY);
// 到达指定位置后
if (leftBall.x >= 22 || rightBall.x >= 22 || leftBall.x <= 10 || rightBall.x <= 10) {
// 设定缩放比例
leftBall.scaleX = rightBall.scaleX;
leftBall.scaleY = rightBall.scaleY;
rightBall.scaleX = leftBall.scaleX;
rightBall.scaleY = leftBall.scaleY;
// 还原X轴移动初始值
dis = 1;
// 变更移动方向
dir = -dir;
}
// 绘制
if (dir > 0) {
// 方向不一样时,小球的绘制顺序要交换,移模拟旋转
rightBall.render(ctx);
leftBall.render(ctx);
} else {
leftBall.render(ctx);
rightBall.render(ctx);
}
};
let timer: symbol;
const { setInterval: setHandler, clearInterval: clearHandler } = defineSetInterval();
const start = (time = 50): void => {
timer = setHandler(move, time);
};
return {
start,
clear: (): void => {
if (timer) {
clearHandler(timer);
}
}
};
};</code></pre><p>可以看到我们先是创建canvas元素,设置宽高,然后创建两个小圆球添加到canvas元素当中,再然后我们定义一个move方法,也就是小圆球的翻转动画的实现,难点可能就主要是翻转动画的实现原理。</p><p>如此一来,我们如果是写js/ts代码,使用起来就很简单,直接调用方法即可,如:</p><pre><code>createBall();
// 如果需要指定特定的容器元素,那么传入一个dom元素,例如 document.querySelector('#app')
// 又或者传入一个字符串也可以,既'#app'
// 也就是createBall('#app');</code></pre><p>接下来我们再封装一下,将这个函数用到react框架中,做成一个组件,很简单,我们利用ref对象来存储dom元素,然后使用useEffect函数监听这个dom元素是否存在,然后存在就调用该方法。代码如下:</p><pre><code>import React, { createRef, CSSProperties, ReactElement, useEffect } from 'react';
import { createBall } from './ball';
import '../style/loading.scss';
export interface LoadingProps extends AnyObj {
style?: CSSProperties;
}
const Loading = (props: LoadingProps = {}): ReactElement | null => {
const loadingRef = createRef<HTMLDivElement>();
useEffect(() => {
// 这里多一个children判断是因为如果该元素已经被渲染过,我们就不需要添加到容器元素中了
if (loadingRef.current && !loadingRef.current.children.length) {
const ball = createBall(loadingRef.current);
ball.start();
}
}, [loadingRef]);
return <div ref={loadingRef} className="loading" {...props} />;
};
export default Loading;</code></pre><p>这里涉及到了一点样式,样式随便自己写:</p><pre><code>@import './extend.scss';
.loading {
position: absolute;
left: 0;
top: 0;
@extend .perfect, .flex-center;
}</code></pre><p>extend.scss代码如下:</p><pre><code>.flex-content-center {
display: flex;
justify-content: center;
}
.flex-align-center {
display: flex;
align-items: center;
}
.flex-center {
@extend .flex-content-center, .flex-align-center;
}
.perfect {
width: percentage(1);
height: percentage(1);
}</code></pre><p>如此一来,我们的抖音旋转小圆球效果就实现了,如下所示:</p><p><a href="https://link.segmentfault.com/?enc=rHKDokvqlcIRE%2BRbpvw7wQ%3D%3D.45kTLuVHhAEK4Jn6i1BbxSfoiIIN1iGOwpk%2FHc4pVOVh0C%2BIBaAFqRYV6pmdR4Zv" rel="nofollow">https://code.juejin.cn/pen/7231520933775671330</a></p>
2022我的年终总结
https://segmentfault.com/a/1190000043186261
2022-12-28T18:31:31+08:00
2022-12-28T18:31:31+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
7
<p>本文参与了 <a href="https://segmentfault.com/a/1190000042923114">SegmentFault 思否年度征文「一名技术人的 2022」</a>,欢迎正在阅读的你也加入。</p><h2>前言</h2><p>时光如梭,岁月匆匆而过,2022年一转眼就已经到了末尾,今年的环境异常艰难,可是想想自己这一年来的付出,也还是值得做一个复盘总结的,正所谓有得必有失,在这一年我失去了太多,不过却也让我成长了不少,当然这些都是题外话,我主要还是来复盘一下今年我所学习的成果。</p><h2>文章总结</h2><p>今年一共写了11篇文章,加上本次年终总结,一共12篇,数量也并不多,分别如下:</p><ul><li><a href="https://segmentfault.com/a/1190000042181290">使用React手写一个手风琴组件</a></li><li><a href="https://segmentfault.com/a/1190000042298118">手写一个有点意思的电梯小程序</a></li><li><a href="https://segmentfault.com/a/1190000042322957">手写一个有点意思的电梯小程序(React版本)</a></li><li><a href="https://segmentfault.com/a/1190000042502121">实现《羊了个羊》的小游戏</a></li><li><a href="https://segmentfault.com/a/1190000042509082">50天用vue3完成了50个web项目,我学到了什么?</a></li><li><a href="https://segmentfault.com/a/1190000042517443">手写一个mini版本的React状态管理工具</a></li><li><a href="https://segmentfault.com/a/1190000042575730">实现《羊了个羊-美女版》小游戏(低配版)</a></li><li><a href="https://segmentfault.com/a/1190000042679801">一个有趣的交互效果的实现</a></li><li><a href="https://segmentfault.com/a/1190000042643627">vue3实现一个思否猫连连看小游戏</a></li><li><a href="https://segmentfault.com/a/1190000042661646">原生javascript手写一个丝滑的轮播图</a></li><li><a href="https://segmentfault.com/a/1190000042910744">三分钟学会go语言的变量定义</a></li></ul><p>其中尤其是<code>50天用vue3完成了50个web项目,我学到了什么?</code>和<code>实现《羊了个羊》的小游戏</code>我最为满意,毕竟这两篇文章是我用心总结出每一个知识点,并且让自己融会贯通学到的知识点,同时也帮助他人学到知识点那就足够了。</p><p>其实今年我主要重心放在了React技术栈上,所以我也用React写了不少东西,对于vue框架,尤其是vue3,最主要的输出就是<code>50天用vue3完成了50个web项目,我学到了什么?</code>,关于这50个项目其实虽然表面上写的是50天,但实际上我花了不止50天,当然这都不重要了,最重要的是我从这50个项目里面深入去了解了一下less和sass这两门css预处理语言的语法。例如混入mixin,函数,循环,条件判断等,两种预处理语言之间也有很多相同的地方,当然也有不同的地方。比如循环,我们在sass当中会有@for关键字,而在less当中,我们需要写选择器 + when(这种更像是在写递归调用自身)。</p><p>同时对于vue3的核心语法,我也有了一定的认知,至少在实际做vue3的项目当中,我认为我还是没有多大的问题的,这些都是通过实际动手做这50个web项目让我学到的,为了方便我还特意用vue3写了一个关于这50个web项目的网站,地址在<a href="https://link.segmentfault.com/?enc=3qufFxyXXc2as0fxCh29JA%3D%3D.TIjCT%2BORabot%2Ba2IICFN%2FoWeQaV33D%2B%2FSD7WVFkfkWO1vdMU25it%2BEjjDm8a76wLTDIKTnQe2pNxZCzfcolKJH%2FU7kss%2FAtL%2BT3FnoQiF2M%3D" rel="nofollow">这里</a>。</p><blockquote>PS: 如果以上地址访问不到,可以访问<a href="https://link.segmentfault.com/?enc=wpGexGU3mOYHALeeLUySjA%3D%3D.tGNknD4Vv%2Bfj0GFjKI6%2BoTrLLhquXgxRLk79jCpi0uw3fEEAn1C4PHHQ33VMLRndZjYR%2FMfnO2Dwqm%2B%2BUhdy9%2BhJ3ZuWxzQUYa8y7dlGAb0%3D" rel="nofollow">这里</a>。</blockquote><p>这个网站是我自己设计并实现的,虽然布局看起来有些简单,但是我认为其中的逻辑功能和样式代码还是有点点难度的,在这里我可以总结一下有哪些知识点值得学习:</p><ul><li>实现一个clickOutside指令</li><li>实现一个下拉框组件</li><li>实现一个图片预加载组件</li><li>实现classnames工具函数(不是复制的classnames源码,是参考实现的)</li><li>实现文本超出省略的判断</li><li>实现回到顶部的功能</li><li>卡片组件以及图标组件的实现</li><li>less核心语法</li></ul><p>以上是我今年在文章上所做的总结,除此之外我还在github上新增贡献并逐步完善了3个仓库,让我们一起来看看吧。</p><h2>3个项目</h2><h3>算法</h3><p>关于剑指offer算法,我基本上已经将剑指offer的官方算法题刷完了,并且解题思路,我也已经记下来建了一个项目,地址在<a href="https://link.segmentfault.com/?enc=mKp2DKzqKQgXT9vUhSFIWQ%3D%3D.NpSk5i2PHcbLkWbtUH5LQBj6E73%2BxqQI1Px04%2BAcQ1I9bpg4BkdRwy3QIJJ8N5aS" rel="nofollow">这里</a>。</p><p>虽然在工作当中我似乎并没有用到太多算法,可事实上做了一下算法题确实是打开了我的眼界,更何况,我也已经将算法给加到了我所做的项目当中,比如那个用vue3实现连连看的小游戏里面就有这样一个算法。</p><pre><code class="ts">const findRepeatItem = function (arr: GlobalModule.MaterialData[]) {
const unique = new Set();
for (const item of arr) {
if (unique.has(item.src)) {
return true;
}
unique.add(item.src);
}
return false;
};</code></pre><p>这个函数顾名思义,就是从数组当中查找重复项,思路就是利用set数据结构存储每一个数组项,然后当数据里面存在要查找的项的时候,就代表重复了,直接返回即可,这个函数的思路就来源于算法当中。也就是这个算法<a href="https://link.segmentfault.com/?enc=w5adkLDWhRU76LGTScRm%2FA%3D%3D.qlgGIl150NjI5FE2a09TR%2FK%2BD2O8NtLLerun8ppvvOgyRQbhEd1uYxJ8xG%2BJ6pybPQ7bDGFttqeX5ruwHlkPi6uUJoLrQVbmiJJ2U93ztxk%3D" rel="nofollow">数组中重复的数字</a>的思路二--哈希表解法。</p><p>这只是其中一个小小的应用而已,如果在实际项目当中碰到相应的需求,我或许也会再次回头来翻看这些笔记,已达到巩固并且举一反三的目的,这个项目也就是我今年完善的3个项目之一。</p><h3>js和css代码段</h3><h4>css代码段</h4><p>这可能是我今年付出精力最多的一个项目了吧,几乎每天都要贡献一段代码段,不信看下图:</p><p><img src="/img/bVc5mSI" alt="" title=""></p><p>每天学习一段javascript或者是css代码段,不至于让自己忘记css和js基础,而且很多代码段都会用到实际业务当中,比如使用css实现自定义的单选框和复选框,我们以单选框作为示例讲一下实现思路如下。</p><h5>css实现单选框</h5><p>思路就是如下列出的几点,我们就可以创建一个带有状态更改动画的样式单选按钮。</p><ul><li>创建一个 .radio-container 并使用 flexbox 为单选按钮创建适当的布局。</li><li>重置 <code><input></code> 上的样式并使用它来创建单选按钮的轮廓和背景。</li><li>使用 ::before 元素创建单选按钮的内圈。</li><li>使用 transform: scale(1) 和 CSS transition 在状态变化时创建动画效果。</li></ul><p>代码量也不算多,我们来看html和css代码分别如下:</p><pre><code class="html"><div class="radio-container">
<input type="radio" class="radio-input" id="male" name="sex"/>
<label for="male" class="radio">男</label>
<input type="radio" class="radio-input" id="female" name="sex"/>
<label for="female" class="radio">女</label>
</div></code></pre><pre><code class="css">.radio-container {
box-sizing: border-box;
background-color: #fff;
color: #545355;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
flex-flow: row wrap;
}
.radio-container * {
box-sizing: border-box;
}
.radio-input {
appearance: none;
background-color: #fff;
width: 16px;
height: 16px;
border: 1px solid #cccfdb;
margin: 0;
border-radius: 50%;
display: grid;
align-items: center;
justify-content: center;
transition: all .3s ease-in;
}
.radio-input::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
transform: scale(0);
transition: .3s transform ease-in-out;
box-shadow: inset 6px 6px #fff;
}
.radio-input:checked {
background-color: #2396ef;
border-color: #2396ef;
}
.radio-input:checked::before {
transform: scale(1);
}
.radio {
cursor: pointer;
padding: 6px 8px;
}
.radio:not(:last-child){
margin-right: 6px;
}</code></pre><p>都是常规的布局,其中我们主要利用了label的for属性和input的id属性绑定在一起,然后通过样式将input框给隐藏,修改label的样式去模拟出单选框,从而达到如下的效果:</p><p><img src="/img/bVc5mSJ" alt="" title=""></p><p>同理,复选框也是这样的思路去实现的,事实上还有很多小技巧,比如隐藏一个元素,我们通常可能会使用display和visibility又或者是opacity属性来达到隐藏,可事实上这三个属性隐藏元素或多或少都会有一定的问题,比如display无法添加过渡效果,而visibility又占用元素本身的空间,opacity只是单纯的设置透明度,元素依然可以被点击等等,而这里我们可以利用clip和定位来达到隐藏元素的目的,从而解决前面三个属性所带来的问题。代码如下:</p><pre><code class="css">.offscreen {
border: 0;
clip: rect(0,0,0,0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}</code></pre><p>然后我们给要隐藏的元素添加一个offscreen类名即可达到隐藏元素,这不失为一种隐藏元素的办法,当然还有很多的css代码段值得学习的,例如border实现等高布局,css实现加载中效果,开关组件,高度过渡效果等等示例。</p><h4>js代码段</h4><p>除了css代码段,js代码段也有许多值得学习的知识点,比如冒泡排序算法.</p><h5>冒泡排序算法</h5><p>我们来看这个算法的实现思路如下:</p><ul><li>声明一个变量,swapped,指示在当前迭代期间是否交换了任何值。</li><li>使用扩展运算符 (...) 克隆原始数组 arr。</li><li>使用 for 循环遍历克隆数组的元素,在最后一个元素之前终止。</li><li>使用嵌套的 for 循环遍历 0 和 i 之间的数组段,交换任何相邻的乱序元素并将 swapped 设置为 true。</li><li>如果在迭代后 swapped 为 false,则不需要更多更改,因此返回克隆的数组。</li></ul><p>代码如下:</p><pre><code class="js">const bubbleSort = arr => {
let swapped = false;
let a = [...arr];
for(let i = 0;i < a.length;i++){
swapped = false;
for(let j = 0;j < a.length - i;j++){
if(a[j + 1] < a[j]){
//数组解构的方式
[a[j],a[j + 1]] = [a[j + 1],a[j]];
swapped = true;
}
}
if(!swapped) {
return a;
}
}
return a;
}</code></pre><p>再比如数组分块,这个也会用到实际业务当中。</p><h5>数组分块</h5><p>我们来看实现数组分块的思路如下:</p><ul><li>使用 Array.from() 创建一个新数组,该数组适合将要生成的块数。</li><li>使用 Array.prototype.slice() 将新数组的每个元素映射到长度为 size 的块。</li><li>如果原始数组不能被平均分割,最终的块将包含剩余的元素。</li></ul><p>js代码如下:</p><pre><code class="js">const chunk = (arr,size) => Array.from({ length:Math.ceil(arr.length / size)},(v,i) => arr.slice(i* size,i * size + size)); </code></pre><p>当然还有更多的css和js代码段,我就不一一举例了,我只是想说明这样每天学习一段代码段让自己学到了很多,更多的代码段请看<a href="https://link.segmentfault.com/?enc=zPZVOujxKzb903dgpUq9RA%3D%3D.MMWXrSx7vjxRyeVDIBvsyLBLNE8dRCXRe3T%2B2koeRJyZN8YS%2BQhLOyOcIJtMc4dl" rel="nofollow">网站</a>。</p><blockquote>如果访问不了,可访问这个<a href="https://link.segmentfault.com/?enc=lznE9CMoeudaR9nELLLw0Q%3D%3D.M8FyT9gjAYEzsfZkrxrMX%2FuOsCXu48%2BC3rZyXXcIhOmw1mGm%2Fg4hs0hfLqof%2F7%2Fu" rel="nofollow">网站</a>。</blockquote><h3>react代码段</h3><p>今年还新开了一个项目,记录react的学习代码段,包含基础组件和hook函数两个部分,除此之外,还有在使用antd design组件库的组件基础上额外封装的组件。比如实现一个弹出框组件,一个倒计时组件一个手风琴组件等等,像hooks函数就更多了,比如useTimeout函数,useInterval等等,useBodyScrollLock函数等等。我还是举其中2个示例来说明吧。</p><h4>受控的input组件</h4><p>主要是样式去美化input组件,同时将input组件的value值和onchange事件暴露出去,在这里我使用的是css in js来美化输入框的,代码如下:</p><pre><code class="tsx">import styled from '@emotion/styled';
import React from 'react';
import type { SyntheticEvent } from 'react';
const StyleInput = styled.input`
box-sizing: border-box;
margin: 0;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: 'tnum';
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
padding: 4px 11px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 2px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
border-right-width: 1px;
outline: 0;
}
`;
type LiteralUnion<T extends U, U> = T & (U & {});
interface ControlledInputProps {
type: LiteralUnion<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week',
string
>;
value: string;
onChange(v: string): void;
placeholder: string;
}
const ControlledInput = (props: Partial<ControlledInputProps>) => {
const { value, onChange, ...rest } = props;
const onChangeHandler = (e: SyntheticEvent) => {
if (onChange) {
onChange((e.target as HTMLInputElement).value);
}
};
return (
<StyleInput value={value} onChange={onChangeHandler} {...rest}></StyleInput>
);
};</code></pre><p>并且每一个组件都有对应的tsx和jsx版本,也有相应的接口,也方便学习如何去实现封装组件,组件的实现个人认为封装的最复杂的是弹出框组件,因为我需要考虑2种方式使用,第一种是通过组件方式使用,第二种则是通过调用方法的方式来使用。</p><p>然后就是我们的hooks函数了,比如我们来看useBodyScrollLock函数的实现。</p><h4>useBodyScrollLock函数的实现</h4><p>在这个函数中,我们通过在useLayoutEffect生命周期钩子函数中获取到body元素,然后给body元素设置一个overflow为hidden的样式,就达到了滚动的锁定,顾名思义这个函数就是用来禁止页面的滚动的。我们来看完整代码如下:</p><pre><code class="ts">import { useLayoutEffect } from 'react';
const useBodyScrollLock = () => {
useLayoutEffect(() => {
const container = document.body;
const originOverflowStyle = window.getComputedStyle(container!).overflow;
container!.style.overflow = 'hidden';
return () => {
container!.style.overflow = originOverflowStyle;
};
}, []);
};
export default useBodyScrollLock;</code></pre><p>当然这只是一个简单的hook函数,很好理解,也还有更复杂的hook函数,比如useCopyToClipboard函数的实现,这里也不需要一一叙述了,想要查看更多实现思路,请看这个<a href="https://link.segmentfault.com/?enc=0FwpkgU9SN%2Fd1QogTPQ0RQ%3D%3D.Pe8QS7YmoTDq0cFCsyx4GrSvB74nphaNlTbpnoavDBtfymqEG2KLaaFDEvq4A2WzN4INV4UQw%2F39hp4hbnqfaA%3D%3D" rel="nofollow">网站</a>。</p><blockquote>如果访问不了,请看这个<a href="https://link.segmentfault.com/?enc=6wpCY1Q6D213MWrC9gf5Gg%3D%3D.pm70qEI2EL12VwGFWacrKAn3in1VgAQNLqTGGxEKbckqRHwbrPIu5QaRbPqa%2BwbqiRE3IKvazSH8oz4SUBHJcQ%3D%3D" rel="nofollow">网站</a></blockquote><p>以上就是我今年的输出了,当然除了这之外,我还在坚持写一部小说,不过这就不需要透露了,哈哈哈,因为我觉得我写的也不怎么样。</p><h2>最后</h2><p>总而言之,我今年的学习成果不算好也不算坏,但是去年立下的flag并没有完成,只能展望于2023年了,感谢阅读到这里,本文就到此为止了,与君共勉。</p>
三分钟学会go语言的变量定义
https://segmentfault.com/a/1190000042910744
2022-11-26T15:53:52+08:00
2022-11-26T15:53:52+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</blockquote><h2>前言</h2><blockquote>特别说明: 本文只适合新手学习</blockquote><p>这篇文章带我们入门go语言的定义变量的方式,其实和javascript很相似,所以特意总结在此。</p><p>在go语言中,也有变量和常量两种,首先我们来看变量的定义,定义变量我们分为定义单个变量和多个变量。</p><p>本文知识点总结如下图所示:</p><p><img src="/img/bVc4dID" alt="" title=""></p><h2>定义单个变量</h2><p>在定义单个变量中,我们通过var关键字来定义一个变量,其后跟变量名和变量类型,其中变量类型可以省略,语法结构如下:</p><pre><code class="go">var <变量名> <变量类型></code></pre><p>例如:</p><pre><code class="go">var name string</code></pre><p>以上代码表示定义一个变量名为name,变量类型为字符串的变量,注意go语言定义变量的类型始终是在最后。</p><blockquote>可以看到,和js定义变量区别不大,只不过是多了一个类型声明,其中这个类型声明还可以省略。</blockquote><h2>定义多个变量</h2><p>在go语言中,我们通过<code>,</code>操作符来定义多个变量,这里定义多个变量也分为两种情况,一种是多个变量都是同一类型,另外一种则是不同类型的多个变量,我们先来看第一种。</p><h3>定义相同类型的多个变量</h3><p>和定义单个变量一样,也是使用var关键字来定义,并且通过<code>,</code>来分隔,语法结构如下:</p><pre><code class="go">var <变量名1>,<变量名2>,<变量名3>,... <变量类型></code></pre><p>如:</p><pre><code class="go">var name1,name2,name3 string</code></pre><p>以上定义了name1,name2,name3 3个变量,变量的类型都是string。</p><p>可以看到以上是定义相同类型的多个变量,那么我们应该如何定义不同类型的多个变量呢?</p><p>很简单,用<code>()</code>包裹起来,然后也是使用<code>,</code>分隔,在每个变量名后面紧跟变量类型即可,语法如下:</p><pre><code class="go">var (<变量名1> <变量1类型>,<变量名2> <变量2类型>...)</code></pre><p>例如以下代码:</p><pre><code class="go">var (name string,age int)</code></pre><p>是不是很简单?</p><h2>变量的初始化</h2><p>在go语言中,定义了变量,同样也会有初始化的操作,也就是说给变量初始化值,也是通过<code>=</code>操作符后跟值即可。语法结构如下:</p><pre><code class="go">var <变量名> <变量类型>? = <变量值></code></pre><p>例如:</p><pre><code class="go">var name string = "eveningwater"</code></pre><h2>变量类型的省略</h2><p>其实我们在初始化变量的时候可以省略变量类型,然后go编译器会在编译的时候帮我们自动推导变量类型,这简直就是在写javascript啊,这也是我在以上语法变量类型那一块中标注?的原因,就表示可以写可以不写。例如以上示例代码就可以写成:</p><pre><code class="go">var name = "eveningwater"</code></pre><p>如此看来,我们定义多个变量同样也可以省略变量类型,如:</p><pre><code class="go">var (name,age) = "eveningwater",26</code></pre><p>又或者是:</p><pre><code class="go">var name1,name2 = "eveningwater","xxx"</code></pre><h2>var关键字的省略(简短声明)</h2><p>go语言的定义变量名的关键字var也可以省略,这也是我没有想到的,如下所示:</p><pre><code class="go">name1,name2 := "eveningwater","xxx"</code></pre><p>感觉挺神奇的,是吧!go把这种省略了var和type的声明叫做<code>简短申明</code>。这样一来我们就可以在定义变量的时候用这个":="来定义变量了,不,你想多了,使用这种方式来定义变量是有限制的,那就是这种方式只能够作用在函数内部,如果我们要定义全局变量的话,还是要老老实实的写var关键字。</p><h3>全局变量与局部变量</h3><p>通过以上的说明,我们知道了定义全局变量和定义局部变量的方式,如下:</p><pre><code class="go">var a string = "hello" //全局变量
func test() {
b := " world" //局部变量
c := a + b
fmt.Printf("%s \n", c)
}</code></pre><h2>特别的变量名</h2><p>go语言有个很特殊的变量名,那就是下划线"_",为什么说它特殊呢,因为定义它的值都会被丢弃,没错,就是丢弃,例如:</p><pre><code class="go">_,num = 35,34</code></pre><p>其中_变量值为35将会被丢弃,最后就只剩下值为34的num变量了,感觉这种设计也是挺有意思的。</p><h2>未使用变量的限制</h2><p>go语言在编译阶段会对声明但未使用变量报错,比如以下代码就会报错: 声明了i变量但并未使用。</p><pre><code class="go">package main
func main(){
//编译阶段报错
var i int
}</code></pre><h2>常量</h2><p>常量其实也就是在程序编译阶段定下来的值吗,无法被修改,在go语言中常量也可以被定义成数值,布尔值或者是字符串等类型。它的语法结构如下:</p><pre><code class="go">const variableName = value;</code></pre><p>例如:</p><pre><code class="go">const num int = 10;</code></pre><p>其中常量的类型如果需要也可以加上。</p><p>以上就是本文内容了,感谢大家观看,看完本文,想来应该要不了几分钟,几分钟就掌握了go语言定义变量的概念和方式,想来还是值得的,非常适合新手学习,哈哈哈。</p>
一个有趣的交互效果的分析与实现
https://segmentfault.com/a/1190000042679801
2022-10-24T18:08:15+08:00
2022-10-24T18:08:15+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
5
<h2>一个有趣的交互效果的实现</h2><h3>效果分析</h3><p>最近在做项目,碰到了这样一个需求,就是页面有一个元素,这个元素可以在限定的区域内进行拖拽,拖拽完成吸附到左边或者右边,并且在滚动页面的时候,这个元素要半隐状态,停止滚动的时候恢复到原来的位置。如下视频所示:</p><p><img src="/img/bVc3e70" alt="" title=""></p><p>根据视频所展示的效果,我们得出了我们需要实现的效果主要有2个部分:</p><ul><li>拖拽并吸附</li><li>滚动半隐元素</li></ul><p>那么如何实现这2个效果呢?我们一个效果一个效果的来分析。</p><blockquote>ps: 由于这里采用的是react技术栈,所以这里以react作为讲解</blockquote><p>首先对于第一个效果,我们要想实现拖拽,有2种方式,第一种就是html5提供的拖拽api,还有一种就是监听鼠标的mousedown,mousemove和mouseup事件,由于这里兼容的移动端,所以我采用的是第二种实现方法。</p><p>思路是有了,接下来我想的就是将这三个事件封装一下,写成一个hook函数,这样方便调用,也方便扩展。</p><p>对于拖拽的实现,我们只需要在鼠标按下的时候,记录一下横坐标x和纵坐标y,在鼠标拖动的时候用当前拖动的横坐标x和横坐标y去与鼠标按下的时候的横坐标x与y坐标相减就可以得到拖动的偏移坐标,而这个偏移坐标就是我们最终要使用到的坐标。</p><p>在鼠标按下的时候,我们还需要减去元素本身所在的left偏移和top偏移,这样计算出来的坐标才是正确的。</p><p>然后,由于元素需要通过设置偏移来改变位置,因此我们需要将元素脱离文档流,换句话说就是元素使用定位,这里我采用的是固定定位。</p><h3>hooks函数的实现</h3><p>基于以上思路,一个任意拖拽功能实现的hooks函数就结构就成型了。</p><p>当然由于我们需要限定范围,这时候我们可以思考会有2个方向上的限定,即水平方向和垂直方向上的限定。除此之外,我们还需要提供一个默认的坐标值,也就是说元素默认应该是在哪个位置上。现在我们用伪代码来表示一下这个函数的结构,代码如下:</p><pre><code class="ts">const useLimitDrag = (el,options,container) => {
//核心代码
}
export default useLimitDrag;</code></pre><h3>参数类型</h3><p>这个hooks函数有3个参数,第一个参数自然是需要拖拽的元素,第二个参数则是配置对象,而第三个参数则是限定的容器元素。拖拽的元素和容器元素都是属于dom元素,在react中,我们还可以传递ref来表示一个dom元素,所以这两个参数,我们可以约定一下类型定义。我们先来定义元素的类型如下:</p><pre><code class="ts">export type ElementType = Element | HTMLElement | null;</code></pre><p>dom元素的类型就是Element | HTMLElement这2个类型,现在我们知道react的ref可以传递dom元素,并且我们还可以传入一个函数当作参数,所以基于这个类型,我们又额外的扩展了参数的类型,也方便配置。让我们继续写下如下代码:</p><pre><code class="ts">import type { RefObject } from 'react';
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;</code></pre><p>这样el和container元素的类型就一目了然,我们再定义一个类型简单合并一下这两个类型,代码如下:</p><pre><code class="ts">export type ParamType = RefElementType | FunctionElementType;</code></pre><p>接下来,让我们看配置对象,配置对象主要有2个地方,第一个就是默认值,第二个则是限定方向,因此我们约定了3个参数,islimitX,isLimitY,defaultPosition,并且配置对象都应该是可选的,我们可以使用Partial内置泛型将这个类型包裹一下,ok,来看看代码吧。</p><pre><code class="ts">export type OptionType = Partial<{
isLimitX: boolean,
isLimitY: boolean,
defaultPosition: {
x: number,
y: number
}
}>;</code></pre><p>嗯现在,我们可以修改一下以上的核心函数了,代码如下:</p><pre><code class="ts">const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType) => {
//核心代码
}
export default useLimitDrag;</code></pre><h3>返回值类型</h3><p>下一步,我们需要确定我们返回的值,首先肯定是当前被计算出来的x和y坐标,其次由于我们这个需求还有一个吸附效果,这个吸附效果是什么意思呢?就是说,以屏幕的中间作为划分界限为左右两部分,当拖动的x坐标大于中间,那么就吸附到最右边,否则就吸附到最左边。</p><p>根据这个需求,我们可以将坐标分为最大x坐标,最小x坐标以及中间的x坐标,当然由于需求只提到了水平方向上的吸附,垂直方向上并没有,但是为了考虑扩展,与之对应的我们同样要分成最大y坐标,最小y坐标以及中间的y坐标。</p><p>最后,我们还可以返回一个是否正在拖动中,方便我们做额外的操作。根据描述,以上的代码我们也就可以构造如下:</p><pre><code class="ts">export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
//
const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType): PositionType => {
//核心代码
}
export default useLimitDrag;</code></pre><h3>核心代码实现第一步---判断当前环境</h3><p>最基本的结构搭建好了,接下来第一步,我们要做什么?首先当然是判断当前环境是否表示移动端啊。那么如何判断呢?浏览器提供了一个navigator对象,通过这个对象的userAgent属性我们就可以判断,这个属性是一个很长的字符串,但是我们可以从其中一些值看出一些端倪,在移动端的环境中,通常都会看到iPhone|iPod|Android|ios这些字符串值,比如在iphone手机中就会有iPhone字符串,同理android也是。所以我们就可以通过写一个正则表达式来匹配这些字符串,如果有这些字符串就代表是移动端环境,否则就是pc浏览器环境,代码如下:</p><pre><code class="ts">const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);</code></pre><p>我们为什么要判断是否是移动端环境?因为在移动端环境,我们通常监听的是触摸事件,即touchstart,touchmove与touchend,而非mousedown,mousemove和mouseup。所以下一行代码自然就是定义好事件呢。如下:</p><pre><code class="ts">const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];</code></pre><h3>核心代码实现第二步---一些初始化工作</h3><p>下一步,我们通过useRef方法来存储拖拽元素和限定拖拽容器元素。代码如下:</p><pre><code class="ts">const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();</code></pre><p>接着我们获取配置对象的值,然后我们定义最大边界的值,代码如下:</p><pre><code class="ts">const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}</code></pre><p>随后,我们用一个变量代表鼠标是否按下的状态,这样做的目的是让拖拽变得更丝滑流畅一些,而不容易出问题,然后我们用useState定义返回的值,再定义一个对象存储鼠标按下时的x坐标和y坐标的值。代码如下:</p><pre><code class="ts">let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}</code></pre><p>另外为了确保拖动在限定区域内,我们需要设置滚动截断的样式,让元素不能在出现滚动条后还能拖动,因为这样会出现问题。我们定义一个方法用来设置,代码如下:</p><pre><code class="ts">const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}</code></pre><p>这个方法也就比较好理解了,如果使用的时候传入isLimitX那么就设置overflowX为hidden,否则不设置,y方向同理。</p><h3>核心代码的实现第三步---监听事件</h3><p>接下来,我们在react的钩子函数中监听事件,此时有了一个选择就是钩子函数我们使用useEffect还是useLayoutEffect呢?要决定使用哪个,我们需要知道这两个钩子函数的区别,这个超出了本文范围,不提及,可以查阅相关资料了解,这里我选择的是useLayoutEffect。</p><p>在钩子函数的回调函数中,我们首先将拖拽元素和容器元素存储下来,然后如果拖拽元素不存在,我们就不执行后续事件,回调函数返回一个函数,在该函数中我们移除对应的事件。代码如下:</p><pre><code class="ts">useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);</code></pre><h3>核心代码实现第四步---拖动开始事件回调</h3><p>接下来,我们来看一下onStartHandler函数的实现,在这个函数中,我们主要其实就是存储按下时候的坐标值,并且设置状态以及拖拽元素的鼠标样式和滚动截断的样式,随后当然是监听拖动和拖动结束事件,代码如下:</p><pre><code class="ts">const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);</code></pre><p>pc端是可以直接从事件对象中拿出来坐标,可是在移动端我们要通过一个changedTouches属性,这个属性是一个伪数组,第一项就是我们要获取到的坐标值。</p><p>接下来就是拖动事件的回调函数以及拖动结束的回调函数的实现了。</p><h3>核心代码实现第五步---拖动事件回调</h3><p>这是一个最核心实现的回调,我们在这个函数当中是要计算坐标的,首先当然是根据isStart状态来确定是否执行后续逻辑,其次,还要获取到当前拖拽元素,因为我们要根据这个拖拽元素的宽高做坐标的计算,另外还要获取到容器元素,如果没有提供容器元素,那么就是我们最开始定义的globalWidthHeight中取,然后获取鼠标按下时的x和y坐标值,将当前移动的x坐标和y坐标分别与按下时相减,就是我们的移动x坐标和y坐标,如果有设置isLimitX和isLimitY,我们还要额外设置滚动截断样式,并且我们通过将0和moveX以及最大值(也就是屏幕或者是容器元素的宽高减去拖拽元素的宽高)得到我们的最终的moveX和moveY值。</p><p>最后,我们将最终的moveX和moveY用react的状态存储起来即可。代码如下:</p><pre><code class="ts">const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement ||globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);</code></pre><h3>核心代码实现第六步--拖动结束回调</h3><p>最后在拖动结束后,我们需要重置我们做的一些操作,比如样式溢出截断,再比如移除事件的监听,以及恢复鼠标的样式等。代码如下:</p><pre><code class="ts">const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);</code></pre><p>到此为止,我们的第一个效果核心实现就已经算是完成大半部分了,最后我们再把需要用到的状态值返回出去。代码如下:</p><pre><code class="ts">return {
...position,
isMove
}</code></pre><p>合并以上的代码,就成了我们最终的hooks函数,代码如下:</p><pre><code class="ts">import { useState, useCallback, useLayoutEffect, useRef } from 'react';
import type { RefObject } from 'react';
export type ElementType = Element | HTMLElement | null;
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;
export type ParamType = RefElementType | FunctionElementType;
export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
export type OptionType = Partial<{
isLimitX: boolean,
isLimitY: boolean,
defaultPosition: {
x: number,
y: number
}
}>;
const useAnyDrag = (el: ParamType, option: OptionType = { isLimitX: true, isLimitY: true,defaultPosition:{ x:0,y:0 } }, containerRef?: ParamType): PositionType => {
const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];
const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();
const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}
let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}
const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}
const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);
const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement || globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);
const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);
useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);
return {
...position,
isMove
}
}
export default useAnyDrag;</code></pre><p>接下来我们来看第二个效果的实现。</p><h3>半隐效果的实现分析</h3><p>第二个效果实现的难点在哪里?我们都知道监听元素的滚动事件可以知道用户正在滚动页面,可是我们并不知道用户是否停止了滚动,而且也没有相关的事件或者是API能够让我们去监听用户停止了滚动,那么难点就在这里,如何知道用户是否停止了滚动。</p><p>要解决这个问题,我们还得从滚动事件中作文章,我们知道如果用户一直在滚动页面的话,滚动事件就会一直触发,假设我们在该事件中延迟个数百毫秒执行某个操作,是否就代表用户停止了滚动,然后我们可以执行相应的操作?</p><p>幸运的是,我从<a href="https://link.segmentfault.com/?enc=ecBqBpr%2B51fkinX0hizdhg%3D%3D.uHpyisFH%2FyPBHGvcLB5HTesA6ZdGn9iqe7iWT5GC3oFTiAikuQkwq8FXCff%2FrLwRdjN0bGsy0jb8rj7P31ft%2BMNL4vcE1Ps0xEnTes3P71ipGQQmRac8hP9BCLK3T5%2BR" rel="nofollow">这里</a>找到了答案,还真的是这么做。</p><p>如此一来,这个效果我们就实现了一大半了,我们实现一个useIsScroll函数,然后返回一个布尔值代表用户是否正在滚动和停止滚动两种状态,为了完成额外的操作,我们还可以返回一个用户滚动停止时的当前元素距离文档顶部的一个距离,也就是scrollTop。</p><p>如此一来,这个函数的实现就比较简单了,还是在react钩子函数中监听滚动事件,然后执行修改状态值的操作。但是现在还有一个问题,那就是我们如何去存储状态?</p><h3>核心代码实现第一步--解决状态存储的响应式</h3><p>如果使用useState来存储的话,似乎并没有达到响应式,好在react还提供了一个useRef函数,这个是一个响应式的,我们可以基于这个hooks函数结合useReducer函数来封装一个useGetSet函数,这个函数也在<a href="https://link.segmentfault.com/?enc=mGOv1GcztW9flm0M56%2BeaA%3D%3D.C%2B23nb82tFlYfx6VcFZCJ3WfTC7sNaK3Tx%2FyL0WvHYzQ0e3Nyo8bctsoxIQpMflVHC1kuYmdoxozut96zUvD%2FeOaS%2F55OjjP7KCrKAEq6YE%3D" rel="nofollow">这里</a>有总结到,感兴趣的可以去看看。</p><p>这个函数的实现其实也不难,主要就是利用useReducer的第二个参数强行去更新状态值,然后返回更新后的状态值。代码如下:</p><pre><code class="ts">export const useGetSet = <T>(initialState:T):[() => T,(s:T) => void] => {
const state = useRef<T>(initialState);
const [,update] = useReducer(() => Object.create(null),{});
const updateState = (newState: T) => {
state.current = newState;
update();
}
return useMemo(() => [
() => state.current,
updateState
],[])
}</code></pre><h3>核心代码实现第二步--构建hooks函数</h3><p>接下来我们来看这个hooks函数,很明显这个hooks函数有2个参数,第一个则是监听滚动事件的滚动元素,第二个则是一个延迟时间。滚动元素的类型与前面的拖拽函数保持一致,我们来详细看看。</p><pre><code class="ts">const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
//核心代码
}</code></pre><p>需要注意这里设置了默认值,el默认值时window对象,throlleTime默认值时300</p><p>接下来我们就是使用useSetGet方法存储状态,定义一个timer用于延迟函数定时器,然后监听滚动事件,在事件的回调函数中执行相应的修改状态值的操作,最后就是返回这两个状态值即可。代码如下:</p><pre><code class="ts">const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
const [isScroll,setIsScroll] = useGetSet(false);
const [scrollTop,setScrollTop] = useGetSet(0);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const onScrollHandler = useCallback(() => {
setIsScroll(true);
setScrollTop(window.scrollY);
if(timer.current){
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setIsScroll(false);
},throlleTime)
},[])
useLayoutEffect(() => {
const ele = typeof el === 'function' ? (el as () => T)() : el;
if(!ele){
return;
}
ele.addEventListener('scroll',onScrollHandler,false);
return () => {
ele.removeEventListener('scroll',onScrollHandler,false);
}
},[]);
return {
isScroll: isScroll(),
scrollTop: scrollTop()
};
}</code></pre><p>整个hooks函数代码实现起来简单明了,所以也没什么难点,只要理解到了思路,就很简单了。</p><h3>两个hooks函数的使用</h3><p>核心功能我们已经实现了,接下来使用起来也比较简单,样式代码如下:</p><pre><code class="css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,html {
height: 100%;
}
body {
overflow:auto;
}
.App {
position: relative;
}
.overHeight {
height: 3000px;
}
.drag {
position: fixed;
width: 150px;
height: 150px;
border: 3px solid #2396ef;
background: linear-gradient(135deg,#efac82 10%,#da5b0c 90%);
z-index: 2;
left: 0;
top: 0;
}
.transition {
transition: all 1.2s ease-in-out;
}</code></pre><p>组件代码如下:</p><pre><code class="tsx">import useAnyDrag from "./hooks/useAnyDrag";
import './App.css';
import useIsScroll from "./hooks/useIsScroll";
import { createRef } from "react";
const App = () => {
// 这里是使用核心代码
const { x, y, isMove, centerX, minX, maxX } = useAnyDrag(() => document.querySelector('#drag'));
//这里是使用核心代码
const {isScroll} = useIsScroll();
const scrollElement = createRef<HTMLDivElement>();
const getLeftPosition = () => {
if (!x || !centerX || isMove) {
return x;
}
if (x <= centerX) {
return minX || 0;
} else {
return maxX;
}
}
const scrollPosition = () => {
if (typeof getLeftPosition() === 'number') {
if (getLeftPosition() === 0) {
return -((scrollElement.current?.offsetWidth || 0) / 2);
} else {
return getLeftPosition() as number + (scrollElement.current?.offsetWidth || 0) / 2;
}
}
return 0;
}
return (
<div className="App">
<div className="overHeight"></div>
<div className={`${ isScroll ? 'drag transition' : 'drag'}`}
style={{ left: (isScroll ? scrollPosition() : getLeftPosition()) + 'px', top: y + 'px' }}
id="drag"
ref={scrollElement}
></div>
</div>
)
}
export default App;</code></pre><h3>结语</h3><p>经过以上的分析,我们就完成了这样一个需求,感觉实现完了之后,还是收获满满的,总结一下我学到了什么。</p><ol><li>拖拽事件的监听以及拖拽坐标的计算</li><li>滚动事件的监听以及react响应式状态的实现</li><li>移动端环境与pc环境的判断</li><li>如何知道用户停止了滚动</li></ol><p>本文就到此为止了,感谢大家观看。</p>
利用思否猫素材实现一个丝滑的轮播图(html + css + js)
https://segmentfault.com/a/1190000042661646
2022-10-21T11:18:16+08:00
2022-10-21T11:18:16+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
16
<h2>使用思否猫素材实现一个轮播图</h2><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042567232">1024程序员节</a>,欢迎正在阅读的你也加入。</blockquote><p>通过本文,你将学到:</p><ul><li>html</li><li>css</li><li>js</li></ul><p>没错,就是html,css,js,现在是框架盛行的时代,所以很少会有人在意原生三件套,通过本文实现一个丝滑的轮播图,带你重温html,css和js基础知识。</p><p>为什么选用轮播图做示例?有如下几点:</p><ul><li>业务当中最常用</li><li>轮播图说简单也不简单,说复杂也不复杂,可以说是一切项目的基石</li><li>轮播图更适合考察你对html,css,js的基础掌握</li></ul><p>废话不多说,让我们先来看一下效果图,如下:</p><p><img src="/img/bVc3ao9" alt="" title=""></p><p>通过上图,我们可以知道,一个轮播图包含了三大部分,第一部分是轮播图的部分,第二部分则是轮播翻页部分,第三部分则是上一页和下一页。</p><p>所以一个轮播图的结构我们基本上就清晰了,让我们来详细看一下吧。</p><h3>html文档结构</h3><p>首先我们要有一个容器元素,如下:</p><pre><code class="html"><!--容器元素-->
<div class="carousel-box"></div></code></pre><p>然后,我们第一部分轮播图也需要一个容器元素,随后就是轮播图的元素列表,结构如下:</p><pre><code class="html"> <div class="carousel-content">
<div class="carousel-item active">
<img src="https://www.eveningwater.com/img/segmentfault/1.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/2.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/3.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/4.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/5.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/6.png" alt="" class="carousel-item-img">
</div>
<div class="carousel-item">
<img src="https://www.eveningwater.com/img/segmentfault/7.png" alt="" class="carousel-item-img">
</div>
</div></code></pre><p>分析下来就三个,容器元素,每一个轮播图元素里面再套一个图片元素。</p><p>接下来是第二部分,同样的也是一个容器元素,套每一个轮播点元素,如下:</p><pre><code class="html"><div class="carousel-sign">
<div class="carousel-sign-item active">0</div>
<div class="carousel-sign-item">1</div>
<div class="carousel-sign-item">2</div>
<div class="carousel-sign-item">3</div>
<div class="carousel-sign-item">4</div>
<div class="carousel-sign-item">5</div>
<div class="carousel-sign-item">6</div>
</div></code></pre><p>无论是轮播图部分还是轮播分页部分,都加了一个active类名,作为默认显示和选中的轮播图和轮播分页按钮。</p><p>第三部分,则是上一页和下一页按钮,这里如果我们将最外层的轮播容器元素设置了定位,这里也就不需要一个容器元素了,我们直接用定位,下一节写样式会详细说明。我们还是来看结构,如下:</p><pre><code class="html"><div class="carousel-ctrl carousel-left-ctrl">&lt;</div>
<div class="carousel-ctrl carousel-right-ctrl">&gt;</div></code></pre><p>这里采用了html字符实体用作上一页和下一页文本,关于什么是html字符实体,可以参考相关文章,这里不做详解。</p><p>通过以上的分析,我们一个轮播图的文档结构就完成了,接下来,让我们编写样式。</p><h3>编写样式</h3><p>首先我们根据效果图可以知道,容器元素,轮播图部分容器元素以及每一个轮播图元素都是百分之百宽高的,样式如下:</p><pre><code class="css">.carousel-box,.carousel-content,.carousel-item ,.carousel-item-img {
width: 100%;
height: 100%;
}</code></pre><p>其次,容器元素和轮播图元素,我们需要设置成相对定位,为什么轮播图也要设置成相对定位?因为我们这里是使用的绝对定位加left和right偏移从而实现的滑动轮播效果。</p><pre><code class="css">.carousel-box,.carousel-item {
position: relative;
}</code></pre><p>然后,由于轮播图只显示当前的轮播图,而超出的部分也就是溢出部分我们需要截断隐藏,因此为容器元素设置截断隐藏。</p><pre><code class="css">.carousel-box {
overflow: hidden;
}</code></pre><p>接着,每一个轮播图元素默认都是隐藏的,只有加了active类名,才显示。</p><pre><code class="css">.carousel-item {
display: none;
}
.carousel-item.active {
display: block;
left: 0;
}</code></pre><p>再然后分别是向左还是向右,这里我们是通过添加类名的方式来实现滑动,所以我们在这里额外为轮播元素增加了left和right类名,如下:</p><pre><code class="css">.carousel-item.active.left {
left: -100%;
}
.carousel-item.active.right {
left: 100%;
}</code></pre><p>有意思的点还在这里,就是每一个轮播图元素还额外的增加了next和prev类名,为什么要增加这两个类名?试想我们当前轮播图显示的时候,前面的是不是应该被隐藏,而后面的下一个应该是紧紧排在当前轮播图之后,然后做准备,而这两个类名的目的就是在这里,让效果看起来更加丝滑一些。</p><pre><code class="css">.carousel-item.next,
.carousel-item.prev {
display: block;
position: absolute;
top: 0;
}
.carousel-item.next {
left: 100%;
}
.carousel-item.prev {
left: -100%;
}
.carousel-item.next.left,
.carousel-item.prev.right {
left: 0%;
}</code></pre><p>最后补充一个轮播图片元素的样式,如下:</p><pre><code class="css">.carousel-item-img {
object-fit: cover;
}</code></pre><p>到了这里,其实轮播图的核心思路已经出现了,就是利用的绝对定位加left偏移来实现,而在javascript逻辑中,我们只需要操作类名就可以了。</p><p>这样做的好处很显然,我们将动画的逻辑包装在css中,因此轮播的动画逻辑也比较好修改,修改css代码总比修改js代码简单吧?</p><p>到了这里,轮播部分的样式我们就已经完成了,接下来看分页按钮组的样式。</p><p>根据图片示例,分页按钮组元素是在底部的,其实分页按钮组也可以说是很常规的按钮布局,所以样式都是一些很基础的,也没有必要做太多的详解。</p><pre><code class="css">.carousel-sign {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
padding: 5px 3px;
border-radius: 6px;
user-select: none;
background: linear-gradient(135deg,#73a0e4 10%,#1467e4 90%);
}
.carousel-sign-item {
width: 22px;
height: 20px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-align: center;
float: left;
color:#f2f3f4;
margin: auto 4px;
cursor: pointer;
border-radius: 4px;
}
.carousel-sign-item:hover {
color:#fff;
}
.carousel-sign-item.active {
color:#535455;
background-color: #ebebeb;
}</code></pre><p>最后就是上一页下一页的样式,有意思的是上一页下一页默认是不应该显示的,鼠标悬浮到轮播图容器元素上,才会显示,所以这里也用到了定位。</p><pre><code class="css">.carousel-ctrl {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 30px;
font-weight: 300;
user-select: none;
background: linear-gradient(135deg,#73a0e4 10%,#1467e4 90%);
color: #fff;
border-radius: 5px;
cursor: pointer;
transition: all .1s cubic-bezier(0.075, 0.82, 0.165, 1);
text-align: center;
padding: 1rem;
}
.carousel-ctrl.carousel-left-ctrl {
left: -50px;
}
.carousel-ctrl.carousel-right-ctrl {
right: -50px;
}
.carousel-box:hover .carousel-ctrl.carousel-left-ctrl {
left: 10px;
}
.carousel-box:hover .carousel-ctrl.carousel-right-ctrl {
right: 10px;
}
.carousel-ctrl:hover {
background-color: rgba(0,0,0,.8);
}</code></pre><p>到了这里,样式的布局就完成了,接下来是javascript核心逻辑,让我们一起来看一下吧。</p><h3>轮播图的核心逻辑</h3><p>我们将轮播图封装在一个类当中,然后通过构造函数调用的方式来使用它,我们先来看使用方式,如下:</p><pre><code class="js">const options = {
el: '.carousel-box',
speed: 1000, // 轮播速度(ms)
delay: 0, // 轮播延迟(ms)
direction: 'left', // 图片滑动方向
monitorKeyEvent: true, // 是否监听键盘事件
monitorTouchEvent: true // 是否监听屏幕滑动事件
}
const carouselInstance = new Carousel(options);
carouselInstance.start();</code></pre><p>通过使用方式,我们得到了什么?</p><ul><li><p>轮播图的配置对象</p><ul><li>el: 容器元素</li><li>speed: 轮播速度</li><li>delay: 轮播执行延迟时间</li><li>direction: 滑动方向,主要有left和right两个值</li><li>monitorKeyEvent: 是否监听键盘事件,也就是说是否可以通过点击键盘上的左右来切换轮播图</li><li>monitorTouchEvent: 是否监听屏幕滑动事件,也就是说是否可以通过滑动屏幕来切换图片</li></ul></li><li>Carousel是一个构造函数</li><li>carousel构造函数内部提供了一个start方法用来开始轮播,很显然这里是开始自动轮播</li></ul><p>根据以上的分析,让我们来一步步实现Carousel这个东西吧。</p><p>首先它是一个构造函数,支持传入配置对象,所以,我们定义一个类,并且这个类还有一个start方法也初始化,如下:</p><pre><code class="js">class Carousel {
constructor(options){
//核心代码
}
start(){
//核心代码
}
}</code></pre><p>在初始化的时候我们需要做什么?</p><p>首先我们要获取到轮播元素,还有上一页下一页按钮以及我们的分页按钮元素。如下:</p><pre><code class="js">class Carousel {
constructor(options){
// 容器元素
this.container = $(options.el);
// 轮播图元素
this.carouselItems = this.container.querySelectorAll('.carousel-item');
// 分页按钮元素组
this.carouselSigns = $$(('.carousel-sign .carousel-sign-item'),this.container);
// 上一页与下一页
this.carouselCtrlL = $$(('.carousel-ctrl'),this.container)[0];
this.carouselCtrlR = $$(('.carousel-ctrl'),this.container)[1];
}
start(){
//核心代码
}
}</code></pre><p>这里用到了$和$$方法,看起来和jQuery的获取DOM很像,用到了jQuery?那当然不是了,我们来看这2个方法的实现。</p><pre><code class="js">const $ = (v,el = document) => el.querySelector(v);
const $$ = (v,el = document) => el.querySelectorAll(v);</code></pre><p>也就是获取dom元素的简易封装啦。</p><ul><li>document.querySelector API</li><li>document.querySelectorAll API</li></ul><p>就是基于以上两个dom查询节点的方法封装的,让我们来看下一步,首先我们需要有一个确定当前轮播图的索引值,然后获取所有轮播图元素的长度,然后就是初始化配置对象。代码如下:</p><pre><code class="js">class Carousel {
constructor(options){
//省略了代码
// 当前图片索引
this.curIndex = 0;
// 轮播盒内图片数量
this.numItems = this.carouselItems.length;
// 是否可以滑动
this.status = true;
// 轮播速度
this.speed = options.speed || 600;
// 等待延时
this.delay = options.delay || 3000;
// 轮播方向
this.direction = options.direction || 'left';
// 是否监听键盘事件
this.monitorKeyEvent = options.monitorKeyEvent || false;
// 是否监听屏幕滑动事件
this.monitorTouchEvent = options.monitorTouchEvent || false;
}
//省略了代码
}</code></pre><p>初始化完成之后,接下来我们有两个逻辑还需要在初始化里面完成,第一个逻辑是添加动画过渡效果,也就是让动画看起来更丝滑一些,第二个就是添加事件逻辑。继续在构造函数中调用2个方法,代码如下:</p><pre><code class="js">class Carousel {
constructor(options){
//省略了代码
// 添加了事件
this.handleEvents();
// 设置过渡效果
this.setTransition();
}
//省略了代码
}</code></pre><p>我们先来看最简单的setTransition方法,其实这个方法很简单,就是通过在head标签内添加一个style标签,通过insertRule方法添加样式。代码如下:</p><pre><code class="js">setTransition() {
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
const styleRule = `.carousel-item {transition: left ${this.speed}ms ease-in-out}`
styleElement.sheet.insertRule(styleRule, 0);
}</code></pre><p>很显然这里是为每个轮播图元素添加过渡效果,接下来我们来看绑定事件方法内部,我们可以尝试思考一下,都有哪些事件呢?总结如下:</p><ul><li>上一页与下一页</li><li>分页按钮组</li><li>滑动事件</li><li>键盘事件</li><li>轮播盒子元素的鼠标悬浮与鼠标移出事件</li></ul><p>根据以上分析,我们的handleEvents方法就很好实现了,如下:</p><pre><code class="js">handleEvents() {
// 鼠标从轮播盒上移开时继续轮播
this.container.addEventListener('mouseleave', this.start.bind(this));
// 鼠标移动到轮播盒上暂停轮播
this.container.addEventListener('mouseover', this.pause.bind(this));
// 点击左侧控件向右滑动图片
this.carouselCtrlL.addEventListener('click', this.clickCtrl.bind(this));
// 点击右侧控件向左滑动图片
this.carouselCtrlR.addEventListener('click', this.clickCtrl.bind(this));
// 点击分页按钮组后滑动到对应的图片
for (let i = 0; i < this.carouselSigns.length; i++) {
this.carouselSigns[i].setAttribute('slide-to', i);
this.carouselSigns[i].addEventListener('click', this.clickSign.bind(this));
}
// 监听键盘事件
if (this.monitorKeyEvent) {
document.addEventListener('keydown', this.keyDown.bind(this));
}
// 监听屏幕滑动事件
if (this.monitorTouchEvent) {
this.container.addEventListener('touchstart', this.touchScreen.bind(this));
this.container.addEventListener('touchend', this.touchScreen.bind(this));
}
}</code></pre><p>这里有意思的点在于bind方法更改this对象,使得this对象指向当前轮播实例元素,还有一点就是我们为每个分页按钮设置了一个slide-to的索引值,后续我们就可以根据这个索引值来切换轮播图。</p><p>接下来,让我们看看每一个事件对应的回调方法,首先是start方法,其实很容易就想到,start方法就是开始轮播,开始轮播也就是自动轮播,自动轮播我们需要用到定时器,因此我们的start函数就很好实现了,代码如下:</p><pre><code class="js">start() {
const event = {
srcElement: this.direction == 'left' ? this.carouselCtrlR : this.carouselCtrlL
};
const clickCtrl = this.clickCtrl.bind(this);
// 每隔一段时间模拟点击控件
this.interval = setInterval(clickCtrl, this.delay, event);
}</code></pre><p>这里有意思的点在于我们的自动轮播,是直接去模拟点击上一页下一页进行切换的,除此之外,这里根据方向将srcElement元素作为事件对象传递,也就是说后面我们会根据这个元素来做方向上的判断。</p><p>接下来我们来看暂停函数,很简单,就是清除定时器即可,如下:</p><pre><code class="js">// 暂停轮播
pause() {
clearInterval(this.interval);
}</code></pre><p>接下来,让我们来看clickCtrl方法,思考一下,我们是如何修改当前轮播图的索引值的,正常情况下,比如说,我们是向左滑动,索引值实际上就是将当前索引值相加,然后再判断是否超出了轮播图元素组的长度,重置索引值。</p><p>但是这里有一个更为巧妙的实现方式,那就是取模,将当前索引值加1然后与轮播图元素组的长度取模,这样也就保证了我们的索引值始终不会超过轮播图元素组的长度。</p><p>如果是向右滑动,那么我们应该是加上轮播图元素组的长度 - 1再取模,也就是与向左方向相反,根据以上分析,我们的代码就好实现了,如下:</p><pre><code class="js">// 处理点击控件事件
clickCtrl(event) {
if (!this.status) return;
this.status = false;
let fromIndex = this.curIndex, toIndex, direction;
if (event.srcElement == this.carouselCtrlR) {
toIndex = (this.curIndex + 1) % this.numItems;
direction = 'left';
} else {
toIndex = (this.curIndex + this.numItems - 1) % this.numItems;
direction = 'right';
}
this.slide(fromIndex, toIndex, direction);
this.curIndex = toIndex;
}</code></pre><p>在这个方法里面还有一个slide方法,顾名思义就是轮播图的滑动方法,其实通篇下来,最核心的就是这个slide方法,这个方法主要做的逻辑就是根据索引值来切换class。代码如下:</p><pre><code class="js">slide(fromIndex, toIndex, direction) {
let fromClass, toClass;
if (direction == 'left') {
this.carouselItems[toIndex].className = "carousel-item next";
fromClass = 'carousel-item active left';
toClass = 'carousel-item next left';
} else {
this.carouselItems[toIndex].className = "carousel-item prev";
fromClass = 'carousel-item active right';
toClass = 'carousel-item prev right';
}
this.carouselSigns[fromIndex].className = "carousel-sign-item";
this.carouselSigns[toIndex].className = "carousel-sign-item active";
setTimeout((() => {
this.carouselItems[fromIndex].className = fromClass;
this.carouselItems[toIndex].className = toClass;
}).bind(this), 50);
setTimeout((() => {
this.carouselItems[fromIndex].className = 'carousel-item';
this.carouselItems[toIndex].className = 'carousel-item active';
this.status = true; // 设置为可以滑动
}).bind(this), this.speed + 50);
}</code></pre><p>这里分成了两块替换类名的逻辑,第一块是轮播图元素的替换类名,需要判断方向,第二块则是分页按钮组的类名替换逻辑,当然分页按钮组的逻辑是不需要替换类名的。</p><p>其实分析到这里,这个轮播图的核心基本已经完成了,接下来就是完善了,让我们继续看分页按钮组的事件回调逻辑。</p><p>其实这里的逻辑也就是拿到索引值,前面为每个分页按钮组设置了一个slide-to属性,这里很显然就通过获取这个属性,然后生成slide方法所需要的参数,最后调用slide方法就行了。代码如下:</p><pre><code class="js">clickSign(event) {
if (!this.status) return;
this.status = false;
const fromIndex = this.curIndex;
const toIndex = parseInt(event.srcElement.getAttribute('slide-to'));
const direction = fromIndex < toIndex ? 'left' : 'right';
this.slide(fromIndex, toIndex, direction);
this.curIndex = toIndex;
}</code></pre><p>最后还剩两个逻辑,一个是键盘事件,另一个是滑动事件逻辑,我们先来看键盘事件逻辑,键盘事件逻辑无非就是利用keyCode判断用户是否点击的是上一页和下一页,然后像自动开始轮播那样模拟调用上一页下一页按钮事件逻辑即可。代码如下:</p><pre><code class="js">keyDown(event) {
if (event && event.keyCode == 37) {
this.carouselCtrlL.click();
} else if (event && event.keyCode == 39) {
this.carouselCtrlR.click();
}
}</code></pre><p>最后就是滑动事件了,滑动事件最难的点在于如何判断用户滑动的方向,其实我们可以通过计算滑动角度来判断,这里的计算逻辑是来自网上的一个公式,感兴趣的可以查阅相关资料了解。我们来看代码如下:</p><pre><code class="js">touchScreen(event) {
if (event.type == 'touchstart') {
this.startX = event.touches[0].pageX;
this.startY = event.touches[0].pageY;
} else { // touchend
this.endX = event.changedTouches[0].pageX;
this.endY = event.changedTouches[0].pageY;
// 计算滑动方向的角度
const dx = this.endX - this.startX
const dy = this.startY - this.endY;
const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
// 滑动距离太短
if (Math.abs(dx) < 10 || Math.abs(dy) < 10) return;
if (angle >= 0 && angle <= 45) {
// 向右侧滑动屏幕,模拟点击左控件
this.carouselCtrlL.click();
} else if (angle >= 135 && angle <= 180) {
// 向左侧滑动屏幕,模拟点击右控件
this.carouselCtrlR.click();
}
}
}</code></pre><p>到此为止,我们的一个轮播图就完成了,总结一下我们学到了什么?</p><ul><li>CSS过渡效果</li><li>CSS基本布局</li><li><p>javascript事件</p><ul><li>鼠标悬浮与移出事件</li><li>滑动事件</li><li>键盘事件</li><li>元素点击事件</li></ul></li><li>javascript定时器</li></ul><blockquote>PS: 以后再也不用自己写轮播了,业务当中遇到,可以直接拿去用了,😄。</blockquote><p>以上源码在<a href="https://link.segmentfault.com/?enc=xk%2FAkm2p%2B0LkTJykBmUiNA%3D%3D.HpyrtPMEh4xRgoRFTpkVkmj1YvtNwkffkkzApc6kMPWQ4k0Q90uvbAbGAOHMqOXq92kWNtFTnNRe56Ionnaks7YnOjRMPZwXY1OkC6eGi7g%3D" rel="nofollow">这里</a>。<br>以上示例在<a href="https://link.segmentfault.com/?enc=2gVnWFAuBHH3P795CYvGIQ%3D%3D.CQmB2nTRpjlz3abTGrEGEJn6BO5PI7yPGD9AHIXijn4NQJJEjGUM87AHCxUiu9BItzNwo0iPMT7NP85QOsLe5Q%3D%3D" rel="nofollow">这里</a>。</p>
利用思否猫素材实现一个连连看小游戏
https://segmentfault.com/a/1190000042643627
2022-10-18T20:43:30+08:00
2022-10-18T20:43:30+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
17
<h3>vue3实现一个思否猫连连看小游戏</h3><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042567232">1024程序员节</a>,欢迎正在阅读的你也加入。</blockquote><p>通过本文,你将学到:</p><ol><li>vue3的核心语法</li><li>vue3的状态管理工具pinia的用法</li><li>sass的用法</li><li>基本算法</li><li>canvas实现一个下雪的效果,一些canvas的基本用法</li><li>rem布局</li><li>typescript知识点</li></ol><h3>开始之前</h3><p>在开始之前,我们先来看一下最终的成品是怎么样的,如下图所示:</p><p>首页如下:</p><p><img src="/img/bVc25In" alt="" title=""></p><p>游戏页如下:</p><p><img src="/img/bVc25Io" alt="" title=""></p><p>如上图所示,我们本游戏包含了两部分,第一部分就是首页,第二部分则是游戏页面。然后首页我们又可以分成两个部分,第一部分则是下雪花的效果,第二部分就是一个背景图和按钮。游戏页面同理也是分成两个部分,第一个部分就是列表,第二个部分则是倒计时效果。</p><p>当然其实还有隐藏的第三部分,其实也就是一个弹框组件,因为游戏结束或者游戏赢了,我们要给予一个反馈,而这个反馈就是弹框组件。</p><p>所有页面分析完成,接下来让我们初始化一个vite工程项目。</p><h3>初始化工程</h3><p>首先在电脑上任意一个目录按住shift + 鼠标右键,选择打开powershell,也就是终端。然后输入如下命令:</p><pre><code class="shell">npm create vite <项目名> --template vue-ts</code></pre><p>然后一路回车,初始化完成工程,初始化完成之后,输入npm install,下载依赖,下载完依赖,由于我们使用到了sass,所以需要额外输入npm install sass --save-dev来安装sass依赖。当然由于我们可能会写tsx,所以我们也安装@vitejs/plugin-vue-jsx,还有就是我们设置导入路径的别名,需要用到node的path模块,所以也需要额外安装@types/node依赖。</p><blockquote>笔记: 初始化工程都是照着<a href="https://link.segmentfault.com/?enc=9MVLlnuRm48osnfehfpmTQ%3D%3D.ZNyB8cc7MyLjgz2Ilqq%2FvEATB2ELcHwyyt1M%2FhMCezY%3D" rel="nofollow">官网</a>文档来的。</blockquote><h3>修改配置与调整目录</h3><p>所有依赖安装完成之后,我们修改一下vite.config.ts的配置,如下:</p><pre><code class="ts">import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
base: "./", //打包路径配置
esbuild: {
jsxFactory: "h",
jsxFragment: "Fragment",
}, //tsx相关配置
server: {
port: 30001,
},//修改端口
resolve: {
alias: [
{
find: "@",
replacement: path.resolve(__dirname, "src"),
},
{
find: "~",
replacement: path.resolve(__dirname, "src/assets/"),
},
],
}, //配置@和~导入别名
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/style/variable.scss";`, //顾名思义,这里是一个定义变量scss文件,变量应该是作用于全局的,所以在这里全局导入
},
},
} //新增的导入全局scss文件的配置
})</code></pre><p>以上代码注释所解释的都是新增的配置,vite默认的配置就只有一个<code>plugins:[vue()]</code>。</p><p>修改完成配置之后,接下来我们来修改目录(主要是修改src目录)以及文件,修改后的目录应该如下所示:</p><pre><code class="ts">// assets: 存储静态资源的目录
// components: 公共组件目录
// core: 游戏核心逻辑目录
// directives: 指令目录
// store: 状态管理目录
// style: 样式目录
// utils: 工具函数目录
// views: 页面视图目录</code></pre><p>思考一下,我们这里需要用到vue-router吗?最开始我也是在思考,但是后面想了一下,这个页面很简单,暂时可以不需要,可是当我们后面进行扩展就需要了,比如自定义关卡和难度配置页面。</p><p>ok,调整好了,让我们继续下一步。</p><h3>定义接口</h3><p>由于本游戏我们会将游戏参数抽离出来,并且用到了typescript,所以我们可以额外的创建一个type.d.ts文件,用于定义全局的接口类型。并且vite工程已经帮我们做好了默认导入全局接口类型,所以我们不需要做额外的配置,在src目录下,新建type.d.ts文件,然后写上如下接口:</p><pre><code class="ts">enum Status {
START,
RUNNING,
ENDING
}
declare namespace GlobalModule {
export type LevelType = number | string;
export type ElementType = HTMLElement | Document | Window | null | Element;
export interface SnowOptionType {
snowNumber?: number;
snowShape?: number;
speed?: number;
}
export interface GameConfigType {
materialList:Record<string,string> [],
time: number,
gameStatus: Status
}
export interface MaterialData {
active: boolean
src: string
title?: string
id: string
isMatch: boolean
}
export type DocumentHandler = <T extends MouseEvent|Event>(mouseup:T,mousedown:T) => void;
export type FlushList = Map<HTMLElement,{ DocumentHandler:DocumentHandler,bindingFn:(...args:unknown[]) => unknown }>
}</code></pre><p>以上代码我们定义了一个全局命名空间GlobalModule,定义了一个枚举Status代表游戏的状态。然后我们来看命名空间里面所有的接口类型代表什么。</p><ul><li>LevelType: 数值或者字符串类型,这里是用作h1 ~ h6标签名的组成的类型,也就是说我们在后面将会封装一个Head组件,代表标题组件,组件会用到动态的标签名,也就是这里的1 ~ 6属性,它可以是字符串或者数值,所以定义在这里。</li><li>ElementType: 顾名思义,就是定义元素的类型,这在实现下雪花以及获取Dom元素当中用到。</li><li>SnowOptionType: 下雪花效果配置对象的类型,包含三个参数值,雪花数量,雪花形状以及雪花速度,都是数值类型。</li><li>GameConfigType: 游戏配置类型,materialList代表素材列表类型,是一个对象数组,因此定义成<code>Record<string,string> []</code>,time代表倒计时时间,gameStatus代表游戏状态。</li><li>MaterialData: 素材列表对象类型。</li><li>DocumentHandler: 文档对象回调函数类型,是一个函数,这在实现自定义指令中会用到。</li><li>FlushList: 用map数据结构存储元素节点的事件回调函数类型,也是用在实现自定义指令当中。</li></ul><h3>创建store</h3><p>在store目录下新建store.ts,写下如下代码:</p><pre><code class="ts">import { defineStore } from 'pinia'
import { defaultConfig } from '../core/gameConfig'
export const useConfigStore = defineStore('config',{
state:() => ({
gameConfig:{ ...defaultConfig }
}),
actions:{
setGameConfig(config:GlobalModule.GameConfigType) {
this.gameConfig = config;
},
reset(){
this.$reset();
}
}
})</code></pre><p>代码逻辑很简单,就是定义一个游戏配置的状态,以及修改游戏配置状态的action函数,这里有点意思的就是reset函数,this.$reset是哪里来的?可能会有人有疑问。</p><p>答案当然是pinia,因为pinia内部封装了一个重置状态的函数,我们可以直接拿来用就是啦。</p><p>随后,我们在main.ts文件里面,注入pinia。修改代码如下:</p><pre><code class="ts">import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
//新增的样式初始化文件
import "./style/reset.scss"
//新增的代码,调用createPinia函数
const pinia = createPinia()
//修改的代码
createApp(App).use(pinia).mount('#app')</code></pre><h3>游戏配置</h3><p>还有一个defaultConfig,也就是游戏默认配置,也非常简单,在core目录下,新建一个gameConfig.ts文件,添加如下代码:</p><pre><code class="ts">// 素材列表是可以随意更换的
export const BASE_IMAGE_URL = "https://www.eveningwater.com/my-web-projects/js/26/img/";
export const materialList: Record<string,string> [] = new Array(12).fill(null).map((item,index) => ({ title:`图片-${index + 1}`,src:`${BASE_IMAGE_URL + (index + 1)}.jpg`}));
export const defaultConfig: GlobalModule.GameConfigType = {
materialList,
time: 120,
gameStatus: 0
}</code></pre><p>这里面其实就做了两件事,第一件事当然是导出素材列表,第二件事就是导出游戏默认配置啦。</p><h3>初始化样式</h3><p>让我们继续,接下来,先初始化一些scss样式变量和初始化样式,在style目录下新建reset.scss和variable.scss文件。</p><ul><li>varaible.scss代码如下:</li></ul><pre><code class="scss">$prefix: bm-;
$white: #fff;
$black: #000;
@mixin setProperty($prop,$value){
#{$prop}:$value;
}
.flex-center {
@include setProperty(display,flex);
@include setProperty(justify-content,center);
@include setProperty(align-items,center);
}</code></pre><p>这个文件干了什么?</p><p>定义了一个class命名前缀bm-,用$prefix变量代表,接着定义了白色和黑色的变量。随后又定义了一个mixin setProperty。</p><p>纵观css无非就是属性名和属性值,所以我定义一个mixin传入两个参数,就是分别代表动态设置属性名和属性值。</p><blockquote>PS: 这里纯属添加了个人的爱好在里面,因为我喜欢这么写scss。</blockquote><p>至于用法,我想在flex-center里面已经体现出来了。就是@include setProperty(属性名,属性值)。</p><ul><li>reset.scss</li></ul><pre><code class="scss">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,html {
width: percentage(1);
height: percentage(1);
overflow: hidden;
background: url("~/header_bg.jpg") no-repeat center / cover;
}
.#{$prefix}clearfix::after {
@include setProperty(content,'');
@include setProperty(display,table);
@include setProperty(clear,both);
}
ul,li {
@include setProperty(list-style,none);
}
.app {
@include setProperty(position,absolute);
@include setProperty(width,percentage(1));
@include setProperty(height,percentage(1));
}</code></pre><p>初始化样式的代码也很好理解,首先是通配选择器*,将所有的外间距和内间距初始化为0,并且设置body和html的宽高,截断溢出内容,并设置背景。加了一个.bm-clearfix用于清除浮动的样式,因为后面会涉及到这个类名的使用,接着是重置ul,li的列表富豪,以及设置类名为app元素的样式。</p><p>基本样式初始化完成,接下来,我们就来实现一下页面当中会用到的工具函数。</p><h3>实现一些会用到的工具函数</h3><p>在utils目录下新建一个util.ts,首先在指令当中会用到的就是一个isServer,用来判断是否是服务端环境,也比较好理解,直接判断window对象是否存在即可。代码如下:</p><pre><code class="ts">export const isServer = typeof window === "undefined";</code></pre><p>接下来,简单封装一个on方法,用来给元素添加事件,on方法接受4个参数,第一个参数为添加事件的元素,类型就是ElementType,第二个参数为事件类型,是一个字符串,比如‘click’,第三个参数是事件回调函数,类型为EventListenerOrEventListenerObject,这个类型是DOM内置定义好的事件回调函数类型,第四个参数也就是一个配置,是一个布尔值,代表事件是冒泡还是捕获阶段。</p><p>这个代码,其实我们就是利用addEventListener方法来简单的封装一下,所以最终代码如下:</p><pre><code class="ts">export function on(
element: GlobalModule.ElementType,
type: string,
handler: EventListenerOrEventListenerObject,
useCapture: boolean = false
) {
if (element && type && handler) {
element.addEventListener(type, handler, useCapture);
}
}</code></pre><p>相应的,我们也有off方法,其实就是将addEventListener缓存removeEventListener方法即可,但在本项目当中似乎并没有用到,所以不必封装。</p><p>接下来是第三个工具方法,叫做isDom,顾名思义,就是判断一个元素是否是一个DOM元素。思考一下,我们如何判断一个元素是否是DOM元素呢?</p><p>或者我们可以这么想,DOM元素都有哪些特点?</p><p>首先第一点,当HTMLElement对象存在时,那么DOM对象节点一定是该对象的一个子实例,因此我们有:</p><pre><code class="ts">if(typeof HTMLElement === 'object'){
return el instanceof HTMLElement;
}</code></pre><p>其次,如果HTMLElement不是一个对象,那我们可以判断el instanceof HTMLCollection。</p><p>最后一种判断方法,那就是判断el是否是一个对象,并且存在nodeType和nodeName属性,其中nodeType = 1代表是一个DOM元素节点,具体可以查看<a href="https://link.segmentfault.com/?enc=PntR9k9gBqpmgAXEZY1MQw%3D%3D.EsFDOSEfgi6Ru81Cmzua08yqaS%2BvLjyCfJ08aNn2GTbsPoYAE1czpoZVAG9OEatkZVa2MenrrHAJQkh0ZWmNgg%3D%3D" rel="nofollow">文档</a>知晓这个属性的值分别代表什么。</p><p>综上所述,isDom方法就呼之欲出了,如下:</p><pre><code class="ts">export function isDom(el: any): boolean {
return typeof HTMLElement === 'object' ?
el instanceof HTMLElement :
el && typeof el === 'object' && el.nodeType === 1 && typeof el.nodeName === 'string'
|| el instanceof HTMLCollection;
}</code></pre><p>接下来的这个工具方法不需细讲,就是一个创建uuid的工具函数,代码如下:</p><pre><code class="ts">export const createUUID = (): string => (Math.random() * 10000000).toString(16).substr(0, 4) + '-' + (new Date()).getTime() + '-' + Math.random().toString().substr(2, 5);</code></pre><p>接下来的一个工具方法可是重中之重,也就是倒计时工具函数,让我们来思考一下,我们主要要返回一个状态出去,也就是倒计时的值,即一个数值,倒计时会有一个起始值,也会有一个结束值,并且还有一个步长,以及执行时间。</p><p>如何实现一个倒计时?这里很显然就要用到定时器啦,不过我这里采用的是另一种方式,也就是延迟函数+递归来实现。一共有5个参数,所以我们的函数结构如下:</p><pre><code class="ts">export const CountDown = (start:number,end:number,step:number = 1,duration:number = 2000,callback:(args: { status:string,value:number,clear:() => void } ) => any) => {
//核心逻辑
}</code></pre><p>这个函数的参数比较长,一共有5个参数,主要在第5个参数上,它是一个函数,参数是3个<code>{ status:'running',value:1,clear:() => {}}</code>,其中status代表当前是什么状态,value就是倒计时的数值,clear是一个函数,用来清空定时器,并阻止递归。</p><p>接下来第一步,定义3个变量,分别代表定时器,当前倒计时数值以及步长,如下:</p><pre><code class="ts">let timer: ReturnType<typeof setTimeout>,
current = start + 1,
step = (end - start) * step < 0 ? -step : step;</code></pre><p>紧接着定义一个需要执行递归的函数,并调用它,然后返回一个clear方法,如下:</p><pre><code class="ts">const handler = () => {
//核心代码
}
handler();
return {
clear:() => clearTimeout(timer);
}</code></pre><p>在递归函数handler中,我们通过current与步长相加得到了倒计时值,随后我们回调状态以及值出去,最后判断当满足了递归条件,就阻止递归并清除定时器,然后将结束状态以及倒计时值回调出去,否则就是延迟递归执行handler函数。如下:</p><pre><code class="ts">current += _step;
callback({
status:"running",
value: current,
clear:() => {
//这里需要注意,必须要修改current为最终状态值,才能清除定时器并停止递归
if(end - start > 0){
current = end - 1;
}else{
current = end + 1;
}
clearTimeout(timer);
}
});
//这里就是递归终止条件
const isOver = end - start > 0 ? current >= end - 1 : current <= end + 1;
if(isOver){
clearTimeout(timer);
callback({
status:"running",
value: current,
clear:() => {
//这里需要注意,必须要修改current为最终状态值,才能清除定时器并停止递归
if(end - start > 0){
current = end - 1;
}else{
current = end + 1;
}
clearTimeout(timer);
}
});
}else{
timer = setTimeout(handler,Math.abs(duration));
}</code></pre><p>合并以上代码就成了我们最终的倒计时函数,如下:</p><pre><code class="ts">export const CountDown = (start: number,
end: number,
step: number = 1,
duration: number = 2000,
callback: (args: { status: string, value: number, clear: () => void }) => any): { clear: () => void } => {
let timer: ReturnType<typeof setTimeout>,
current = start + 1,
_step = (end - start) * step < 0 ? -step : step;
const handler = () => {
current += _step;
callback({
status: "running",
value: current,
clear: () => {
// 需要修改值
if (end - start > 0) {
current = end - 1;
} else {
current = end + 1;
}
clearTimeout(timer);
}
});
const isOver = end - start > 0 ? current >= end - 1 : current <= end + 1;
if (isOver) {
clearTimeout(timer);
callback({
status: "end",
value: current,
clear: () => {
// 需要修改值
if (end - start > 0) {
current = end - 1;
} else {
current = end + 1;
}
clearTimeout(timer);
}
})
} else {
timer = setTimeout(handler, Math.abs(duration));
}
}
handler();
return {
clear: () => clearTimeout(timer)
}
}</code></pre><h3>实现下雪花效果</h3><p>在utils下新建snow.ts,然后我们思考一下,如何实现下雪花的效果?</p><p>我们可以知道下雪花分成两部分下雪花和雪花,在这里,我们需要用到canvas相关语法,我们把下雪花叫做SnowMove,雪花叫做Snow,如此一来,我们就可以定义好两个类,代码如下:</p><pre><code class="ts">class Snow {
//雪花类核心代码
}
class SnowMove {
//下雪花类核心代码
}</code></pre><h3>实现Snow类</h3><p>现在,我们先来实现雪花类,首先我们要知道要实现雪花,就需要添加一个canvas标签,在这里我们选择的是动态添加canvas标签,所以雪花类构造函数中应当有2个参数,第一个就是canvas元素添加的容器元素,另一个就是雪花配置对象。因此,我们继续添加如下代码:</p><pre><code class="ts">class Snow {
constructor(element:GlobalModule.ElementType,option?:GlobalModule.SnowOptionType){
//初始化代码
}
}</code></pre><p>注意2个参数的类型,还有第2个参数是可选的,这样我们就可以定义一个默认配置对象,如果没有传option,就采用默认配置对象,接下来我们要在构造函数里面做什么?那当然是要初始化一些属性,定义一些公共属性来存储容器元素和配置对象。</p><pre><code class="ts">class Snow {
public el: GlobalModule.ElementType;
public snowOption: GlobalModule.SnowOptionType;
public defaultSnowOption: Required<GlobalModule.SnowOptionType> = {
snowNumber: 200,
snowShape: 5,
speed: 1
};
public snowCan: HTMLCanvasElement | null;
public snowCtx: CanvasRenderingContext2D | null;
public snowArr: SnowMove [];
constructor(element:GlobalModule.ElementType,option?:GlobalModule.SnowOptionType){
this.el = element;
this.snowOption = option || this.defaultSnowOption;
this.snowCan = null;
this.snowCtx = null;
this.snowArr = [];
}
}</code></pre><p>以上代码虽然稍微有点长,但事实上很好理解,我们就是在类的this对象上绑定了一些属性,比如容器元素,还有初始化canvas元素和元素上下文对象,可能不好理解的是这里有一个snowArr属性,它代表存储的每一个雪花移动的类的数组。</p><p>初始化属性完成,接下来创建一个init方法,用来初始化雪花的一些方法,在init方法中,我们调用了3个方法。</p><ul><li>createCanvas: 顾名思义,就是创建canvas元素的方法。</li><li>createSnowShape: 这是一个创建雪花形状的方法。</li><li>drawSnow: 画雪花的方法。</li></ul><p>代码如下:</p><pre><code class="ts">class Snow {
//省略了部分代码
init(){
this.createCanvas();
this.createSnowShape();
this.drawSnow();
}
}</code></pre><p>让我们先来看第一个方法,createCanvas方法的实现,我们知道动态创建一个元素,其实也就是使用document.createElement方法,创建canvas元素之后,我们需要额外设置一点样式让canvas填充满整个容器元素,为了方便获取canvas元素,我们给它添加一个id,随后我们需要设置canvas元素的宽度和高度,最后我们将canvas元素添加到容器元素中去。</p><p>但是我们需要知道,在这里屏幕可能会发生变动,发生了变动之后,我们的canvas元素应该也会变动,所以我们还需要监听resize事件,用来修改元素的宽高。</p><p>让我们来看一下实现的代码吧:</p><pre><code class="ts">import { isDom,on } from './util'
class Snow {
//省略了代码
createCanvas(){
//创建一个canvas元素
this.snowCan = document.createElement('canvas');
// 设置上下文
this.snowCtx = this.snowCan.getContext('2d');
// 设置id属性
this.snowCan.id = "snowCanvas";
// canvas元素设置样式
this.snowCan.style.cssText += "position:absolute;left:0;top:0;z-index:1;";
//设置canvas元素宽度和高度
this.snowCan.width = isDom(this.el) ? (this.el as HTMLElement).offsetWidth : document.documentElement.clientWidth;
this.snowCan.height = isDom(this.el) ? (this.el as HTMLElement).offsetHeight : document.documentElement.clientHeight;
// 监听resize事件
on(window,'resize',() => {
(this.snowCan as HTMLElement).width = document.documentElement.clientWidth;
(this.snowCan as HTMLElement).height = document.documentElement.clientHeight;
});
//最后一步,将canvas元素添加到页面中去
if(isDom(this.el)){
(this.el as HTMLElement).appendChild(this.snowCan);
}else{
document.body.appendChild(this.snowCan);
}
}
//省略了代码
}</code></pre><p>createCanvas到此为止了,接下来我们来看下一个方法,也就是createSnowShape方法。这个方法其实也很简单,主要是根据参数创建一个雪花移动的数组并存储起来。如下:</p><pre><code class="ts">class Snow {
//省略了代码
createSnowShape(){
const maxNumber = this.snowOption.snowNumber || this.defaultSnowption.snowNumber,
shape = this.snowOption.snowShape || this.defaultSnowption.snowShape,
{ width,height } = this.snowCan as HTMLCanvasElement,
snowArr: SnowMove [] = this.snowArr = [];
for(let i = 0;i < maxNumber;i++){
snowArr.push(
new SnowMove(width,height,shape,{ ...this.defaultSnowOption,...this.snowOption })
)
}
}
//省略了代码
}</code></pre><p>显然这个方法就是把每一个雪花移动当作一个实例存储到数组中,这个雪花移动的类我们后面会说到,这里先不说。让我们来看下一个方法drawSnow。</p><p>其实通过这个方法我们也可以看到真正画雪花是在SnowMove类当中,这个类当中我们实现了render也就是渲染雪花的方法,以及update更新雪花的方法。所以在这个方法但这个方法当中,我们主要做的事情就是🏪存储的雪花数组snowMove,然后调用每一个snowMove实例的render方法和update方法,然后再使用requestAnimationFrame重复调用drawSnow方法。</p><p>当然在遍历之前,我们要先调用clearRect方法清除画布。</p><pre><code class="ts">class Snow {
//省略了代码
drawSnow(){
//清除画布
this.snowCtx?.clearRect(0,0,this.snowCan?.width as number,this.snowCan?.height as number);
//遍历snowArr
const snowNumber = this.snowOption.snowNumber || this.defaultSnowption.snowNumber;
for(let i = 0;i < snowNumber;i++){
this.snowArr[i].render(this.snowCtx as CanvasRenderingContext2D);
this.snowArr[i].update(this.snowCan as HTMLCanvasElement);
}
// 重复调用
requestAnimationFrame(() => this.drawSnow());
}
//省略了代码
}</code></pre><p>除此之外,Snow类还额外封装了一个remove方法,用来移除Snow创建的canvas元素,虽然在本示例当中没有用到,但是也可以说一下。</p><pre><code class="ts">class Snow {
//省略了代码
remove(){
if(isDom(this.el)){
(this.el as HTMLElement).removeChild(this.snowCan);
}else{
document.body.removeChild(this.snowCan);
}
}
//省略了代码
}</code></pre><p>接下来我们来看SnowMove类的实现。</p><h3>实现SnowMove类</h3><p>通过前面的代码,我想我们对这个类的实现已经有了一定的了解了,比如render和update方法,顾名思义,一个就是渲染方法,另一个就是更新方法。接下来我们要思考一下,雪花移动改变的是什么?</p><p>雪花移动主要就是改变坐标,也就是x和y坐标的值,它会有一个步长,然后根据步长结合数学函数计算出垂直下落的x和y坐标的一个速度,我们称之为verX和verY,在下落的时候,可能也会飞出边界,所以我们就需要在飞出边界的时候,我们就应该做一个重置操作,所以也就额外增加了一个reset方法。</p><p>根据以上分析,我们得出SnowMove类,我们应该初始化的属性有x,y,shape,fallspeed,verX,verY,step,stepNum等属性,分别代表x坐标以及y坐标,雪花形状,下落速度,垂直方向上的x坐标和y坐标,步长,以及步数。</p><p>当然为了方便获取在Snow类里面定义的配置属性,我们将Snow定义的配置属性对象当作参数也要传给SnowMove类。</p><p>代码如下:</p><pre><code class="ts">class SnowMove {
public x:number;
public y:number;
public shape:number;
public fallspeed:number;
public verx:number;
public verY:number;
public step:number;
public stepNum: number;
public context: Required<GlobalModule.SnowOptionType>;
// 注意构造函数的参数
constructor(w:number,h:number,s:number,context:Required<GlobalModule.SnowOptionType>){
// 初始化x和y坐标,取随机数,由于我们的x和y坐标是在canvas元素内部变动,因此我们取canvas元素的宽度和高度去乘以随机数得到初始化的随机x和y坐标
this.x = Math.floor(w * Math.random());
// 这也是为什么要将canvas的宽度和高度当作SnowMove的参数原因
this.y = Math.floor(h * Math.random());
// 初始化形状
this.shape = Math.random() * s;
// 初始化下落速度
this.fallspeed = Math.random() + context.speed;
// 初始化x和y方向下落的速度
this.verY = context.speed;
this.verX = 0;
// 初始化context
this.context = context;
}
}</code></pre><p>如此一来我们的初始化工作就完成了,但事实上我们第二个方法reset方法本质上也是重新初始化一次,因此我们可以将初始化的逻辑抽取出来,创建一个init方法,然后直接调用这个方法来初始化。修改代码如下:</p><pre><code class="ts">class SnowMove {
//省略了代码
constructor(w:number,h:number,s:number,context:Required<GlobalModule.SnowOptionType>){
this.context = context;
this.init(w,h,s,context.speed);
}
init(w:number,h:number,s:number,speed: number){
this.x = Math.floor(w * Math.random());
this.y = Math.floor(h * Math.random());
this.shape = Math.random() * s;
this.fallspeed = Math.random() + speed;
this.verY = speed;
this.verX = 0;
}
}</code></pre><p>如此一来,我们的reset方法也就完成了,如下:</p><pre><code class="ts">class SnowMove {
//省略了代码
reset(can: HTMLCanvasElement){
this.init(can.width,can.height,this.context.speed);
}
//省略了代码
}</code></pre><p>接下来,我们来完成update方法,update方法传入canvas作为参数,因为我们要使用到canvas元素的宽度和高度,接下来思考一下,我们要在update方法里面做什么?</p><p>我们是不是要更新下落坐标?也可以称之为更新下落速度,这样我们也就相当于更改verX和verY的值,那么如何更改?</p><p>verX的计算公式为:</p><p>this.verX = this.verX <em> 一个随机移动的数(这里是0.95)+ Math.cos(this.step += (一个数,这里取的是0.4)) </em> this.stepNum;</p><p>verY的计算公式为:</p><p>this.verY = Math.max(this.fallspeed,this.verY);</p><p>然后我们再将两者自增,这样雪花就达到了从最上方落到最下方的效果,当然这个计算公式不是唯一的,根据实际效果而定。</p><p>更新了坐标完成之后,我们需要做一个边界处理,边界的判断条件是什么?</p><p>很简单不能小于(可以等于可以不等于,这里取等于)0,其次不能大于canvas元素的宽度和高度。</p><p>综上所述,update方法就呼之欲出啦,代码如下:</p><pre><code class="ts">class SnowMove {
//省略了代码
update(can: HTMLCanvasElement){
this.verX *= 0.95;
if(this.verY <= this.fallspeed){
this.verY = this.fallspeed;
}
this.verX += Math.cos(this.step += .4) * this.stepNum;
this.verY += this.verY;
this.verX += this.verX;
// 边界判断
if(this.verX <= 0 || this.verX > can.width || this.verY <= 0 || this.verY > can.height){
this.reset(can);
}
}
//省略了代码
}</code></pre><p>update方法完成后,render方法才是最核心的构建雪花的方法,构建雪花我们采用渐变颜色填充,并且这里的雪花是圆形的,所以我们需要用到arc方法来画圆,画圆要用到半径,所以我我们将最开始配置对象的参数shape作为半径。</p><p>canvas画一个图形的步骤有,</p><ul><li>ctx.save 保存状态</li><li>ctx.fillStyle 填充颜色</li><li>ctx.beginPath 开始路径</li><li>ctx.arc 画圆</li><li>ctx.fill 填充路径</li><li>ctx.restore 弹出状态</li></ul><p>想要知道canvas的这些具体代表什么,可以查看<a href="https://link.segmentfault.com/?enc=2WfglUnxs15y4%2ByvXXry%2Fw%3D%3D.rl1WnfuQYQdQlehInYrNP7hnKB9Z%2BOSVQIVbWrLTsEoqCn8oJSrc9GBsPjtmJOTmPTZvpw2S0ZOK%2F1e7PaJk3wRO7acMp9Fp1osrACV6w4IuPCkDOBZj4A1dWIeutm2e" rel="nofollow">文档</a>。</p><p>这里我们使用createRadialGradient和addColorStop方法来创建一个渐变颜色。</p><p>根据以上分析,render方法,我们基本上就完成了。如下:</p><pre><code class="ts">class SnowMove {
//省略了代码
render(ctx:CanvasRenderingContext2D){
const snowStyle = ctx.createRadialGradient(this.x,this.y,0,this.x,this.y,this.shape);
snowStyle.addColorStop(0.8, 'rgba(255,255,255,1)');
snowStyle.addColorStop(0.1, 'rgba(255,255,255,.4)');
ctx.save();
ctx.fillStyle = snowStyle;
ctx.beginPath();
ctx.arc(this.x,this.y,this.shape,0,Math.PI * 2);
ctx.fill();
ctx.beginPath();
}
//省略了代码
}</code></pre><p>将以上的分析代码合并,我们的一个Snow下雪花效果就写好了,接下来我们来看是如何使用的。</p><pre><code class="ts">const s = new Snow(document.querySelect('.test'));
s.init();</code></pre><h3>一些公共组件的实现</h3><p>我们来尝试分析一下页面,我们可以将哪些组件做成公共组件,首先是首页,我们可以将按钮组件,还有就是ready go也分别做成公共组件,其次我们还需要一个Modal组件,公共组件基本就这些了。</p><h3>Button组件的实现</h3><p>button组件的实现很简单,就是一个button,然后写点样式(样式是可以自己随便写的),然后通过defineEmits方法将点击事件传给父组件即可。代码如下:</p><pre><code class="vue"><script lang="ts" setup>
const emit = defineEmits(['click']);
</script>
<template>
<button type="button" class="bm-play-btn" @click="emit('click')">开始游戏</button>
</template>
<style lang="scss" scoped>
$color: #753200;
.#{$prefix}play-btn {
@include setProperty(position, absolute);
@include setProperty(width, 2rem);
@include setProperty(height, .6rem);
@include setProperty(left, percentage(0.5));
@include setProperty(top, percentage(0.5));
@include setProperty(background, linear-gradient(135deg,#fefefe 10%,#fff 90%));
@include setProperty(transform, translate(-50%, -50%));
@include setProperty(font, bold .34rem/.6rem '微软雅黑');
@include setProperty(text-align, center);
@include setProperty(color, $color);
@include setProperty(border-radius,.4rem);
@include setProperty(letter-spacing,2px);
@include setProperty(cursor,pointer);
@include setProperty(outline,none);
@include setProperty(border,none);
&:hover {
@include setProperty(background, linear-gradient(135deg,#e8e8e8 10%,#fff 90%));
}
}
</style></code></pre><p>在这里,我通过写scss的mixin来写样式,满屏的setProperty可能会让人有些迷惑,你只需要知道它就是mixin即可,也许这不是一个好的方式,这纯属个人的爱好,不一定非要跟着我这样写。</p><blockquote>PS: 这里为了兼容移动端,我们也用到了rem布局,这个我们放到最后来讲。</blockquote><h3>go和ready组件的实现</h3><p>要实现这两个组件,我们首先需要先简单包装一下标题组件,创建一个Head.vue,代码如下:</p><pre><code class="vue"><script lang="ts" setup>
import { PropType, toRefs } from 'vue';
const props = defineProps({
level: {
type: [Number, String] as PropType<GlobalModule.LevelType>,
default: '1',
validator: (v: GlobalModule.LevelType) => {
return [1, 2, 3, 4, 5, 6].includes(Number(v));
}
},
content: String as PropType<string>
})
const { level, content } = toRefs(props);
const ComponentName = `h${level.value}`;
</script>
<template>
<Component :is="ComponentName">
<slot>{{ content }}</slot>
</Component>
</template></code></pre><p>这个组件的代码也是很好理解的,利用vue的动态组件component,来实现从h1 ~ h6根据props来决定是使用哪个标签元素渲染。</p><p>这里使用了对象解构,为了不让props在对象解构当中失去响应式特性,我们使用toRefs方法来包裹了props。</p><p>props有两个参数,第一个为level,代表标题标签使用哪种,有6个数值,即1 ~ 6,其次content可以作为标签的内容,当然如果写了插槽内容,默认还是以插槽内容为主。</p><p>接下来Go和Ready组件就是基于Head组件来实现的,两者有些共同之处,主要不同的地方在于动画效果的不同,一个是渐隐效果,一个是渐隐 + 缩放效果。</p><p>到了这里,我想很多人已经分析出来了,就是使用animation动画来实现。</p><p>首先,我们将这两个组件的公共样式提取出来,放到style目录下,新增一个Head.scss,然后写上样式代码。</p><blockquote>我认为样式还是比较简单好理解的,应该不需要细讲,直接附上源码即可。</blockquote><pre><code class="scss">@mixin head {
color:$white;
width: percentage(1);
text: {
align: center;
}
line: {
height: 400px;
}
position: absolute;
display: block;
}</code></pre><p>这里值得一提的就是scss的属性语法,我们还可以将属性拆分,比如本示例中的text-align被拆分成了text和align,同理line-height也是,这样我们也可以举一反三,比如border,background等也都可以这么写,当然这种写法与scss的版本也有关系,需要注意你使用的scss版本是否支持。</p><p>然后我们来看Go和Ready组件的源码,两者应该是类似的,基本上写了一个,另一个就好写了,无非是动画的效果不同罢了。</p><ul><li>Go.vue</li></ul><pre><code class="vue"><script setup lang="ts">
import { PropType } from 'vue';
import Head from './Head.vue';
const props = defineProps({
modelValue: Boolean as PropType<boolean>
});
const emit = defineEmits(['update:modelValue']);
emit('update:modelValue');
</script>
<template>
<Head class="bm-go" :class="{ 'active':props.modelValue }">Go</Head>
</template>
<style scoped lang="scss">
@import "../style/head.scss";
.#{$prefix}go {
@include head();
opacity: 0;
transform: scale(0);
&.active {
animation: goSlide 1.5s .5s;
}
@keyframes goSlide {
from {
opacity: 0;
transform: scale(0);
}
to {
transform: scale(1.7);
opacity: 1;
}
}
}
</style></code></pre><ul><li>Ready.vue</li></ul><pre><code class="vue"><script setup lang="ts">
import { PropType } from 'vue';
import Head from './Head.vue';
const props = defineProps({
modelValue: Boolean as PropType<boolean>
});
const emit = defineEmits(['update:modelValue']);
emit('update:modelValue');
</script>
<template>
<Head class="bm-ready" :class="{ 'active':props.modelValue }">Ready</Head>
</template>
<style scoped lang="scss">
@import "../style/head.scss";
.#{$prefix}ready {
@include head();
transform: translateY(-150%);
&.active {
animation: readySlide 1.5s;
}
// 不同的是动画效果
@keyframes readySlide {
from {
opacity: 1;
transform: translateY(-150%);
}
to {
transform: translateY(0);
opacity: 0;
}
}
}
</style></code></pre><p>最后一个公共组件就是Modal.vue呢,也就是一个弹框组件的实现,让我们一起来看一下吧。</p><h3>clickoutside指令的实现</h3><p>在开始这个组件之前,我们还需要额外使用到一个指令,即clickOutside指令,顾名思义,就是点击元素区域之外所执行的逻辑。试想一下,我们通常在实现弹框组件的时候,点击弹框内容里面是不用关闭弹框的,但是点击遮罩层就需要关闭弹框了,所以这个指令在此也就有了用武之地。</p><p>像一些下拉框组件Select,Popover组件(悬浮框)组件等,都可能会用到这个指令。</p><p>那么如何实现这个指令呢?</p><p>我们思考一下,要实现点击区域之外,也就是说我们需要一个事件的全局代理,即我们点击整个屏幕,然后通过点击屏幕的事件对象中的点击触发节点来判定是否在弹框内容组件节点中。</p><p>有两种方式实现这种效果,一种是通过节点的方式,另一种则是通过判断坐标的方式,这在我的实现颜色选择器的<a href="https://link.segmentfault.com/?enc=j%2Fy%2FNxqYguPti%2B47L80SJA%3D%3D.L%2FaT6OxJ93vzSPIiKmwghYoXkN0evsiQvcjBcLJvQcuTlX3iNI5Jcqcgv9aKpk1I" rel="nofollow">文章</a>和<a href="https://ke.segmentfault.com/course/1650000040761646">课程</a>当中有详细讲解。</p><p>当然以上是题外话,让我们继续,我们在这里很明显需要有一个数据结构,将绑定该指令的所有节点都存储起来,然后通过监听document或者是window对象的mousedown事件,比较节点是否在存储的数据结构中能够找到,如果能够找到,就不执行后续逻辑,否则就执行指令绑定的对应方法。</p><p>整体思路就是这么一回事,接下来,我们来看具体的实现,在directives目录下新建一个clickoutside.ts文件。</p><pre><code class="ts">import { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
import { isServer,on } from '../utils/util';
const nodeList:GlobalModule.FlushList = new Map();
let startClick:MouseEvent | Event;
if(!isServer){
on(document,'mousedown',(e:MouseEvent | Event) => startClick = e);
on(document,'mouseup',(e:MouseEvent | Event) => {
for(const { DocumentHandler } of nodeList.values()){
DocumentHandler(e,startClick);
}
});
}
const createDocumentHandler = (el:HTMLElement,binding:DirectiveBinding):GlobalModule.DocumentHandler => {
// the excluding elements
let excludes:HTMLElement[] = [];
if(binding.arg){
if(Array.isArray(binding.arg)){
excludes = binding.arg;
}else{
excludes.push(binding.arg as unknown as HTMLElement);
}
}
return (mouseup,mousedown) => {
// Maybe we can not considered the tooltip component,which is the popperRef type
const popperRef = (binding.instance as ComponentPublicInstance<{ popperRef:NonNullable<HTMLElement> }>).popperRef;
const mouseUpTarget = mouseup.target as Node;
const mouseDownTarget = mousedown.target as Node;
const isBinding = !binding || !binding.instance;
const isExistTargets = !mouseUpTarget || !mouseDownTarget;
const isContainerEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
const isSelf = el === mouseUpTarget;
const isContainByPopper = popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
const isTargetExcluded = excludes.length && (excludes.some(item => item.contains && item?.contains(mouseUpTarget)) || excludes.indexOf(mouseDownTarget as HTMLElement) > -1);
if(isBinding || isExistTargets || isContainerEl || isSelf || isTargetExcluded || isContainByPopper)return;
// the directive should binding a method or function
binding.value();
}
}
const setNodeList = (el:HTMLElement,binding:DirectiveBinding) => {
nodeList.set(el,{
DocumentHandler:createDocumentHandler(el,binding),
bindingFn:binding.value
})
}
const clickOutside:ObjectDirective = {
beforeMount(el,binding){
setNodeList(el,binding);
},
updated(el,binding){
setNodeList(el,binding);
},
unmounted(el){
nodeList.delete(el);
}
}
export default clickOutside;</code></pre><p>通过以上源码,我们需要知道哪些点,首先我们是通过map数据结构来存储整个节点,每个节点对应一个对象,对象里面对应一个文档节点的回调方法,和指令值所执行的方法。</p><p>我们知道,在vue的指令当中也有对应的生命周期钩子函数,在这里我们用到了beforeMount,updated,以及unmounted钩子函数,在元素挂载和数据更新的钩子函数中,我们存储调用的逻辑对象,在组件卸载的钩子函数中,我们删除以元素作为存储的对应节点的逻辑对象。</p><p>在mousedown事件中,我们用了一个变量来存储事件对象,然后在mouseup事件中,我们就调用对应的文档节点存储的回调方法。</p><p>这里的判断元素节点是否是在弹框内容之外的核心逻辑,其实就在createDocumentHandler这个函数中。</p><p>在这个函数当中,我们首先用一个数组来存储指令的arg参数,这个参数如果传了,并且是一个dom元素,我们就保存起来。</p><p>然后我们返回一个函数,函数有2个参数,分别是鼠标按下的事件对象和鼠标释放的事件对象,在这个函数里面,我们主要对每一种情况都做了分析。</p><p>归根结底就是判断元素是否存在,并且元素不应该是popover组件,并且在我们存储的数组当中存在该元素,都直接return,代表我们点击的是元素区域内。</p><p>如果不满足这些条件,我们才调用指令的值,它是一个方法。</p><p>这个指令理解了,接下来我们的弹框组件就好理解多了。</p><h3>弹框组件的实现</h3><p>弹框组件整体逻辑并不算复杂,主要需要考虑样式的编写,以及配置属性,可以尝试思考一下,一个弹框组件应该会有哪些基本属性,如下。</p><ul><li>title: 弹框的标题</li><li>content: 弹框的内容</li></ul><p>其余的属性都是额外延伸出来的,例如hasFooter属性,顾名思义,就是是否显示弹框底部内容,其他额外的属性如下所示:</p><ul><li>showCancel: 是否显示取消按钮</li><li>isRenderContentHTML: 弹框内容是否渲染html元素</li><li>maskCloseable: 是否允许点击遮罩层关闭弹框</li><li>canceText: 取消按钮文本</li><li>okText: 确认按钮文本</li><li>align: 弹框底部的布局方式</li><li>container: 渲染弹框的容器元素</li></ul><p>当然一个复杂的弹框还会有更多属性,用来应对各种各样的场景,但是这些属性在这个示例当中已经足够了。</p><p>除此之外,为了实现自定义组件的v-model指令,我们在这里也定义了一个modelValue属性,属性方面分析完成,接下来就是分析事件的注册,主要有三个事件,第一就是update:modelvalue,还有两个就是点击确认和取消事件。</p><p>在这里,我们也知道了clickoutside指令的使用方式,首先就是导入指令,然后用一个变量(为了添加独特的标志,代表是Vue框架的指令),我们定义成VClickOutside,然后在模板代码中,我们就可以直接v-click-outside这样来使用了。</p><p>其实分析到这里,一个弹框组件基本也就完成了,接下来就是添加样式,去美化弹框组件了,当然这里还使用了一个teleport组件,这个组件是Vue3独特添加的一个组件,用来将组件插入到某个容器元素的,现在我们就来看完整的代码吧:</p><pre><code class="vue"><script setup lang="ts">
import { PropType, toRefs } from 'vue';
import clickOutside from "../directives/clickOutside";
const props = defineProps({
modelValue: Boolean as PropType<boolean>,
title: String as PropType<string>,
content: String as PropType<string>,
hasFooter: {
type: Boolean as PropType<boolean>,
default: true
},
showCancel: {
type: Boolean as PropType<boolean>,
default: true
},
isRenderContentHTML: {
type: Boolean as PropType<boolean>,
default: false
},
maskCloseable: {
type: Boolean as PropType<boolean>,
default: true
},
cancelText: {
type: String as PropType<string>,
default: "取消"
},
okText: {
type: String as PropType<string>,
default: "确认"
},
align: {
type: String as PropType<string>,
default: 'right',
validator: (v: string) => {
return ['left', 'center', 'right'].includes(v);
}
},
container: {
type: String as PropType<string>,
default: 'body'
}
});
const emit = defineEmits(['update:modelValue', 'on-ok', 'on-cancel']);
emit('update:modelValue');
const { modelValue, title, content, hasFooter, cancelText, okText, align, container, maskCloseable, isRenderContentHTML } = toRefs(props);
const onClickOutsideHandler = () => {
if (maskCloseable.value) {
emit('update:modelValue', false);
}
}
const VClickOutside = clickOutside;
const onCancelHandler = () => {
emit('update:modelValue', false);
emit('on-cancel');
}
const onOkHandler = () => {
emit('on-ok');
}
</script>
<template>
<teleport :to="container">
<Transition name="modal">
<div v-if="modelValue" class="bm-modal-mask">
<div class="bm-modal-wrapper">
<div class="bm-modal-container" v-click-outside="onClickOutsideHandler">
<div class="bm-modal-header" v-if="title">
<slot name="header">{{ title }}</slot>
</div>
<div class="bm-modal-body" v-if="content">
<slot name="body">
<p v-if="isRenderContentHTML" v-html="content"></p>
<template v-else>{{ content }}</template>
</slot>
</div>
<div class="bm-modal-footer" v-if="hasFooter" :class="{ ['text-' + align]: true }">
<slot name="footer">
<button class="bm-modal-footer-btn" @click="onCancelHandler" v-if="showCancel">{{
cancelText
}}</button>
<button class="bm-modal-footer-btn primary" @click="onOkHandler">{{ okText
}}</button>
</slot>
</div>
</div>
</div>
</div>
</Transition>
</teleport>
</template>
<style lang="scss" scoped>
$btnBorderColor: #c4c4c4;
$primaryBgColor: linear-gradient(135deg, #77b9f3 10%, #106ad8 90%);
$primaryHoverBgColor: linear-gradient(135deg, #4d95ec 10%, #0754cf 90%);
$btnHoverColor: #3a6be7;
$btnHoverBorderColor: #2c92eb;
.#{$prefix}modal-mask {
@include setProperty(background-color, fade-out($black, .5));
@include setProperty(position, fixed);
@include setProperty(z-index, 2000);
@include setProperty(top, 0);
@include setProperty(left, 0);
@include setProperty(bottom, 0);
@include setProperty(right, 0);
@include setProperty(transition, all .2s ease-in-out);
@include setProperty(font-size,.2rem);
.#{$prefix}modal-wrapper {
@extend .flex-center;
@include setProperty(height, percentage(1));
.#{$prefix}modal-container {
@include setProperty(min-width, 300px);
@include setProperty(margin, 0 auto);
@include setProperty(background-color, $white);
@include setProperty(border-radius, 4px);
@include setProperty(transition, all .2s ease-in-out);
@include setProperty(box-shadow, 0 1px 6px fade-out($black, .67));
.#{$prefix}modal-header {
@include setProperty(padding, 20px 30px);
@include setProperty(border-bottom, 1px solid fade-out($white, .65));
@include setProperty(color, fade-out($black, .15));
}
.#{$prefix}modal-body {
@include setProperty(padding, 20px 30px);
}
.#{$prefix}modal-footer {
@include setProperty(padding, 20px 30px);
&.text-left {
@include setProperty(text-align, left);
}
&.text-center {
@include setProperty(text-align, center);
}
&.text-right {
@include setProperty(text-align, right);
}
&-btn {
@include setProperty(outline, none);
@include setProperty(display, inline-block);
@include setProperty(background, transparent);
@include setProperty(border, 1px solid $btnBorderColor);
@include setProperty(border-radius, 8px);
@include setProperty(padding, 8px 12px);
@include setProperty(color, fade-out($black, .15));
@include setProperty(letter-spacing, 2px);
@include setProperty(font-size, 14px);
@include setProperty(font-weight, 450);
@include setProperty(cursor, pointer);
@include setProperty(transition, background .3s cubic-bezier(.123, .453, .56, .89));
&:first-child {
@include setProperty(margin-right, 15px);
}
&:hover {
@include setProperty(color, $btnHoverColor);
@include setProperty(border-color, $btnHoverBorderColor);
}
&.primary {
@include setProperty(background, $primaryBgColor);
@include setProperty(color, $white);
&:hover {
@include setProperty(background, $primaryHoverBgColor);
}
}
}
}
}
}
}
.baseModalStyle {
@include setProperty(transform, scale(1));
}
.modal-enter-from {
@include setProperty(opacity, 0);
.#{$prefix}modal-container {
@extend .baseModalStyle;
}
}
.modal-leave-to {
@include setProperty(opacity, 0);
.#{$prefix}modal-container {
@extend .baseModalStyle;
}
}
</style></code></pre><p>弹框组件实现完成,我们本示例所用到的公共组件也就完成了,接下来,我们来完善游戏的核心逻辑,在core目录下新建game.ts文件。</p><h3>游戏核心逻辑</h3><p>由于我们每一个素材需要一个唯一的uuid标志,所以createUUID方法需要在这里导入进来,另外我们需要随机打乱顺序,虽然可以自己写方法来实现,但是这里为了方便,我们使用lodash.js,然后我们还要将游戏配置的状态管理store给导入进来。</p><p>其实这个文件我们主要导出一个函数组件,所以我们先写一个基本结构,代码如下:</p><pre><code class="ts">import { createUUID } from './../utils/util';
import { useConfigStore } from './../store/store';
import _ from 'lodash';
import { onMounted, ref } from 'vue';
const useGame = () => {
//游戏核心逻辑
}
export default useGame;</code></pre><p>游戏的核心逻辑其实也不难,主要是打乱素材列表然后导出的逻辑,然后还有一个逻辑,那就是如果用户点击的是2个相同的素材,那么我们需要执行相应的逻辑,比如更改素材列表。</p><p>我们一步步来看,首先是第一步,拿到游戏的配置状态,代码如下:</p><pre><code class="ts">const { gameConfig } = useConfigStore();</code></pre><p>接着,我们用一个数组来存储数组列表,并且用另外一个数组用来存储用户点击的素材列表,素材列表的对象有如下几个属性:</p><ul><li>active 表示当前素材是否被选中,用来确定是否添加一个选中样式</li><li>src 表示素材的路径,也就是图片路径</li><li>title? 表示描述素材的标题</li><li>id 唯一标志,uuid</li><li>isMatch 表示是否匹配</li></ul><p>这里可能有人疑惑为什么不能用active来同时表示选中和是否匹配,其实增加一个字段来表示是否匹配,我们会更方便写逻辑,因为只有在满足2项选中素材的情况下,我们才需要考虑判断是否匹配。</p><p>所以定义好两种数据结构,代码如下:</p><pre><code class="ts">const materialDataList = ref<GlobalModule.MaterialData[]>([]);
const activeDataList = ref<GlobalModule.MaterialData[]>([]);</code></pre><p>下一步,我们还用了两个变量来存储错误和正确的audio对象,用来添加音效,当然其实音效逻辑不应该放在这游戏核心逻辑中。</p><pre><code class="ts">const rightAudio = ref<HTMLAudioElement>();
const wrongAudio = ref<HTMLAudioElement>();</code></pre><p>最后,我们还需要定义一个匹配数用来判断用户是否匹配完成所有的素材,以及一个用来确定游戏状态的值,如下:</p><pre><code class="ts">const totalMatch = ref(0);
const gameStatus = ref(gameConfig.gameStatus);</code></pre><p>接下来的逻辑也就比较简单了,其实就是重复复制素材列表,然后随机打乱顺序,并修改。如下:</p><pre><code class="ts">const onStartGame = () => {
materialDataList.value = _.shuffle(_.flatten(_.times(2, _.constant(gameConfig.materialList.map(item => ({
src: item.src,
title: item.title,
active: false,
isMatch: false
})))))).map(item => ({
id: createUUID(),
...item
}));
}</code></pre><p>这里使用了lodash的shuffle方法来实现打乱顺序,用了flatten,times,constant方法来实现重复复制,这一段逻辑还确实有点点复杂,主要需要了解lodash的4个方法的使用。</p><p>接下来就是游戏的点击逻辑,点击逻辑,我们思考一下,可以先将点击的素材对象添加到数组中去,然后判断点击的素材数组中是否有重复的项。</p><p>这里难点就来了,如何判断是否重复?</p><p>这里我们用到了一个哈希表的算法,详细算法思路可以参考<a href="https://link.segmentfault.com/?enc=aaInYzRyKTHyJpGv1m%2BXuw%3D%3D.eWE5QH11bmte97GyeC4LGPRfJ491sOCojDw%2BNJKUSB75CVAHd2D%2FKTPjVUE4MlgVnOppIBuVf2ywVir5%2FEMl0M0LXjoZKr%2BlhpL9P9v%2Bpj8%3D" rel="nofollow">剑指offer-查找重复的数字</a>,我这里就是依据这里来进行稍微的改造,从而实现了算法。代码如下:</p><pre><code class="ts">const findRepeatItem = function (arr: GlobalModule.MaterialData[]) {
const unique = new Set();
for (const item of arr) {
if (unique.has(item.src)) {
return true;
}
unique.add(item.src);
}
return false;
};</code></pre><p>点击事件的核心逻辑,其实细细分下来,就主要是2点,添加选中样式,然后判断是否重复,分别执行对应的逻辑。说到这里,相信没有人会看不懂如下代码了:</p><pre><code class="ts">const onClickHandler = (block: GlobalModule.MaterialData) => {
block.active = true;
// 这里判断如果用户点击的是同一张素材,则下面的逻辑就不执行
if (activeDataList.value.findIndex(item => item.id === block.id) > -1) {
return;
}
// 添加到选中素材数组中
activeDataList.value.push(block);
// 获取正确和错误音效audio元素,并存储到数据中
if(!rightAudio.value){
rightAudio.value = document.getElementById('rightAudio') as HTMLAudioElement;
}
if(!wrongAudio.value){
wrongAudio.value = document.getElementById('wrongAudio') as HTMLAudioElement;
}
// 判断是否存在重复项
if (findRepeatItem(activeDataList.value)) {
// 存在就更改isMatch值,并从选中素材数组中删除对应的值
materialDataList.value = materialDataList.value.map(item => {
const index = activeDataList.value.findIndex(active => active.id === item.id);
if (index > -1) {
item.isMatch = true;
activeDataList.value.splice(index, 1);
}
return item;
});
// 统计匹配的数量,这里加2主要是方便,后面该值等于materialDataList.value.length === 2就代表全部消除完了,游戏胜利
totalMatch.value += 2;
// 播放音效
rightAudio.value?.play();
wrongAudio.value?.pause();
} else {
// 素材列表长度不等于2,就代表用户只点击了一张,无法进行匹配,所以后续逻辑不执行
if (activeDataList.value.length !== 2) {
return;
}
// 重置选中素材列表以及素材列表的喧哗走过呢状态
activeDataList.value = [];
materialDataList.value = materialDataList.value.map(item => ({
...item,
active: false
}));
// 播放音效
rightAudio.value?.pause();
wrongAudio.value?.play();
}
}</code></pre><p>下一步,我们就在mounted挂载钩子函数中调用游戏开始函数,如下:</p><pre><code class="ts">onMounted(() => {
onStartGame();
})</code></pre><p>最后,我们导出需要用到的东西,如下:</p><pre><code class="ts">return {
materialDataList,
gameConfig,
gameStatus,
totalMatch,
onClickHandler,
onStartGame
}</code></pre><p>合并以上代码,我们的游戏核心逻辑就完成了,到了这里,其实我们本游戏就已经基本完成一半了,让我们继续。</p><h3>更改根元素字体的函数</h3><p>继续下一个素材列表页面组件的实现之前,我们先来看如何让页面根据浏览器设备自动更改字体大小的函数。</p><p>由于这里采用的是javascript写法,所以我直接写在了index.html文件里面,当然这并不是一个好的方式。</p><p>首先定义了一个自调用函数,在javascript中,我们通常是这样些自调用函数的:</p><pre><code class="js">(function(){
// 函数核心代码
})();</code></pre><p>事实上自调用函数不止可以使用括号包裹,还可以使用感叹号,加号等操作符,这里使用的就是感叹号!。</p><p>然后在这个自调用函数当中,传入了2个参数,第一个是window对象,第二个则是配置对象,如下:</p><pre><code class="js">!function(win,option){
//核心代码
}(window,{ designWidth: 750 })</code></pre><p>然后这个自调用函数可以拆分3部分,第一部分就是初始化变量,第二部分则是更改fontsize的函数,第三部分就是监听事件。我们先来看第一部分的变量初始化:</p><p>通过变量的初始化,我们可以看到option配置对象的参数有4个。如下:</p><pre><code class="js">var count = 0,
designWidth = option.designWidth,
designHeight = option.designHeight || 0,
designFontSize = option.designFontSize || 100,
callback = option.callback || null,
root = document.documentElement,
body = document.body,
rootWidth, newSize, t, self;</code></pre><p>下一个函数,设置字体大小的函数_getNewFontSize,这个函数主要是对字体大小做一个计算,取比例scale与设计图字体的大小相乘,比例可以通过宽度除以设计图宽度或者是高度除以设计图高度即可得到,而设计图宽度和高度就是option配置对象传入的值。代码如下:</p><pre><code class="js">function _getNewFontSize() {
const iw = win.innerWidth > 750 ? 750 : win.innerWidth;
const scale = designHeight !== 0 ? Math.min(iw / designWidth, win.innerHeight / designHeight) : iw / designWidth;
return parseInt(scale * 10000 * designFontSize) / 10000;
} </code></pre><p>下一步也是一个自调用函数,函数里面,我们做了判断,从而来确定设置字体的大小,代码如下:</p><pre><code class="js">!function () {
rootWidth = root.getBoundingClientRect().width;
self = self ? self : arguments.callee;
if (rootWidth !== win.innerWidth && count < 20) {
win.setTimeout(function () {
count++;
self();
}, 0);
} else {
newSize = _getNewFontSize();
if (newSize + 'px' !== getComputedStyle(root)['font-size']) {
// 核心代码就这一行
root.style.fontSize = newSize + "px";
return callback && callback(newSize);
};
};
}();</code></pre><p>最后监听屏幕旋转事件orientationchange和改变窗口大小事件resize,延迟调用设置字体大小函数即可。代码如下:</p><pre><code class="js">win.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", function () {
clearTimeout(t);
t = setTimeout(function () {
self();
}, 200);
}, false);</code></pre><p>到此为止,这个函数就分析完成了,让我们继续下一步。</p><h3>素材列表页面组件</h3><p>素材列表页面组件主要包含3个部分,如下:</p><ul><li>倒计时</li><li>素材列表</li><li>弹框逻辑</li></ul><p>本页面采用了浮动和rem布局。根据以上分析,我们的html代码就很简单了,如下:</p><pre><code class="html"><div class="bm-container bm-clearfix" :class="{ active:props.active }">
<!-- 倒计时部分 -->
<div class="bm-start-time">{{ count }}</div>
<!-- 素材列表部分 -->
<ul class="bm-game-list bm-clearfix">
<li class="bm-game-list-item" v-for="item inmaterialDataList" :key="item.id"
:class="{ active: item.active }" @click="() =>onClickHandler(item)"
:style="{ opacity: item.isMatch ? 0 : 1 }">
<img :src="item.src" :alt="item.title" class="bm-game-list-item-image" />
</li>
</ul>
<slot></slot>
<!-- 弹框组件 -->
<Modal v-model="showModal" :title="modalTitle" :content="modalContent" :okText="modalOkText"
@on-ok="onOkHandler" :maskCloseable="false" :show-cancel="false" />
</div></code></pre><p>我们用来自父组件的active属性用来确定这个组件是否显示,样式部分其实也没什么好说的,分成了两部分,第一部分是PC端的样式,第二部分则是移动端的样式。代码如下:</p><pre><code class="scss">$boxShadowColor: #eee;
$activeBorderColor: #2f3394;
$bgColor: #1f3092;
.#{$prefix}container {
@include setProperty(position, relative);
@include setProperty(padding, 0 .1rem .18rem .1rem);
@include setProperty(left, percentage(.5));
@include setProperty(top, percentage(.5));
@include setProperty(width, 10.9rem);
@include setProperty(height, auto);
@include setProperty(border-radius, .2rem);
@include setProperty(transform, translate(-50%, -50%));
@include setProperty(text-align, center);
@include setProperty(user-select, none);
@include setProperty(z-index, 99);
@include setProperty(background, $bgColor);
&.active {
@include setProperty(animation, bounceIn 1s);
@include setProperty(box-shadow, 0 0 .1rem .1rem $boxShadowColor);
@keyframes bounceIn {
from {
@include setProperty(opacity, 0);
}
to {
@include setProperty(opacity, 1);
}
}
}
.#{$prefix}start-time {
@include setProperty(position, absolute);
@include setProperty(top, -.4rem);
@include setProperty(color, $white);
@include setProperty(right, -.5rem);
@include setProperty(font-size, .28rem);
}
.#{$prefix}game-list {
@include setProperty(width, percentage(1));
@include setProperty(height, percentage(1));
@include setProperty(float, left);
@include setProperty(display, block);
&-item {
@include setProperty(float, left);
@include setProperty(margin, .18rem 0 0 .1rem);
@include setProperty(width, 1.67rem);
@include setProperty(height, .9rem);
@include setProperty(cursor, pointer);
@include setProperty(border, .03rem solid $white);
&:hover {
@include setProperty(box-shadow, 0 0 .2rem $white);
}
&.active {
@include setProperty(border-color, $activeBorderColor);
}
&-image {
@include setProperty(width, percentage(1));
@include setProperty(height, percentage(1));
@include setProperty(display, inline-block);
@include setProperty(vertical-align, top);
}
}
}
}
@media screen and (max-width: 640px) {
.#{$prefix}container {
@include setProperty(width, 6rem);
@include setProperty(padding-bottom, .3rem);
.#{$prefix}game-list {
&-item {
@include setProperty(width, percentage(.3));
@include setProperty(margin-left, .15rem);
@include setProperty(margin-top, .3rem);
}
}
}
}</code></pre><p>都是一些常规的样式布局,我们主要来看一下核心的逻辑,其实核心的逻辑在game.ts里面基本实现了,我们只需要拿出来用即可。</p><p>首先是用一个变量存储倒计时的值,其次用一个变量控制弹框组件的显隐,还有3个变量分别代表弹框组件的标题,内容和确定按钮的内容,为什么要用变量代表弹框组件的标题,内容和确定按钮的内容呢?</p><p>这里我们的游戏分为两种状态,第一种就是游戏胜利,第二种则是游戏失败,两种状态的反馈提示是不一样的,所以才需要变量来代替。</p><p>所以以下代码就比较好理解了。</p><pre><code class="ts">import { PropType, ref, watch } from 'vue';
import useGame from '../core/game';
import { CountDown } from '../utils/util';
import Modal from '../components/Modal.vue';
const count = ref<number>();
const showModal = ref(false);
const modalTitle = ref<string>('温馨提示');
const modalContent = ref<string>();
const modalOkText = ref<string>();</code></pre><p>接下来,我们获取游戏核心逻辑函数中导出的方法和数据,如下:</p><pre><code class="ts">const { materialDataList, onClickHandler, gameConfig, totalMatch,onStartGame,gameStatus } = useGame();</code></pre><p>随后,我们定义一个active的属性,用来确定这个组件是否显示,动画效果已经在scss中实现了,就是渐隐效果,通过类名控制,如以上的模版代码中所写。</p><p>接着,我们定义好暴露给父组件的事件,分为3种,游戏结束,游戏胜利和点击弹框确认按钮事件。代码如下:</p><pre><code class="ts">const props = defineProps({
active: {
type: Boolean as PropType<boolean>
}
})
const emit = defineEmits(['on-game-over', 'on-win', 'on-ok']);</code></pre><p>最后,我们监听props.active,如果这个值是true,就代表这个组件显示,也就代表游戏开始,然后我们执行倒计时函数,在倒计时回调函数中,我们通过返回的status是否等于end来判定倒计时时间是否已执行完成,随后我们如前面所说,根据totalMatch是否等于素材列表的长度代表用户是否消除掉所有图片素材,从而确定游戏是否胜利,游戏结束和游戏胜利,我们都要清空倒计时的定时器,并且修改弹框组件的内容和确定按钮的文本,然后暴露出事件传递给父组件,因为父组件可能会在游戏胜利和游戏结束中执行一些逻辑,比如添加音效之类的,所以我们暴露出去。根据这个分析,以下代码就比较好理解了。</p><pre><code class="ts">watch(() => props.active, (val) => {
if (val) {
CountDown(gameConfig.time, 0, 1, 1000, (res) => {
count.value = res.value;
const isWin = () => totalMatch.value === materialDataList.value.length;
if (res.status === 'end') {
if (!isWin()) {
showModal.value = true;
modalContent.value = `游戏已结束!`;
modalOkText.value = '重新开始';
res.clear?.();
emit('on-game-over');
}
} else {
if (isWin()) {
showModal.value = true;
modalContent.value = `完成游戏共耗时:${gameConfig.time - count.value}s!`;
modalOkText.value = '再玩一次';
res.clear?.();
emit('on-win');
}
}
});
}
});</code></pre><p>然后还有一个逻辑,就是点击确认按钮事件,这个没什么好说的,就是重置游戏的素材列表和一些状态。如下:</p><pre><code class="ts">const onOkHandler = () => {
showModal.value = false;
onStartGame();
totalMatch.value = 0;
emit('on-ok');
}</code></pre><p>到此为止,这个素材列表组件就完成了,最后一步就是根组件App.vue里面了,这里面主要做一些音效逻辑,我们来详细看一下吧。</p><h3>根组件里的逻辑实现</h3><p>根组件主要处理6种音效逻辑,并且用一种状态控制素材列表页面和首页的切换,然后还有一个逻辑,就是使用我们已经封装好的下雪花的逻辑。我们来看模板代码,如下:</p><pre><code class="html"><!-- 雪花效果容器元素 -->
<div ref="snow" class="bm-snow"></div>
<!-- 音效元素 -->
<audio :src="bgMusic" ref="bgAudio"></audio>
<audio :src="readyMusic" ref="readyAudio"></audio>
<audio :src="rightMusic" id="rightAudio"></audio>
<audio :src="wrongMusic" id="wrongAudio"></audio>
<audio :src="loseMusic" ref="loseAudio"></audio>
<audio :src="winMusic" ref="winAudio"></audio>
<!-- ready和go组件以及按钮组件 -->
<Ready v-model="countShow" v-show="countShow"></Ready>
<Go v-model="countShow" v-show="countShow"/>
<Button @click="onStart" :style="{ display: countShow ? 'none' : 'block'}"></Button>
<!-- 素材列表组件 -->
<Container
v-show="gameStatus === 1"
:active="gameStatus === 1"
@on-game-over="onGameOver"
@on-win="onWin"
@on-ok="onOkHandler"
></Container></code></pre><p>样式也没什么好说的,就是给雪花效果容器元素设置一下,让它撑满全屏即可,用绝对定位。</p><pre><code class="scss">.#{$prefix}snow {
@include setProperty(width,percentage(1));
@include setProperty(height,percentage(1));
@include setProperty(position,absolute);
@include setProperty(z-index,0);
}</code></pre><p>js逻辑代码也很简单,都是一些资源导入以及变量的初始化,还有就是相关事件的逻辑。看下源码基本很好理解。</p><pre><code class="ts">import { onMounted,ref } from 'vue';
import Snow from './utils/snow';
import Button from './components/Button.vue';
import Go from './components/Go.vue';
import Ready from './components/Ready.vue';
import bgMusic from '@/assets/audio/bgMusic.mp3';
import readyMusic from '@/assets/audio/go.mp3';
import rightMusic from '@/assets/audio/right.mp3';
import wrongMusic from '@/assets/audio/wrong.mp3';
import loseMusic from '@/assets/audio/lose.mp3';
import winMusic from '@/assets/audio/win.mp3';
import Container from './views/Container.vue';
import { useConfigStore } from './store/store';
import useGame from './core/game';
// 使用到的游戏配置和游戏状态
const { setGameConfig,gameConfig } = useConfigStore();
const { gameStatus } = useGame();
// 一些状态
const snow = ref<HTMLDivElement>();
const countShow = ref(false);
const bgAudio = ref<HTMLAudioElement>();
const readyAudio = ref<HTMLAudioElement>();
const loseAudio = ref<HTMLAudioElement>();
const winAudio = ref<HTMLAudioElement>();
// 游戏开始
const onStart = () => {
countShow.value = true;
readyAudio.value?.play();
bgAudio.value?.play();
bgAudio.value?.setAttribute('loop','loop');
setTimeout(() => {
setGameConfig({
...gameConfig,
gameStatus: 1
})
gameStatus.value = 1;
},1800);
}
// 关闭背景音效
const onStopMusic = () => {
bgAudio.value?.pause();
}
// 游戏结束
const onGameOver = () => {
onStopMusic();
loseAudio.value?.play();
}
// 游戏胜利
const onWin = () => {
onStopMusic();
winAudio.value?.play();
}
// 确认按钮的逻辑
const onOkHandler = () => {
countShow.value = false;
gameStatus.value = 0;
}
onMounted(() => {
// 初始化雪花效果
if(snow.value){
const s = new Snow(snow.value!);
s.init();
}
});</code></pre><p>到此为止,我们的连连看小游戏就算是大功告成了,当然我只是完成了一个基础版,我们还可以扩展,比如游戏时间的设置,以及素材列表的设置,那就是再添加一个配置页面,或许到了后面我会扩展也说不一定。</p><p>本文源码点<a href="https://link.segmentfault.com/?enc=VqZobmv1olB1D1L%2FGxLaYw%3D%3D.d8H%2FFhwu1ORBpik0maZsBJ4pJnVRhtL4CRBOPsWUO7sxL%2BU9721Peb6DAILAoeHlpyhtRa0NbYaCDT97iCoZ2ITU6z46XxAuNpg7zeJOMCs%3D" rel="nofollow">这里</a>。<br>在线示例点<a href="https://link.segmentfault.com/?enc=3UP8w%2BM%2FgVVqr%2BrqCqwOkQ%3D%3D.9lm%2Bgp3qAK3f58JmO6GY9LwTA36KjxJoAfu9pnBMJrtaQeLzfqVjk8JsrzK5bJj3z%2F3cRTJCqdUxNVB4AHVdYQ%3D%3D" rel="nofollow">这里</a>。</p>
实现《羊了个羊-美女版》小游戏(低配版)
https://segmentfault.com/a/1190000042575730
2022-10-06T12:20:39+08:00
2022-10-06T12:20:39+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
5
<h3>实现《羊了个羊》小游戏(低配版)</h3><p>前面有总结过一篇<a href="https://segmentfault.com/a/1190000042502121">《简易动物版的羊了个羊》</a>的实现,今天参考了鱼皮大佬的<a href="https://link.segmentfault.com/?enc=lhGiwjtAC0ALNPK5Trxkcg%3D%3D.yywgLBEU7UG7WNuf8QNBH1Dk%2BOTku8wPQTL%2FXL7dI97UJG6zlVtoyAp%2Fri7DENnO" rel="nofollow">鱼了个鱼</a>,我好奇研究了一下他的源码实现,并结合自己的理解,也用react + antd实现了一版,在实现的过程中,我只是简单修改了一下核心逻辑和游戏配置,然后借助于<a href="https://link.segmentfault.com/?enc=pOizabUKhSEnzbckJxfAog%3D%3D.lkKnyYQhAMI8cmj%2BkWBbFUiwl0FtzqHVf4lJzdNPN0g7ur5yiHGhQCBJb0M4zWHW" rel="nofollow">reactivue</a>这个库将vue的composition API用到了这里。</p><p>游戏的核心逻辑我也大致理了一遍,只是做了一些小改动就拿过来用了,里面也有源码实现的注释,所以不用多讲,主要在于核心UI的构建,这里可以大致讲一下。</p><p>在这里我也用到了之前文章<a href="https://segmentfault.com/a/1190000042517443">手写一个mini版本的React状态管理工具</a>来用作游戏参数的状态管理,因此关于这里的实现也没有必要多做细讲。接下来,我们就来看它在这里的使用。如下所示:</p><pre><code class="tsx">import { createModel } from './createModel';
import { useState } from "react"
import { defaultGameConfig } from '../core/gameConfig';
export interface ConfigType {
gameConfig: GameConfigType
customGameConfig: GameConfigType
}
const useConfig = () => {
const [config,setConfig] = useState<ConfigType>({
gameConfig:{
...defaultGameConfig
},
customGameConfig:{
...defaultGameConfig
}
})
const updateConfig = (config:ConfigType) => {
setConfig(config)
}
// 重置为初始值
const reset = () => setConfig({ gameConfig:{ ...defaultGameConfig },customGameConfig:{ ...defaultGameConfig } })
return {
config,
updateConfig,
reset
}
}
const GameConfigStore = createModel(useConfig);
export default GameConfigStore;</code></pre><p>首先导入createModel方法还有react的useState方法以及游戏参数的默认配置,接着定义一个hooks用作createModel的参数,返回游戏的配置以及更新配置函数以及还原最初初始配置的方法,即updateConfig和reset方法,这里其实也没有多大的难度,主要可能在于类型的定义。</p><p>我们来看接口的定义,也在全局type.d.ts文件下,如下所示:</p><pre><code class="ts">/**
* 块类型
*/
interface BlockType {
id: number;
x: number;
y: number;
level: number;
type: string;
// 0 - 正常, 1 - 已点击, 2 - 已消除
status: 0 | 1 | 2;
// 压住的其他块
higherThanBlocks: BlockType[];
// 被哪些块压住(为空表示可点击)
lowerThanBlocks: BlockType[];
}
/**
* 每个格子单元类型
*/
interface BlockUnitType {
// 放到当前格子里的块(层级越高下标越大)
blocks: BlockType[];
}
/**
* 游戏配置类型
*/
interface GameConfigType {
// 槽容量
slotNum: number;
// 需要多少个一样块的才能合成
composeNum: number;
// 素材类别数
materialTypeNum: number;
// 每层块数(大致)
levelBlockNum: number;
// 边界收缩步长
borderStep: number;
// 总层数(最小为 2)
levelNum: number;
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: number[];
// 素材列表
materialList: Record<string,string> [];
// 最上层块数(已废弃)
// topBlockNum: 40,
// 最下层块数最小值(已废弃)
// minBottomBlockNum: 20,
}
/**
* 技能类型
*/
interface SkillType {
name: string;
desc: string;
icon: string;
action: function;
}</code></pre><p>接口的定义大同小异,也只是在原版的基础上稍微做了一点修改。</p><p>定义好状态之后,我们在App.tsx中导入使用,App.tsx中代码如下:</p><pre><code class="tsx">import styled from '@emotion/styled';
import { BASE_IMAGE_URL } from './core/gameConfig';
import Router from './router/router';
import GameConfigStore from './store/store';
const StyleApp = styled.div({
background:`url(${BASE_IMAGE_URL}1.jpg)no-repeat center/cover`,
padding:'16px 16px 50px',
minHeight:'100vh',
backgroundSize:"100% 100%",
width:"100%"
});
const StyleContent = styled.div`
max-width:480px;
margin: 0 auto;
`
const App = () => {
const { Provider } = GameConfigStore;
return (
<StyleApp>
<Provider>
<StyleContent>
<Router />
</StyleContent>
</Provider>
</StyleApp>
)
}
export default App</code></pre><p>可以看到,我们通过对象结构的方式在App组件中取得Provider组件,将Provider组件包裹中间的路由组件,事实上Provider组件还可以传入一个默认参数的值,在这里我们并没有传入。</p><p>这个组件其实主要考察了2个知识点,第一个就是styled-component,这里用到的是<a href="https://link.segmentfault.com/?enc=zI4Gdsv8%2FVnilIV5FlNxNQ%3D%3D.1PNY0M25yKWP0GlF%2B0N1LykZXnAfTMZIJnKAmgseNzb6aNB%2Bt4t%2BnzfcOMVbhAUu" rel="nofollow">@emotion/styled</a>这个库,有关语法的使用可以详细看<a href="https://link.segmentfault.com/?enc=FovTtIoMGmEjOPxwUDLQTg%3D%3D.xGsrJ1c%2BP3crVw41gzRgWegC2%2BT3f%2Fwrz0b2BCoV%2Bj3bzU12PS1iwIBh%2FFBTqXdH" rel="nofollow">官方文档</a>。</p><p>第二个知识点就是路由的使用,我们来看Router.tsx的代码,如下:</p><pre><code class="tsx">import React from 'react';
import { useRoutes } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import Load from '../components/Loader';
const lazy = (component: <T>() => Promise<{ default: React.ComponentType<T> }>) => {
const LazyComponent = React.lazy(component);
return (
<React.Suspense fallback={<Load></Load>} >
<LazyComponent></LazyComponent>
</React.Suspense>
)
}
const routes:RouteObject [] = [
{
path:"/",
element:lazy(() => import('../views/IndexPage'))
},
{
path:"/config",
element:lazy(() => import('../views/ConfigPage'))
},
{
path:"/game",
element:lazy(() => import('../views/GamePage'))
}
]
export default () => useRoutes(routes)</code></pre><p>路由里面,其实也就是用到了懒加载lazy函数,以及Suspense组件,然后通过useRoutes方法导出一个路由组件使用,这样就可以像Vue那样通过配置路由的方式来使用路由。</p><p>这里有一个load组件的实现,我们来看它的源码,如下所示:</p><pre><code class="tsx">import styled from '@emotion/styled';
import { Spin } from 'antd';
const StyleLoad = styled.div({
display:'flex',
minHeight:'100vh',
justifyContent:'center',
alignItems:"center"
});
export interface LoadProps {
message?: string
}
const Load = (props: Partial<LoadProps>) => {
const { message = 'loading....' } = props;
return (
<StyleLoad>
<Spin tip={message}></Spin>
</StyleLoad>
)
}
export default Load;</code></pre><p>其实也很简单,就是添加了一个message的props配置,然后使用antd的Spin组件。</p><p>接下来就是核心的三个页面,分别是首页,选择游戏模式,以及自定义游戏配置和游戏页面,这个我们后面再看,这里我们来看一下有一个hooks函数,也就是强制更新的hooks函数useForceUpdate,如下:</p><pre><code class="ts">import { useReducer } from 'react';
function useForceUpdate() {
const [, dispatch] = useReducer(() => Object.create(null), {});
return dispatch;
}
export default useForceUpdate;</code></pre><p>其实也就是利用useReducer函数强制更新组件,这里为什么要用这个hook函数呢?答案其实就在game.ts里面,我们来看game.ts里面:</p><pre><code class="ts">
import _ from "lodash";
import { createSetup } from 'reactivue'
import GameConfigStore from "../store/store";
const useGame = () => {
const { config: { gameConfig } } = GameConfigStore.useModel();
const setup = createSetup(() => {
// 核心游戏逻辑,参考源码注释
});
return setup();
};
export default useGame;</code></pre><p>可以看到这里,我们通过导出一个useGame函数,就可以在游戏页面里使用这些核心的游戏逻辑接口,但是我们游戏里面的核心逻辑是使用的Vue的响应式数据对象,尽管vue帮我们更新了数据,可是react并不知道数据是否更新,所以这里就采用useForceUpdate函数来更新数据,虽然这并不是一种好的更新数据的方式。</p><blockquote>如果有更好的更新视图和数据的方式,还望大佬指点迷津。</blockquote><p>至于游戏配置文件也没什么好解释的,可以自己看一下源码。</p><p>接下来,我们来看组件的实现,这里有几个公共组件About.tsx,Footer.tsx,Win.tsx以及Title.tsx的实现,其中可能也就Win.tsx中的爱心组件和Title.tsx组件的实现可以说一下,我们先来看Title.tsx的实现。如下:</p><pre><code class="tsx">import { ElementType, ReactNode } from "react";
export interface TitleProps extends Record<string,any>{
children: ReactNode
level: number | string
}
const levelList = [1,2,3,4,5,6];
const Title = (props: Partial<TitleProps>) => {
const { level,children,...rest } = props;
const Component = (level && levelList.includes(+level) ? `h${level}` : 'h1') as ElementType;
return (
<Component {...rest}>{ children }</Component>
)
}
export default Title;</code></pre><p>这里值得说一下的就是将Component断言成ElementType元素,从逻辑上似乎有些说不通,因为本身Component就是一个字符串,可是字符串在react当中也可以算作是一个元素组件,所以也就断言成ElementType也就理所当然没问题啦。</p><p>然后就是Heart组件的实现,实际上也就是利用css画一个爱心,如下所示:</p><pre><code class="tsx">const StyleHeart = styled.div({
width: 25,
height: 25,
background:"#e63f0c",
position: 'relative',
margin: '1em auto',
transform:'rotate(45deg)',
animation:'scale 2s linear infinite',
'@keyframes scale':{
'0%':{
transform:'scale(1) rotate(45deg)'
},
'100%':{
transform:"scale(1.2) rotate(45deg)"
}
},
'&::before,&::after':{
content:'""',
width:'100%',
height:'100%',
borderRadius:'50%',
position:'absolute',
background:"#e63f0c",
},
'&::before':{
left:'-15px',
top: 0
},
'&::after':{
top:'-15px',
left: 0
}
});</code></pre><p>这是emotion的语法。</p><p>接下来就是对三个页面的详解,首先我们来看首页,代码如下:</p><pre><code class="tsx">
import styled from '@emotion/styled';
import Title from '../components/Title';
import { Button } from 'antd';
import About from '../components/About';
import Footer from '../components/Footer';
import { useNavigate } from 'react-router-dom';
import { easyGameConfig, hardGameConfig, lunaticGameConfig, middleGameConfig, skyGameConfig, yangGameConfig } from '../core/gameConfig';
import GameConfigStore from '../store/store';
const StyleIndexPage = styled.div({
textAlign:'center'
});
const StyleTitle = styled(Title)({
});
const StyleDescription = styled.div`
margin-bottom: 16px;
color: rgba(0,0,0,.8);
`;
const StyleButton = styled(Button)({
marginBottom: 16
})
const ButtonList = [
{
text:"简单模式",
config:easyGameConfig
},
{
text:"中等模式",
config:middleGameConfig
},
{
text:"困难模式",
config:hardGameConfig
},
{
text:"地狱模式",
config:lunaticGameConfig
},
{
text:"天狱模式",
config:skyGameConfig
},
{
text:"羊了个羊模式",
config:yangGameConfig
},
{
text:"自定义",
config:null
}
]
const IndexPage = () => {
const navigate = useNavigate();
const { config:{ customGameConfig },updateConfig } = GameConfigStore.useModel();
const toGame = (config:GameConfigType | null) => {
if(config){
updateConfig({ gameConfig: config,customGameConfig });
navigate('/game');
}else{
navigate('/config');
}
}
return (
<StyleIndexPage>
<StyleTitle level={2}>羊了个羊(美女版)</StyleTitle>
<StyleDescription>低配版羊了个羊小游戏,仅供消遣</StyleDescription>
{
ButtonList.map((item,index: number) => (
<StyleButton block onClick={() => toGame(item.config)} key={`${item.text}-${index}`}>{ item.text }</StyleButton>
))
}
<About />
<Footer />
</StyleIndexPage>
)
}
export default IndexPage;</code></pre><p>其实也比较好理解,就是渲染一个按钮列表,然后给按钮列表添加导航跳转,将游戏的配置传过去,而核心当然是用到我们定义的状态来传递。</p><p>useNavigate方法也就是react-router-dom提供的API,也没什么好说的,我们继续来看ConfigPage.tsx,本质上这个页面就是一个表单页面,所以也没什么好说的。代码如下:</p><pre><code class="tsx">import styled from '@emotion/styled'
import Title from '../components/Title'
import { Button, Form, InputNumber, Select, Image, Tooltip } from 'antd'
import { useNavigate } from 'react-router-dom'
import { materialList } from '../core/gameConfig'
import { useEffect, useState } from 'react'
import GameConfigStore from '../store/store'
const StyleConfigPage = styled.div`
padding: 5px;
`
const { Item } = Form
const { Option } = Select
const StyleTitle = styled(Title)({
'&::before,&::after': {
clear: 'both',
display: 'block',
content: '""'
}
})
const StyleButton = styled(Button)({
float: 'right'
})
const StyleInputNumber = styled(InputNumber)({
'width': "100%"
})
const StyleFooterButton = styled(Button)({
marginBottom: 16
})
const ConfigPage = () => {
const navigate = useNavigate()
const [form] = Form.useForm()
const { config: { customGameConfig },reset,updateConfig } = GameConfigStore.useModel()
const setFormConfig = (config: GameConfigType) => {
const { materialList: configMaterialList, ...rest } = config
return {
...rest,
materialNum: configMaterialList.map(item => item.value),
randomAreaNum: 2,
randomBlockNum: 8,
}
}
const [customFormConfig,setCustomFormConfig] = useState(setFormConfig(customGameConfig))
useEffect(() => {
setCustomFormConfig(setFormConfig(customGameConfig))
},[customGameConfig])
const goBack = () => {
navigate(-1);
}
const onFinishHandler = () => {
const config = form.getFieldsValue(true);
config.materialList = config.materialNum.map((key: string) => materialList.find(item => item.value === key));
delete config.materialNum;
updateConfig({
gameConfig: config,
customGameConfig: config
});
navigate('/game');
}
return (
<StyleConfigPage>
<StyleTitle level={2}>
自定义难度
<StyleButton onClick={goBack}>返回</StyleButton>
</StyleTitle>
<Form
autoComplete="off"
label-align="left"
labelCol={{ span: 4 }}
onFinish={onFinishHandler}
form={form}
initialValues={customFormConfig}
>
<Item label="槽容量" name="slotNum">
<StyleInputNumber />
</Item>
<Item label="合成数" name="composeNum">
<StyleInputNumber />
</Item>
<Item label="素材类别数" name="materialTypeNum">
<StyleInputNumber />
</Item>
<Item label="素材列表" name="materialNum">
<Select mode='multiple'>
{
materialList.map((item, index) => (
<Option key={`${item.value}-${index}`} value={item.value}>
<Tooltip title={item.label} placement="leftTop">
<Image src={item.value} width={40} height={40}></Image>
</Tooltip>
</Option>
))
}
</Select>
</Item>
<Item label="总层数" name="levelNum">
<StyleInputNumber />
</Item>
<Item label="每层块数" name="levelBlockNum">
<StyleInputNumber />
</Item>
<Item label="边界收缩" name="borderStep">
<StyleInputNumber />
</Item>
<Item label="随机区数" name="randomAreaNum">
<StyleInputNumber />
</Item>
<Item label="随机区块数" name="randomBlockNum">
<StyleInputNumber />
</Item>
<Item>
<StyleFooterButton htmlType='submit' block>开始</StyleFooterButton>
<StyleFooterButton block onClick={() => form.resetFields()}>重置</StyleFooterButton>
<Button danger block onClick={reset}>还原初始配置</Button>
</Item>
</Form>
</StyleConfigPage>
)
}
export default ConfigPage</code></pre><p>antd提供了useForm方法可以将表单数据很好的管理在一个状态中,我们也就不用为每个表单元素绑定value和change事件了。</p><p>接下来就是游戏核心页面,如下:</p><pre><code class="tsx">import styled from "@emotion/styled";
import { Row, Button, Space } from "antd";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import Win from "../components/Win";
import useGame from "../core/game";
import { AUDIO_URL } from "../core/gameConfig";
import useForceUpdate from "../hooks/useForceUpdate";
const StyleGamePage = styled.div({
padding: 5,
});
const StyleBackButton = styled(Button)({
marginBottom: 8,
});
const StyleLevelBoard = styled.div<{ show: boolean }>`
display: ${({ show }) => (show ? "block" : "none")};
position: relative;
`;
const StyleBlock = styled.div({
width: 42,
height: 42,
border: "1px solid #eee",
display: "inline-block",
verticalAlign: "top",
background: "#fff",
cursor: "pointer",
"& .image ": {
width: "100%",
height: "100%",
border: "none",
objectFit: "cover",
},
"&.disabled": {
background: "rgba(0,0,0,.85)",
cursor: "not-allowed",
"& .image": {
display: "none",
},
},
});
const StyleLevelBlock = styled(StyleBlock)`
position: absolute;
`;
const StyleRandomBoard = styled(Row)({
marginTop: 8,
});
const StyleRandomArea = styled.div({
marginTop: 8,
});
const StyleSlotBoard = styled(Row)({
border: "10px solid #2396ef",
margin: "16px auto",
width: "fit-content",
});
const StyleSkillBoard = styled.div({
textAlign: "center",
});
const skillList = [
{
method: "doRevert",
text: "撤回",
},
{
method: "doRemove",
text: "移出",
},
{
method: "doShuffle",
text: "洗牌",
},
{
method: "doBroke",
text: "破坏",
},
{
method: "doHolyLight",
text: "圣光",
},
{
method: "doSeeRandom",
text: "透视",
},
];
const GamePage = () => {
const navigate = useNavigate();
const forceUpdate = useForceUpdate();
const onBack = () => navigate(-1);
const [isPlayed, setIsPlayed] = useState(false);
const [audio, setAudio] = useState<HTMLAudioElement>();
const {
clearBlockNum,
totalBlockNum,
gameStatus,
levelBlocksVal,
doClickBlock,
isHolyLight,
widthUnit,
heightUnit,
randomBlocksVal,
canSeeRandom,
slotAreaVal,
...rest
} = useGame();
useEffect(() => {
if (!audio) {
const audio = new Audio();
audio.src = AUDIO_URL;
setAudio(audio);
}
if (isPlayed) {
audio?.play();
} else {
audio?.pause();
}
}, [isPlayed]);
return (
<StyleGamePage>
<Row justify="space-between">
<StyleBackButton onClick={onBack}>返回</StyleBackButton>
<Button onClick={() => setIsPlayed(!isPlayed)}>
{isPlayed ? "暂停" : "播放"}
</Button>
<Button>
块数: {clearBlockNum} / {totalBlockNum}
</Button>
</Row>
<Win isWin={gameStatus === 3}></Win>
<Row justify="center">
<StyleLevelBoard className="level-board" show={gameStatus > 0}>
{levelBlocksVal?.map((block, index) => (
<div key={`${block.id}-${index}`}>
{block.status === 0 ? (
<StyleLevelBlock
className={`${
!isHolyLight && block.lowerThanBlocks.length > 0
? "disabled"
: ""
}`}
data-id={block.id}
style={{
zIndex: 100 + block.level,
left: block.x * widthUnit + "px",
top: block.y * heightUnit + "px",
}}
onClick={() => {
doClickBlock(block);
forceUpdate();
}}
>
<img className="image" src={block.type} alt={block.type} />
</StyleLevelBlock>
) : null}
</div>
))}
</StyleLevelBoard>
</Row>
<StyleRandomBoard justify="space-between">
{randomBlocksVal?.map((item, index) => (
<StyleRandomArea key={`${item}-${index}`}>
{item.length > 0 ? (
<StyleBlock
data-id={item[0].id}
onClick={() => {
doClickBlock(item[0], index);
forceUpdate();
}}
>
<img className="image" src={item[0].type} alt={item[0].type} />
</StyleBlock>
) : null}
{item?.slice(1).map((randomBlock, index) => (
<StyleBlock
className="disabled"
key={`${randomBlock.id}-${index}`}
>
<img
className="image"
src={randomBlock.type}
alt={randomBlock.type}
style={{
display: canSeeRandom ? "inline-block" : "none",
}}
/>
</StyleBlock>
))}
</StyleRandomArea>
))}
</StyleRandomBoard>
{
<StyleSlotBoard>
{slotAreaVal?.map((item, index) => (
<StyleBlock key={`${item?.id}-${index}`}>
<img src={item?.type} alt={item?.type} className="image" />
</StyleBlock>
))}
</StyleSlotBoard>
}
<StyleSkillBoard>
<Space>
{skillList.map((item, index) => (
<Button
size="small"
key={item.method + "-" + index}
onClick={() => {
const methods = { ...rest };
const handler = methods[item.method as keyof typeof methods];
if (typeof handler === "function") {
handler();
forceUpdate();
}
}}
>
{item.text}
</Button>
))}
</Space>
</StyleSkillBoard>
</StyleGamePage>
);
};
export default GamePage;</code></pre><p>可以看到这个页面,我们恰好就用了forceUpdate方法,一个是在点击块的时候使用了,还要一个就是点击对应的圣光,撤销等按钮的时候也调用了这个方法强行更新视图。</p><p>这里也添加了一个音乐的播放配置,代码也很简单,就是监听一个播放状态,然后创建audio元素。可能比较不好理解的是这段代码:</p><pre><code class="ts">const methods = { ...rest };
const handler = methods[item.method as keyof typeof methods];
if (typeof handler === "function") {
handler();
forceUpdate();
}</code></pre><p>其实就是对应的字符串方法名从我们导出的useGame核心逻辑拿方法并调用而已。</p><p>到此为止,我们这个游戏就算是完成了,感谢鱼皮大佬的贡献,让我对这个游戏的原理实现有了更深刻的认识。</p><p>以下是源码和示例,</p><p><a href="https://link.segmentfault.com/?enc=ZxV%2BNsLsyl3c54Yeky%2FbDw%3D%3D.%2FA%2FuOvpF16MUBnXov0ab7uTaXisVe27OSZ2FStoIsdESo756oKNGig4QBZe9h56RigZ1hYsetAKlADtQISt2RXSYpBnUdK4rJoes40AO%2F4Y%3D" rel="nofollow">游戏源码</a></p><p><a href="https://link.segmentfault.com/?enc=BaOXzTdrU7GTT6bGQgs5PQ%3D%3D.MUGGmrVVnkfuLWPHzQ31RJHoAKU%2BIOUJDUreAVenWM9bNhbm98U71cUPKIgn1D7sLK4YvQJO6zRCwFoU3KYI1g%3D%3D" rel="nofollow">在线示例</a></p>
手写一个mini版本的React状态管理工具
https://segmentfault.com/a/1190000042517443
2022-09-20T20:34:11+08:00
2022-09-20T20:34:11+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
3
<h3>手写一个mini版本的React状态管理工具</h3><p>目前在React中,有很多各式各样的状态管理工具,如:</p><ul><li><a href="https://link.segmentfault.com/?enc=%2BtlIGVMlz%2F76Ioqnpgy3PQ%3D%3D.%2FBzTb2Atn8BMPDoPn7WwKoj7%2FecciWDWhQRmsNmtej0%3D" rel="nofollow">React Redux</a></li><li><a href="https://link.segmentfault.com/?enc=t08GKBFFkuC6J1rAxHpZjQ%3D%3D.LQoQ7N99TkPihA85Fq1bmmx15GZcRxBnx7H3qmt%2Bl9M%3D" rel="nofollow">Mobx</a></li><li><a href="https://link.segmentfault.com/?enc=FzkyfXRkxUZT8wOqJYtVhg%3D%3D.ZpNez2YS6ElFXsOVWUVB27sqOVAMpdVlFsP%2Bdb5iBAs%3D" rel="nofollow">Hox</a></li></ul><p>每一个状态管理工具都有着不尽相同的API和使用方式,而且都有一定的学习成本,而且这些状态管理工具也有一定的复杂度,并没有做到极致的简单。在开发者的眼中,只有用起来比较简单,那么才会有更多的人去使用它,Vue不就是因为使用简单,上手快,而流行的吗?</p><p>有时候我们只需要一个全局的状态,放置一些状态和更改状态的函数就足够了,这样也达到了最简化原则。</p><p>下面让我们一起来实现一个最简单的状态管理工具吧。</p><p>这个状态管理工具的核心就使用到了<a href="https://link.segmentfault.com/?enc=V6TSYTieq5SJxpzhDpfy5w%3D%3D.AaI8mGY%2FTjrIseWEwpKST0bv1mTF5hAXb%2FNZwhkynqIHiLmAsSMTzxSARtwCiQuT" rel="nofollow">Context API</a>,在了解本文之前务必先了解并熟悉使用这个API的用法。</p><p>首先我们来看这个状态管理工具是如何使用的。假设有一个计数器状态,然后我们通过二个方法分别去修改计数器,也就是做加法和减法,换句话说我们需要用到一个计数器状态,二个方法来修改这个状态。在React函数组件中,我们使用useState方法来初始化一个状态,因此,我们可以很容易的写出如下代码:</p><pre><code class="ts">import { useState } from 'react'
const useCounter = (initialCount = 0) => {
const [count,setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return {
count,
increment,
decrement
}
}
export default useCounter;</code></pre><p>现在,让我们创建一个组件来使用这个useCounter钩子函数,如下:</p><pre><code class="tsx">import React from 'react'
import useCounter from './useCounter'
const Counter = () => {
const { count,increment,decrement } = useCounter();
return (
<div className="counter">
{ count }
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={decrement}>plus</button>
</div>
)
}</code></pre><p>然后在根组件App当中使用,如下:</p><pre><code class="tsx">import React from 'react'
const App = () => {
return (
<div className="App">
<Counter />
<Counter />
</div>
)
}</code></pre><p>这样,一个计数器组件就大功告成了,可是真的只是这样吗?</p><p>首先,我们应该知道计数器组件的状态应该是一致的,也就是说我们的计数器组件应该是共享同一个状态,那么如何共享同一个状态?这时候就需要Context出场了。将以上的组件改造一下,我们将状态放在根组件App当中初始化,并且传到子组件中去。先修改App根组件的代码如下:</p><p>新建一个CounterContext.ts文件,代码如下:</p><pre><code class="ts">const CounterContext = createContext();
export default CounterContext;</code></pre><pre><code class="tsx">import React,{ createContext } from 'react'
import CounterContext from './CounterContext'
const App = () => {
const { count,increment,decrement } = useCounter();
return (
<div className="App">
<CounterContext.Provider value={{count,increment,decrement}}>
<Counter />
<Counter />
</CounterContext.Provider>
</div>
)
}</code></pre><p>然后在Counter组件代码我们也修改如下:</p><pre><code class="tsx">import React,{ useContext } from 'react'
import CounterContext from './CounterContext'
const Counter = () => {
const { count,increment,decrement } = useContext(CounterContext);
return (
<div className="counter">
{ count }
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={decrement}>plus</button>
</div>
)
}</code></pre><p>如此一来,我们就可以共享count状态,无论是在多深的子组件当中使用都没有问题,但是这并没有结束,让我们继续。</p><p>虽然这样使用解决了共享状态的问题,可是我们发现,我们在使用的时候还要额外的传入一个context名,所以我们需要包装一下,到最后,我们只需要像如下这样使用:</p><pre><code class="ts">const Counter = createModel(useCounter);
export default Counter;</code></pre><pre><code class="tsx">const { Provider,useModel } = Counter;</code></pre><p>然后我们的App组件就应该是这样:</p><pre><code class="tsx">import React,{ createContext } from 'react'
import counter from './Counter'
const App = () => {
const { Provider } = counter;
return (
<div className="App">
<Provider>
<Counter />
<Counter />
</Provider>
</div>
)
}</code></pre><p>继续修改我们的Counter组件,如下:</p><pre><code class="tsx">import React,{ useContext } from 'react'
import counter from './Counter'
const Counter = () => {
const { count,increment,decrement } = counter.useModel();
return (
<div className="counter">
{ count }
<button type="button" onClick={increment}>add</button>
<button type="button" onClick={decrement}>plus</button>
</div>
)
}</code></pre><p>通过以上代码的展示,其实我们也就明白了,我们无非是将useContext和createContext内置到我们封装的Model里面去了。</p><p>接下来我们就来揭开这个状态管理工具的神秘面纱,首先要用到React相关的API,所以我们需要导入进来。如下:</p><pre><code class="tsx">// 导入类型
import type { ReactNode,ComponentType } from 'react';
import { createContext,useContext } from 'react';</code></pre><p>接下来定义一个唯一标识,用于确定传入的Context,并且这个用来确定使用者使用Context时是正确使用的。</p><pre><code class="tsx">const EMPTY:unique symbol = Symbol();</code></pre><p>接下来我们要定义Provider的类型。如下:</p><pre><code class="tsx">export interface ModelProviderProps<State = void> {
initialState?: State
children: ReactNode
}</code></pre><p>以上我们定义了context的状态类型,是一个泛型,参数就是状态的类型,默认初始化为undefined类型,并且定义了一个children的类型,因为Provider的子节点是一个React节点,所以也就定义成ReactNode类型。</p><p>然后就是我们的Model类型,如下:</p><pre><code class="tsx">export interface Model<Value,State = void> {
Provider: ComponentType<ModelProviderProps<State>>
useModel: () => Value
}</code></pre><p>这个也很好理解,因为Model暴露了两个东西,第一个是Provider,第二个就是useContext,只是换了一个名字而已,定义这两个的类型就够了。</p><p>接下来就是我们的核心函数createModel函数的实现,我们一步一步来,首先当然是定义这个函数,注意类型,如下:</p><pre><code class="tsx">export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => {
//核心代码
}</code></pre><p>以上函数难以理解的应该是类型的定义,我们createModel函数传入一个hook函数,hook函数传入一个状态作为参数,然后返回值就是我们定义好的Model泛型,参数为类型就是我们定义好的这个函数的泛型。</p><p>接下来,我们要做的自然是创建一个context,如下:</p><pre><code class="tsx">//创建一个context
const context = createContext<Value | typeof EMPTY>(EMPTY);</code></pre><p>然后我们要创建一个Provider函数,本质上也是一个React组件,如下:</p><pre><code class="tsx">const Provider = (props:ModelProviderProps<State>) => {
// 这里使用ModelProvider主要是不能和定义的函数名起冲突
const { Provider:ModelProvider } = context;
const { initialState,children } = props;
const value = useHook(initialState);
return (
<ModelProvider value={value}>{children}</ModelProvider>
)
}</code></pre><p>这里也很好理解,实际上就是通过父组件拿到初始状态和子节点,从context中拿到Provider组件,然后返回即可,注意我们的value是通过传入的自定义hook函数包装后的值。</p><p>第三步,我们就需要定义一个hook函数拿到这个自定义的Context,如下:</p><pre><code class="tsx">const useModel = ():Value => {
const value = useContext(context);
// 这里确定一下用户是否正确使用Provider
if(value === EMPTY){
//抛出异常,使用者并没有用Provider包裹组件
throw new Error('Component must be wrapped with <Container.Provider>');
}
// 返回context
return value;
}</code></pre><p>这个函数的实现也很好理解,就是获取context,判断context是否正确使用,然后返回。</p><p>最后我们在这个函数内部返回这2个东西,即返回Provider和useModel两个函数。如下:</p><pre><code class="tsx">return { Provider,useModel }</code></pre><p>把以上代码全部合并起来,createModel函数就大功告成啦。</p><p>最后,我们把所有代码合并起来,这个状态管理工具也就完成了。</p><pre><code class="tsx">// 导入类型
import type { ReactNode,ComponentType } from 'react';
import { createContext,useContext } from 'react';
const EMPTY:unique symbol = Symbol();
export interface ModelProviderProps<State = void> {
initialState?: State
children: ReactNode
}
export interface Model<Value,State = void> {
Provider: ComponentType<ModelProviderProps<State>>
useModel: () => Value
}
export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => {
//创建一个context
const context = createContext<Value | typeof EMPTY>(EMPTY);
// 定义Provider函数
const Provider = (props:ModelProviderProps<State>) => {
const { Provider:ModelProvider } = context;
const { initialState,children } = props;
const value = useHook(initialState);
return (
<ModelProvider value={value}>{children}</ModelProvider>
)
}
// 定义useModel函数
const useModel = ():Value => {
const value = useContext(context);
// 这里确定一下用户是否正确使用Provider
if(value === EMPTY){
//抛出异常,使用者并没有用Provider包裹组件
throw new Error('Component must be wrapped with <Container.Provider>');
}
// 返回context
return value;
}
return { Provider,useModel };
}</code></pre><p>更近一步,我们再导出一个useModel函数,如下:</p><pre><code class="tsx">export const useModel = <Value,State = void>(model:Model<Value,State>):Value => {
return model.useModel();
}</code></pre><p>到目前为止,我们的整个状态管理工具就完成啦,使用起来也很简单,很多轻量的共享状态项目当中我们也就再也不需要使用像Redux这样的比较复杂的状态管理工具了。</p><p>当然这个想法也并不是我本人想的,文末已注明来源,本文对源码做了一遍分析。</p><p><a href="https://link.segmentfault.com/?enc=rkIkHR7Kzl%2FvUoOVXh4P9w%3D%3D.fuTCYIAth993mBMRI3x7yLWPZQ88URxpBXpCt15E9HInG%2FOs%2F21rtHnuZ%2Fi6g14bgzMYma23yRtDQ5xQP%2Fl069ccqmHIujIiIug3Ukrogw24e9bK0aZvNSgxfwJ6QZnL" rel="nofollow">源码地址</a>。</p><blockquote>PS: 本文源码来自<a href="https://link.segmentfault.com/?enc=mwv01%2B0KCgBEOTUsGoATkQ%3D%3D.Gc8KJqE4Axk0FlcBLgoX3KT4pVz5s%2BrISbi0VGfPWGHYdDXGjYn6mOgzTEEjyPAaWwUJRMH4%2B5VoVuw5OkrOyGQYb4tUFc3XikBp6JMs%2FYk%3D" rel="nofollow">unstated-next</a>。</blockquote>
50天用vue3完成了50个web项目,我学到了什么?
https://segmentfault.com/a/1190000042509082
2022-09-19T11:40:23+08:00
2022-09-19T11:40:23+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
27
<p>通过本文的50个web示例你将学到:</p><ul><li>Vue3核心基础语法和进阶语法</li><li>less核心基础语法和进阶语法</li><li>scss核心基础语法和进阶语法</li></ul><h3>1.Expanding Cards</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509084" alt="1.png" title="1.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2B42vJx7T%2FQ2SGcBkqsDBVA%3D%3D.sIC1Kf1F9f%2BioR7bFB6r0npfZKStMSMU6wpaxOCZphoNWdta5Z248a1X2%2FN26iMDXhNz%2B4IW33M6mG8PFjmxkwPj8w0MFzGfks5bMdKqV1E%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=B8eYUc8P%2BRC%2BWazYqpAWFw%3D%3D.vYjk1X5jTeM4IY0rYf3cbfdP0b1uAFZJkBap1CUR7siLXrCc2lF8gixNx%2Fh4LdNM6jP2acHOtlr%2B2mwfnXY4%2Fg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><ul><li>Vue ref方法定义基本响应式变量。如:</li></ul><pre><code class="js">const currentIndex = ref(0);</code></pre><p>ref方法当然也可以定义一个对象,但是通常定义对象应该使用reactive方法。</p><ul><li>Vue v-for指令渲染列表。如:</li></ul><pre><code class="js">v-for="(item,index) in imageItems" </code></pre><p>v-for指令渲染时最好指定key属性,方便虚拟DOM Diff算法比对。</p><ul><li>Vue 动态绑定class与style。如:</li></ul><pre><code class="js">:class="index === currentIndex ? 'active' : ''"
:style="{ backgroundImage:`url(${ imageURL + item })`}"</code></pre><p><code>:class</code>和<code>:style</code>代表设置动态类名和动态行内样式,其中<code>:</code>为<code>v-bind</code>的简写,值可以是一个javascript表达式,如三元表达式,或者是对象,又或者是数组。</p><ul><li>Vue 事件绑定。如:</li></ul><pre><code class="js">@click="currentIndex = index"</code></pre><p>vue事件可以简写为@ + 事件名,值为一个javascript表达式或者是一个函数。</p><h4>less</h4><ul><li>定义公共样式</li></ul><pre><code class="less">.base-flex {
display: flex;
justify-content: center;
align-items: center;
}
//使用方式
.app {
.base-flex;
}</code></pre><ul><li>嵌套语法</li></ul><pre><code class="less">.app {
//...这里写核心样式
.ec-panel {
//...这里写核心样式
}
}</code></pre><p>less定义变量是@ + 变量名,如:</p><pre><code class="less">@width: 100px;</code></pre><ul><li>&</li></ul><p>该符号表示对父选择器的一个引用,例如:</p><pre><code class="less">.ec-panel {
//...核心样式
&.active {
//...核心样式
}
}</code></pre><p>其中&.active其实就是.ec-panel.active的写法。</p><h3>2. Progress Steps</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509085" alt="2.png" title="2.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2FhAVfhtYvKaLeqlb4Rkuug%3D%3D.xUMyhRnocmYZrYmg74xn9A9s7eVh37vSsS9%2FGTgedItTEhP2DZdwRnEIa%2BRF8n4Pl5ywx%2F0kEA%2Fv8tAx4ZePcBAjmaMIbeAvlNcv5zhq8AQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=NLL3kAINhRa6bWw2jZJzcg%3D%3D.2SCZDg33AfcZ8NkYevtQrf9UXyBuzs5SlwtUV0X7%2Br2CawiG26pHhucf5Y0sMWARCAYAZr6BDeG1IowdPDr7DA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><p>通过defineProps定义props,如:</p><pre><code class="ts">const props = defineProps({
width:{
type:Number,
default:350
},
progressWidth:{
type:Number,
default:0
}
})</code></pre><p>通过computed方法定义计算属性:</p><pre><code class="ts">const styleWidth = computed(() => props.width+'px');</code></pre><p>依然是通过slot标签定义插槽:</p><pre><code class="html"><div class="ps-step-container">
<div class="ps-step-progress"></div>
<slot></slot>
</div></code></pre><p>通过defineEmits方法定义事件传递:</p><ul><li>defineEmits注册事件名,方法通常是一个数组,传递多个事件名</li><li>调用方法的返回值,然后将该事件向上传递</li></ul><pre><code class="ts">const emit = defineEmits(["on-click"]);
const changeActive = () => {
emit("on-click");
}</code></pre><p>通过reactive定义响应式对象,它与ref方法的区别就是,通常我们使用ref方法来定义基本数据类型,而reactive方法则是定义多个对象:</p><pre><code class="ts">const bool = ref(false);
const state = reactive({
status: false,
list:[
{
label:"测试值",
value:1
}
]
})</code></pre><p>浏览器控制台花式玩法:</p><pre><code class="js">console.log("%c " + consoleText,"background:#0ca6dc;padding:4px 10px;border-radius:3px;color:#fff");</code></pre><h4>less</h4><p>动态绑定样式变量的写法有两种,第一种则是字符串属性名,第二种则是直接写变量名:</p><pre><code class="less">width:v-bind("styleWidth");
width:v-bind(styleProgressWidth);</code></pre><p>::focus-visible选择器,表示键盘伪类焦点选择器,在规范中定义为:元素聚焦,同时浏览器认为聚焦轮廓应该显示。</p><p>有时候我们希望键盘触发聚焦轮廓,而鼠标关注焦点则不触发轮廓,此时用以下一行CSS代码搞定。</p><pre><code class="css">:focus:not(:focus-visible) {
outline: 0;
}</code></pre><p>属性选择器语法:</p><pre><code class="css">[属性选择器] {
//CSS样式
}
//如:
[disabled] {
background-color: @color;
color: @font_color;
cursor: not-allowed;
}</code></pre><p>其它知识点同前面示例。</p><h3>3. Rotating Navigation Animation</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509086" alt="3.png" title="3.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=Pne5KbkEZx4%2B%2BBKuMNi66A%3D%3D.LpPqNd4G940SNQijNa%2FSFIMTVlKKmcj8F7PR5IVGZDhQRWQCrLFwvwOtS1VcQA0PdlPF1t1nFLOUQ29bxawA5mayBPsKM8dLXfi1OQCol1s%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Bg5V1UOeJ1MzgaeQJTl8IA%3D%3D.rXqwTSfgQUoPhqxXwoviFzaTRxxWN%2FwDhXifE%2FszA5IrkeqKqeURIUCoGt30mFojgqO5an%2FDr7WhcpBsveQu6w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><p>v-if与v-else指令,表示条件,v-html表示渲染html:</p><pre><code class="html"><div class="rna-page-content">
<slot>
<template v-if="props.isRenderHTML">
<p v-html="content"></p>
</template>
<template v-else>{{ props.content }}</template>
</slot>
</div></code></pre><h4>typescript</h4><p>typescript 定义接口通过interface关键字,就像定义对象一样,其后跟属性名:属性类型,如:</p><p>定义基本类型就是:类型值,如:</p><pre><code class="ts">const URL: string = "";
//代表URL是一个字符串类型</code></pre><pre><code class="ts">interface NavItemType {
url:string;
text:string;
icon:string;
}</code></pre><p>typescript泛型,如:</p><pre><code class="ts">Array<NavItemType></code></pre><p>就代表是一个泛型,也就是数组类型,并且数组项的值应该是类型NavItemType,所以数据格式就应该类似如下:</p><pre><code class="ts">export const navList = [
{
url: "http://www.eveningwater.com/",
text: "个人网页",
icon:"Website"
},
{
url: "https://www.eveningwater.com/my-web-projects/",
text: "我的项目",
icon:"project"
},
{
url: "http://github.com/eveningwater",
text: "github",
icon:"github"
},
];</code></pre><p>再如:</p><pre><code class="ts">Array<string> //表示定义一个字符串数组</code></pre><p>导出多个模块语法:</p><pre><code class="ts">export { default as Content } from "./Content.vue";
export { default as Menu } from "./LeftMenu.vue";
export { default as NavList } from "./NavList.vue";</code></pre><p>表示将三个组件的默认模块更名为对应的名字。</p><h4>less</h4><p>:deep深度选择器,类似于vue2的>>>和/deep/,通常用于影响子组件的样式,也就是因为加了scoped组件只作用在当前组件,而无法作用到子组件,也就需要使用该选择器:</p><pre><code class="less">:deep(p) {
text-indent: 2em;
color:@content_font_color;
line-height: 2;
margin-bottom: 15px;
letter-spacing: 1px;
}</code></pre><p>动态样式访问数组变量,可以根据索引来访问,就像访问数组元素一样:</p><pre><code class="less">background-image: v-bind("beforeResourceURL[0]");</code></pre><p>css同级元素选择器,如:</p><pre><code class="less">& + .rna-nav-list ul {
transform: translateX(15px);
& .rna-nav-item {
transform: translateX(0);
transition-delay: .4s;
}
}</code></pre><p>表示选中当前选择器的兄弟选择器.rna-nav-list。</p><p>其它知识点同前面示例。</p><h3>4. hidden-search-widget</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509087" alt="4.png" title="4.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=F6EYfhRoU8GpOz14FREZOg%3D%3D.XBdCswT8nL45gQb6sTcDosDsoqCvTdcE9hsKSVk5O0cwkPiIWEMrNpUpLEfzq9Um%2B68xuKuf8OlZF7iY86AQCBPU2WPAQ3%2FFQEBXXlu22Ik%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=nMaRfkHFhsngT3Ung0zrUA%3D%3D.mPMtZfu9XwJje%2BvjIcxuiQOk1wI%2FB%2BH3%2B7fIvE0Gknzq6PTnlB3GtFhoAzmVsgDSN%2FBKhViIvwROzzC%2B0hxuog%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><p>给元素或者是组件绑定ref,可以访问组件或者元素的实际DOM元素,例如:</p><pre><code class="ts">const inputRef = ref(null);</code></pre><p>watch方法,接受三个参数,第一个参数表示监听的数据,可以是一个函数,也可以是一个变量,或者也可以是一个监听的字符串属性名,第二个参数则是一个回调函数,可以获取到监听的值,第三个参数表示监听的配置对象,如深度监听deep属性。如:</p><pre><code class="ts">watch(isActive,val => {
const inputElement = inputRef.value;
if(val && inputElement){
(inputElement as HTMLInputElement).focus();
}
});</code></pre><h4>typescript</h4><p>as 关键字可以将任意变量断言成任意类型,通常如果转换类型不对可以先强制转换成unknown,然后再转换成对应的类型。例如:</p><pre><code class="ts">//假设inputElement不能被断言成HTMLInputElement类型,就需要先转换成unknown
inputElement as unknown as HTMLInputElement</code></pre><p>或者也可以直接断言成any。如:</p><pre><code class="ts">inputElement as any </code></pre><p>虽然这样做可以逃避ts的类型检查,但如无必要,尽量少使用as关键字断言变量的类型。</p><p>模板绑定:</p><pre><code class="html"><input type="text" class="hsw-input" ref="inputRef" :placeholder="props.placeHolder" /></code></pre><p>其它知识点同前面示例。</p><h3>5. blurry loading</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509088" alt="5.png" title="5.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=XXKV9PusLmkOuw9XiP%2BHTA%3D%3D.%2Bda6B7P7ECJLSN8WMvTRF5EU95%2FRcOLfIPnEdPbsjlIneJgV3zRfVr2B23%2FmkTpob4cF%2BNzqCC8pWCKqEMygMb6t7ya7uRnjGny%2FCVzwSzE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=FgQu8v36r8XaOOsrIj%2Bcpw%3D%3D.0KFcvvgO3YjqrixGlau33WQ6Oh6oCFNPZNzvQhu83BjqbjZyNYJGGQ66kNGIAd9TeO87BFiBdkkjyWyAZhgw9Q%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><p>一个工具函数,表示转换数字范围的工具函数,感觉很常用,可以记下来:</p><pre><code class="ts">// https://stackoverflow.com/questions/10756313/javascript-jquery-map-a-range-of-numbers-to-another-range-of-numbers
export const scale = (n:number,inMin:number,inMax:number,outerMin:number,outerMax:number) => (n - inMin) * (outerMax - outerMin) / (inMax - inMin) + outerMin;</code></pre><h4>typescript</h4><p>Ref类型也是一个泛型,当然这里其实应该如此来定义setTimeout的返回值,而不应该使用any:</p><pre><code class="ts">const timer:Ref<any> = ref(null);</code></pre><p>应该修改成:</p><pre><code class="ts">const timer:Ref<ReturnType<typeof setTimeout>> = ref(null);</code></pre><p>typeof setTimeout表示获取setTimeout函数的类型,而ReturnType是typescript的内置类型,表示返回值的类型,结合起来就代表是setTimeout函数的返回值。</p><p>onMounted和onUnmounted是vue生命周期的钩子函数,分别代表组件挂载后和组件卸载后,接受一个回调函数作为参数,然后在回调函数当中执行相应的操作。</p><pre><code class="ts">onMounted(() => {
//这里写逻辑
});
onUnmounted(() => {
//这里写逻辑
})</code></pre><h4>less</h4><p>获取对象值的变量作为样式,可以使用字符串和点语法获取,如:</p><pre><code class="less">opacity: v-bind("props.number");</code></pre><p>其它知识点同前面示例。</p><h3>6. scroll animation</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509089" alt="6.png" title="6.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=I%2Fkesv29zMaNsvku3JYO0g%3D%3D.mFrB9Qu%2BnGHHLC3ijzxz9i%2FTVs5Ir51XwmXqg0XX5M5GY7dNpzwyDkZr%2FkSkQ8ZXLXcND5aWgHP0ZKEQwYNcIquAxtU3GlXeyCPpdmr6OOE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ftvj%2BLxeRnMySN6F90kU%2Bw%3D%3D.Zc64cBLcp0KKF8eAiZWwhW%2FdVJRjEc3KRxnJ4LQIoiSRPDDEk%2BqhCc7AJYmaEphi3XMSe32yLyxxu5xRrNdMKQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>函数节流,就是利用延迟定时器,结合时间戳来调用给定的函数。在这个函数里面也涉及到了typescript泛型,任意类型,函数类型,并且也涉及到了apply方法的使用,apply传入一个this对象,后续的参数则是一个数组。</p><pre><code class="ts">export function throttle(fn:(...args:any []) => any,delay:number){
let timer:ReturnType<typeof setTimeout> | null = null,last:number = 0;
return function<T>(this:(...args:any []) => any,...args:Array<T>) {
let now = +new Date();
if(last && now < last + delay){
clearTimeout(timer);
timer = setTimeout(() => {
last = now;
fn.apply(this,args);
},delay);
}else{
last = now;
fn.apply(this,args);
}
}
}</code></pre><h4>typescript</h4><p>PropType类型,是一个泛型,用于定义prop的类型。如:</p><pre><code class="ts">props:{
title:{
type:String as PropType<string>
},
content:{
type:String as PropType<string>
}
},</code></pre><p>setup函数为vue3的入口,也是一个生命周期钩子函数,相当于vue2的beforeCreate和created的合并。第一个参数可以获取到props,第二个参数是当前上下文,可以通过对象结构的方式获取emit,slots等。如:</p><pre><code class="ts">setup(props,{ slots }){
const renderTitle = () => slots.title ? slots.title() : props.title;
const renderBoxContent = () => slots.default ? slots.default() : props.content;
return () => (
<div class="scroll-ani-box">
<Title level="3" class="scroll-ani-box-title">{ renderTitle() }</Title>
{ renderBoxContent() }
</div>
)
}</code></pre><p>defineComponent表示定义一个组件,也是Vue3提供的一个定义组件的方法,该方法接收一个对象作为参数,在这个对象里面有点像vue2的new Vue的参数,写vue组件的配置参数,例如props,methods等。</p><p>在这里封装了一个Title.tsx,也就是将h1 ~ h6封装成一个组件,也叫标题组件。代码如下:</p><pre><code class="tsx">import { defineComponent, PropType } from "@vue/runtime-core";
export const TitleNumberCollection = [1,2,3,4,5,6];
export default defineComponent({
props:{
level:{
type:[Number,String] as PropType<number|string>,
default:1,
//在这里检查level的类型
validator:(value:string | number) => {
return TitleNumberCollection.indexOf(Number(value)) > -1;
}
},
content:{
type:String as PropType<string>
}
},
setup(props,{ slots }){
const { level,content,...rest } = props;
const TitleName = "h" + level;
const renderChildren = () => slots.default ? slots.default() : props.content;
return () => (
<TitleName {...rest}>
{ renderChildren() }
</TitleName>
)
}
})</code></pre><p>这个组件的实现思路就是定义一个level属性和一个content属性,前者代表使用的是h1 ~ h6之中的元素,这也是为什么TitleNumberCollection数组是[1,2,3,4,5,6]的原因。</p><p>这里为了严谨,对level做了验证,只允许传入1 ~ 6的值,也可以是字符串和数字。需要注意的就是validator是一个函数,返回的是一个布尔值,并且函数的参数必须要指定类型,否则会出现意料之外的错误,导致程序无法执行。</p><p>如果组件没有写默认的插槽内容,就会使用content属性字符串作为承载的内容。使用这个组件的方式很简单,如下:</p><pre><code class="html"><Title level="3">这是h3元素的内容</Title>
<Title :level="4">这是h4元素的内容</Title></code></pre><blockquote>PS: 这个组件在后面的示例中几乎都有用到。</blockquote><p>可以在onMounted钩子函数中监听页面滚动事件,如:</p><pre><code class="ts">onMounted(() => {
triggerBottom.value = window.innerHeight * 4 / 5;
window.addEventListener("scroll",throttle(onScrollHandler,20))
});</code></pre><p>UnwrapNestedRefs类型也是Vue3定义的类型,是一个泛型,可以定义reactive方法的返回值类型,如:</p><pre><code class="ts">interface BoxType {
active:string;
content:string;
}
const state:UnwrapNestedRefs<{ boxList:Array<BoxType>}> = reactive({
boxList:[]
});</code></pre><p>v-slot指令,其后跟指定的插槽名,也就是具名插槽。</p><h4>less</h4><p>定义Mixin实际上就是写CSS样式。如下:</p><pre><code class="less">.flex-center {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}</code></pre><p>使用方式就是把它当成函数调用,如:</p><pre><code class="less">//使用方式
.app {
.flex-center();
}</code></pre><p><a href="https://link.segmentfault.com/?enc=BaiGFFKvX8L6JVZBqZNeDw%3D%3D.PkBm2r1%2BXvYqGb7pk1jNmeBUl3b7vRSa4a7MWxuTjWm%2Fc%2FXDQSWKvVl%2F2qriBodZO4cXdW4vhpyVbu4JUKtXOw%3D%3D" rel="nofollow">svg-gradient函数</a>,是less的核心函数,生成一个svg渐变色,它至少有三个参数,第一个参数指定渐变类型和方向,其余参数列出颜色和位置,第一个和最后一个指定颜色的位置是可选的,其余颜色必须指定位置。该函数最终会被渲染成svg图片,如下图所示:</p><p><img src="/img/remote/1460000042509090" alt="" title=""></p><p><a href="https://link.segmentfault.com/?enc=kXPmP6M6t6MLlUKE0dRb8w%3D%3D.cQ%2FKZMCHT9XSrfaxk46AZhWmCNLObaEvaXPhYr0JTZUeSRFaIt%2BV82Zs%2Bn%2FGXo%2FcVoCNORrbaBArshFIP2ao6A%3D%3D" rel="nofollow">percentage函数</a>,接受一个浮点值作为参数,如:</p><pre><code class="less">percentage(.5); //渲染成50%</code></pre><p>less当中的map对象,格式如下:</p><pre><code class="less">.(map名字) {
属性名: 属性值(可以是变量);
}</code></pre><p>例如:</p><pre><code class="less">@boxBgColor-1:#f1bd81;
@boxBgColor-2:#e07e0e;
.boxColors() {
lightColor:@boxBgColor-1;
darkColor:@boxBgColor-2;
}</code></pre><p>使用的时候,就像读取javascript对象那样使用中括号语法:</p><pre><code class="less">.test {
color:.boxColors[lightColor];
}</code></pre><p>min函数,顾名思义,求最小值,如:</p><pre><code class="less">min(8px,9px,10px) // => 返回8px</code></pre><p>range函数,求范围值,如:</p><pre><code class="less">range(10px, 30px, 10); // => 10px 20px 30px
padding:range(10px, 30px, 10); // => padding: 10px 20px 30px;</code></pre><p>fade函数,对颜色进行透明度的相加,例如:</p><pre><code class="less">fade(#fff,40%); // => rgba(255,255,255,.4);</code></pre><p>类似的还有fadeout以及fadein函数。</p><p>extract函数,有2个参数,第一个参数是一个列表,第二个参数是一个索引值,也就是说该函数表示从列表当中取出列表项,如:</p><pre><code class="less">@textTransform:uppercase,capitalize,none,lowercase,inherit;
text-transform: extract(@textTransform,1);</code></pre><p>escape表示可以将任意的字符串转成属性或者值,格式就是~ + 字符串值,例如:</p><pre><code class="less">@min768: ~"(min-width: 768px)";
.element {
@media @min768 {
font-size: 1.2rem;
}
}</code></pre><p>将会渲染成:</p><pre><code class="less">@media (min-width: 768px) {
.element {
font-size: 1.2rem;
}
}</code></pre><p>这个示例用到了很多less核心语法,对less的用法也就更深一步。</p><p>其它知识点同前面示例。</p><h3>7. split panel</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509091" alt="7.png" title="7.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=agsYNCn5mdmjnJ34UqFx1Q%3D%3D.4Oik4P93vtTijF8NKIA5v3jrGVFVJYnd%2BB5f7PWG3lM2XBnZ9VZ2jL50Ne%2BD7CuCNx7oA21u0sjq4O4ZjV%2BTRccfOx2WR1Byt94s3wC26VU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=oyd27df%2F9ewi2CGx%2FCy9ug%3D%3D.x2T1m8XRdDZ1W91yxhQOGdFm4vpxcUZuquBWCDwAkl1HO5ZWQF138B%2B5qC8tEukk2v7zow2aCLpzZ1I3Pwt0QA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>知识点同前面示例。</p><h4>less</h4><p>unit函数,也就是添加单位的函数,如:</p><pre><code class="less">unit(100,vh) // =>100vh</code></pre><p>covert函数,转换单位,如:</p><pre><code class="less">covert(1s,'ms'); // => 1000ms</code></pre><p>其它知识点同前面示例。</p><h3>8. form wave</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509092" alt="8.png" title="8.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=e4O%2BHpEuqutqZpGdQBmJ2Q%3D%3D.H3SwOByZsR%2FQTlsUDdn5hi4w4Fhs07sLsvVv%2FPb0Q5deoYsBmYxB2%2Byb03w0Cjg67y%2BgjWMzN0pOlPz3jC%2BKpV28rY6WGx6KJrj0CDCxWuk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=jW%2BBM%2FAHFtUaCrPqHKNJKA%3D%3D.9rkIyu39YNm8oKqD9H6M0xKxZE%2FYDcMPWnR0xlTx0SnvVh2MhvaIfZpUlA%2BIdlIVNwxEuy04d5tnyFd5KlU%2BYQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>defineAsyncComponent函数,传入一个回调函数作为参数,回调函数的返回值必须是import('组件路径'),表示异步导入组件。如:</p><pre><code class="vue">const AsyncFormComponent = defineAsyncComponent(() => {
return import("./components/Form.vue");
});</code></pre><h4>less</h4><p>pi函数,表示获取数学上的π值。即3.14.....</p><ul><li>ceil函数(四舍五入),</li><li>round函数(向上取整),</li><li>sqrt函数(求绝对值),</li><li>floor函数(向下取整),</li><li>length函数(获取列表的长度),</li><li>mod函数(求余)。</li></ul><p>其它知识点同前面示例。</p><h3>9. sound board</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509093" alt="9.png" title="9.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=bfQb8Xh8E63gb%2BKeq5SwCQ%3D%3D.aOdp4z1qgAjFO%2BhNzhyZjO%2BHPdavxN%2FExdez7WePDQAnhnabmMeUJ6PQgd%2F9rVSIy2F3oSXeGC%2FV5aIIJ%2FIkrV8TQq5MVkao0F5WitrftZM%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=4l1f4%2F6v6uuxRceTU0sglw%3D%3D.7wqpQLAKckruEmrTzgz%2FyKy2SMg7BbA7i8Lv3KUFUFaP70GQ1wXqHE6pqOCPzWbzy5JeYUrFfvRt5uRT%2BY0Kvw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>ref函数不仅可以用来定义数据,也可以用来存储一个dom元素的引用。如:</p><pre><code class="ts">const audioRef = ref<HTMLAudioHTMLElement | null>(null);</code></pre><p>vue template模板代码如下:</p><pre><code class="vue"><audio ref={audioRef}></audio></code></pre><h4>less</h4><p>css选择器前缀名,可以通过定义变量,然后.@{变量名}的方式来添加css选择器前缀。如:</p><pre><code class="less">@prefix:sb-;
.@{prefix}button-container {
//样式
}
// => 渲染成
// .sb-button-container {
// //样式
// }</code></pre><p>sin函数,求正弦的函数。</p><p>其它知识点同前面示例。</p><h3>10. dad jokes</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509094" alt="10.png" title="10.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=ThqTbTU8UvI9yhNHktF28g%3D%3D.kWOFNIofbe0UJjrFKGbGPTCj%2BfccwLQXIOdzlQeV5VP24uuTlDvxbVILBN%2BoT8eceRoDtcXfPnV%2BAO8cgnA7AM1eu6SXsFBxCoBzzfDZDcw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=RYlGkMlvltTG%2F4GFPNJCAg%3D%3D.wMEu0X0dxgTH31OCtjx8CtjubEqbsRrYbxSO6rPbE%2FUXOaGT%2BMl99sYLXwTA%2BXfMZEyB2bUGJWSJIM71jX4Aeg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>Javascript</h4><p>Fragment元素,也就是一个占位标签,实际不会渲染该元素,类似于vue的template元素。</p><p>window.open函数打开一个窗口,在打开之后,通过设置opener = null,可以避免window.open带来的安全问题。</p><p>如:</p><pre><code class="js">const win = window.open("");
win && (win.opener = null);</code></pre><h4>less</h4><p>color函数,用于将属性转换成颜色值字符串,如:</p><pre><code class="less">color(#eb6b44); // => '#eb6b44';</code></pre><p>max函数,与min函数相反,取最大值,如:</p><pre><code class="less">max(600,700,800) // => 800</code></pre><p>pow函数,幂函数,等同于javascript的Math.pow函数,传入2个参数,第一个参数为数,第二个参数为幂数。如:</p><pre><code class="less">pow(2,2); // => 4</code></pre><p>:focus-visible选择器,这个是关注焦点选择器。</p><p>其它知识点同前面示例。</p><h3>11. Event keyCodes</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509095" alt="11.png" title="11.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=R2L5kNvwlz0Rigc%2Bqc%2Fm6g%3D%3D.Wjels0RNqABLFKsDEhmniKBR6hAJJavZWLwh32NBmb6eOELpXMzgnHnqjYf3PC5LYB02B6qblhCK7M75vHhR0T7PANqCUnYYD7E0TPaSvKc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=VSfbO1b4xgq1Unrajry%2Bvw%3D%3D.UZDQuBz1o9NxlFTGjRXXg%2FNxjWF0JPAjLG%2BBEZxXiaVeYHhVDp%2Fnt6sTSUF1zcdScu1fUAhgcmWojF%2Bxurghkw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>useCssModule方法,顾名思义,就是样式模块化,我们可以给vue的单文件组件的style标签加上module属性,然后在setup函数当中就可以通过该方法来访问style。例如:</p><pre><code class="vue"><style module lang="less">
.test {
//核心样式
}
</style>
<script>
//setup函数中
const style = useCssModule();
</script>
<template>
<div :class="style.test"></div>
</template></code></pre><p>该方法还可以接一个具名参数,也就是代表获取的style模块名,比如:</p><pre><code class="vue"><style module="name" lang="less">
.test {
//核心样式
}
</style>
<script>
//setup函数中
const style = useCssModule('name');
</script>
<template>
<div :class="style.test"></div>
</template></code></pre><h4>typescript</h4><p>KeyboardEvent类型,也就是键盘事件对象的类型。</p><h4>less</h4><p>less定义mixin(混合)与sass还是有很大的区别的,比如定义一个普通的mixin就是写一个css选择器,然后再写样式,如:</p><pre><code class="less">.test {
//样式代码
}</code></pre><p>然后我们通过调用函数的方式来使用这个mixin,即:</p><pre><code class="less">.common {
.test();
}</code></pre><p>如果不希望mixin被输出编译在css当中,可以给mixin加入括号,即:</p><pre><code class="less">.test() {
//核心样式
}</code></pre><p>使用方式仍然是通过调用函数的方式,同上。</p><p>mixin同样有命名空间的概念,如:</p><pre><code class="less">#test(){
.test {
//这里写样式
}
}</code></pre><p>这里的#test实际上也就是命名空间,当然我们也可以使用.test来代替#test,也是可以代表是一个命名空间名称的。</p><p>命名空间也是受保护的,我们可以使用when关键字,结合判断条件来让这个命名空间受保护,如:</p><pre><code class="less">.mt-(@num) when (default()) {
margin-top:unit(@num,px);
}</code></pre><p>意思就是当满足默认条件的时候,我们的.mt类名,其后跟数字前缀就会渲染出margin-top多少px的样式。例如:</p><pre><code class="less">.mt-(10); // => 渲染成margin-top:10px;</code></pre><p>其它知识点同前面示例。</p><h3>12. faq-collapse</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509096" alt="12.png" title="12.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=gxzN8uvNfoPHeV4UwhR4bg%3D%3D.h42Hbz3mnKNcsHQP%2FAUKMYVRBgNGWKFOp61JYxy079bSvea35Dmy48Bz2OKmr2DkW82vqWGPchRRXUq0RinpnJFLJAGiyWKwCk3x0DOi2zI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=zMqIfBdOhJFF%2BccIa7oUUA%3D%3D.ruO4f8Yjs9pkPSe66kfLjEK2JuMlCLQJ4U%2FL1RYfU0rYKFEfowtxCh%2BixE78LkMEN4lFdxXVeoc%2B1CUWgaUETQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>readonly方法,可以让一个通过ref或者是reactive定义的响应式数据变成只读。如:</p><pre><code class="ts">const state = readonly(reactive({
faqList: [
{
title:"Why shouldn't we trust atoms?",
text:"They make up everything."
},
{
title:"What do you call someone with no body and no nose?",
text:"Nobody knows."
},
{
title:"What's the object-oriented way to become wealthy?",
text:"Inheritance."
},
{
title:"How many tickles does it take to tickle an octopus?",
text:"Ten-tickles!"
},
{
title:"What is: 1 + 1?",
text:"Depends on who are you asking."
}
]
}));</code></pre><p>此时的faqList就是只读的,无法被修改。</p><h4>less</h4><p>luma函数,计算一个颜色对象的亮度。如:</p><pre><code class="less">luma(rgb(100, 200, 30)) // => 44%</code></pre><p>其它知识点同前面示例。</p><h3>13. random-choice-picker</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509097" alt="13.png" title="13.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=9K931w7TfqK9Uw3MUfnZNQ%3D%3D.Npr%2FGsPCQcJLjg5UMc6oCAEvLomvaAXwr%2FStuEeeFA4HeJ5soqIg7hGjkqmYIwWNfsUmqqHKqXvMUjU2jzv8RC7pxMZNFEq5g31Y8QLiVKw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=8v9X0rGIYhOPxd6hTVnrNw%3D%3D.uPjjB%2FhVh38LIPqLExrMYD94bYql85ugjvBqI51oRCc1lsYkiePGygoxGRlieVLRpd9PGkHltJHwpcbX%2BdVy7w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>通过withDefaults可以将defineProps包裹住,方便定义类型,并且提供props的默认值,如:</p><pre><code class="ts">interface PropsType {
placeholder:string;
}
const props = withDefaults(defineProps<PropsType>(),{
placeholder:""
});</code></pre><p>计算属性的getter函数与setter函数。如:</p><pre><code class="ts">type ListType = Array<{text?:string,isActive?:boolean}>;
const computedChoices = computed<ListType>({
get(){
return choices.value;
},
set(newValue){
if(newValue !== choices.value){
choices.value = newValue;
}
}
});</code></pre><p>setTimeout延迟函数的使用。</p><p>然后就是这个函数稍微难以理解一些:</p><pre><code class="ts">const changeChoices = (v:string) => v.split(choice.value.indexOf(",") > -1 ? "," : "").filter(v => v.trim()).map(v => ({ text:v,isActive:false }));</code></pre><p>实际上也很好理解,将每个函数拆分一下,就能理解了。</p><p>其它知识点同前面示例。</p><h3>14. Animated Navigation</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509098" alt="14.png" title="14.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=XCZ92929P1hRYj6d3yPllw%3D%3D.5L5mY4ig7cXELRPc3Lg8sdeFQLJANnVPz9JLu9aajWK724tHmHLeoHFB4QC6FVLMHIYYrH7bpZly3d%2BOMsCAPOJxu9Fch0TpT6018I5MMrs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=lKGtMnSQ3ALmi6OzVRCwkw%3D%3D.OkNMcMFnKRerHhyq11AHqgjkaWlgNgQ5C6KL58nkINbDKf1NGcB%2FxtwsOmttCova9Fl8iwbMqIosvca6clpGig%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>知识点同前面示例。</p><h4>scss</h4><p>scss定义变量是$ + 变量名,而less则是@ + 变量名,scss是sass的下一版本,sass的语法有些类似于stylus,去掉了括号,但是在scss当中却保留了括号。scss中定义mixin通过@mixin。如:</p><pre><code class="scss">@mixin common-bg {
background: linear-gradient(135deg,$bgColor-1 10%,$bgColor-2 90%);
}</code></pre><p>使用mixin就是通过@include关键字加mixin的名字。如:</p><pre><code class="scss">.test {
@include common-bg;
}</code></pre><p>同样的scss中在选择器中使用前缀应该是#{变量名}的方式。如:</p><pre><code class="scss">$baseSelector:an-;
.#{$baseSelector}nav {
//核心样式
}</code></pre><p>scss中可以使用for循环,结构为:for 变量名 from 起始值 through 结束值。如:</p><pre><code class="scss">@for $i from 0 through 100 {
.mt-#{$i} {
margin-top:$i + px;
}
}</code></pre><p>opacify函数,传入2个参数,第一个参数为颜色值,第二个参数为一个浮点数,表示相加的透明度,这个函数也就是为颜色值添加透明度。如:</p><pre><code class="scss">opacify(rgba(#036, 0.7), 0.3) // => #036
opacify(rgba(#6b717f, 0.5), 0.2); // rgba(107, 113, 127, 0.7)</code></pre><p>transparentize函数,传入两个参数,同opacify函数一致,作用同opacify函数相反。如:</p><pre><code class="scss">transparentize(rgba(#6b717f, 0.5), 0.2) // rgba(107, 113, 127, 0.3)</code></pre><blockquote>opacify与transparentize函数就类似于类似于less的fadein和fadeout函数</blockquote><p>其它知识点同前面示例。</p><h3>15. incrementing-counter</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509099" alt="15.png" title="15.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=uVWEUWmFGaLCJGvojeylhQ%3D%3D.O8owl2i4pnwP4en5%2F8jgUcPuio2YuIUA9W16NBGN1TxJeRhWTeNhwCj4Som168PDSbBY30UEkL9P3RkVEyjt%2FhaNVLtjU4D5WRSiDa61W2s%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=FD26pa2%2B%2BYgQcn2v0CcOtw%3D%3D.XZGBtXt74ZITzLkbNnfIAMhIjDf5JUd0S8jh5NcmvIYrNQYqyyl7t%2FQeNsmwHNqigwIau8mMu0XGd27rxw4y0w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>toRefs函数,当我们在vue中使用解构获取props时,props会失去响应式,此时我们就需要使用toRefs将props包裹一下,让props不失去响应式,这在<a href="https://link.segmentfault.com/?enc=GI%2Fk2tu5wxgHcQMqZpzrkQ%3D%3D.NESkvwJGNsPMe5fw%2FuNoBLl8REQrPV0mufGdMuUbVdigP5%2B81o5PwZQoaK3BNtDZ1mbE1TUWra6mmGHBEmtjlV1ID3neHvFTZUmqkPxv%2FlET9QTu3R8yxUMrvCGS8tFs" rel="nofollow">Counter.vue</a>文件当中是注释了的。</p><h4>scss</h4><p>知识点前面示例讲过。</p><p>其它知识点同前面示例。</p><h3>16. Drink Water</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509100" alt="16.png" title="16.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=Z1ERuGi%2F1O8tDUADph8WMQ%3D%3D.ochTDNSAdDYUIa8z2pA7PgukKtzBZHWigQ1noQCnALkjxn7dVuybzRMRkUzmFNLr9ZyM1hVVWzeO9vegnXY1K5KOpXT%2Fe0nhXMrgVIJxU1Q%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=2eIr55Yu%2F%2BO1c1Fpvu1U%2Fg%3D%3D.Yspva3VSOLql6hVdB3TZXMA%2F8jI2YAXy%2B5aI%2BWXg9cXV%2FUItYsHWMG5c6b4%2BV5S6Tha508dTodB%2BgW0MV%2Buv9Q%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>javascript没什么好说的,倒是typescript有2个类型需要解释一下。</p><p>StyleValue类型,顾名思义,就是样式值的类型,是typescript内置类型。</p><p>keyof关键字,后面跟一个类型,表示一个类型属于后面类型的一个子类型。如:</p><pre><code class="ts">interface DataType {
name: string;
age: number;
}
const a: keyof DataType = 1;</code></pre><h4>scss</h4><p>继承的语法,首先写一个公共的样式,然后通过@extend关键字使用,并且可以写多个继承,通过<code>,</code>分隔。如:</p><pre><code class="scss">.test-1 {
//核心样式
}
.test-2 {
//核心样式
}
.common {
@extend .test-1,.test-2;
}</code></pre><p>其它知识点同前面示例。</p><h3>17. Movie App</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509101" alt="17.png" title="17.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=SX5oE%2FpPGWYKE9aeR9rjdg%3D%3D.RgLmXdBNeuTB%2B3YX2hdpsfb0kPDZMRlZ1vhMPTFfbzhrpZe%2FXJv2FPR28LGHFOim6EAGAYw%2Fxtw7KECm2cqr%2BceOYZ2y8fNfC5uj%2FqXD15A%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=im9L62gZLUdXx69HESZ6RQ%3D%3D.sO4STy0TGKxdRhzc%2BFnX6Pk1Vj82is1CTlNo3irlEKa7QENUWvQFOvK2DNAUGQqQRDcklR8sO599ow0dnjp2Aw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>javascript也没有什么新知识点,主要是typescript新增了3个类型,FocusEvent,HTMLInputElement和Partial类型</p><p>FocusEvent就是关注焦点事件对象类型,HTMLInputElement为input元素的类型,Partial为typescript内置类型,将所有类型变成可选的,也是一个泛型。</p><h4>scss</h4><p>percentage函数,原理同less的percentage没什么好说的。</p><p>mixin同样也可以传参数,并且在@include的时候传入即可。</p><p>类似border这样的,还可以写成嵌套语法。如:</p><pre><code class="scss">border: {
width:1px;
style:solid;
color:ffefefe;
}
// => 渲染border-width: 1px;border-style:solid;border-color:#fefefe;</code></pre><p>其它知识点同前面示例。</p><h3>18. background slider</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509102" alt="18.png" title="18.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=5A9TMQz1ePx0dGrKnY5jYw%3D%3D.w5iVAPhZYYv7wT%2BNIzXzngHa97J4si4hpXrB01ZvsSLWufSbZl1qpQ91Ymc5OF3ZZ%2FtCqB9kpap5LuCwHooqwUopRpns%2FLuUD4mIVg0F3N0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=iMzsGblgXvkjzbzsl9aZFg%3D%3D.vVYHK2TP%2F6Hr3aQOYfm%2FWFTd7YteXgBm5NcRy4u85STgl%2FKVvkjTSCxAwED6W2hqUjfoJ8ozMN22mpLHdQA1WA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>nextTick函数,传入一个回调函数作为参数,该函数为等待下一次 DOM 更新刷新的工具方法,返回值是一个promise。如示例:</p><pre><code class="vue"><script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template></code></pre><h4>scss</h4><p>clamp函数,传入3个参数,返回第二个参数的值,第一个参数为最小值,第3个参数为最大值。语法:</p><pre><code class="scss">clamp($min,$number,$max) // => $number</code></pre><p>三个参数都必须有单位或者无单位,如果$number小于$min的值,则返回$min,如果$number大于$max的值,则返回$max。</p><p>map-get函数,获取map类型的值,来看一个示例:</p><pre><code class="scss">$font-weights: ("regular": 400, "medium": 500, "bold": 700);
map-get($font-weights, "medium"); // 500
map-get($font-weights, "extra-bold"); // null</code></pre><p>其它知识点同前面示例。</p><h3>19. theme clock</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509103" alt="19.png" title="19.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=uapDK%2FlKLBWbuBfDrv%2B%2BmA%3D%3D.yaJ7N9WRHdpfd9T7gKSr0g0ckcuFCEnjeA9Y9P4ZUWnR%2FXAHmbE1lIP2MncQfjnZKLr35ApS5zejksfPsFKxOv4YNHw9P9zvJrAIORI%2B4Iw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=MO%2FoGn%2FKDJQqNYYaQvjq5A%3D%3D.Z1f8DICYx8P2dJjDpxKlz8fXYB6vGn3QQ8Z8oh2lGVaX68w1g8MW3eB8tu0cnS2dFOzSZ%2FQQQpAJSYS4yxyFAA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>知识点同前面示例。</p><p>可选链操作符?的使用。</p><h4>typescript</h4><p>Readonly类型,表示数据类型只读。</p><p>CSSProperties类型,就是css属性名的类型。</p><h4>scss</h4><p>@if...@else...判断语句。@function定义函数,@return 返回函数值。如:</p><pre><code class="scss">@function setMargin($i,$j,$unit){
$result:0;
@for $_ from $i through $j {
@if $unit {
$result:$_ + $unit;
} @else {
$result:$_;
}
}
@return $result;
}</code></pre><p>这个函数表示设置间距,传入开始值,结束值以及单位。</p><p>max函数,同less的max函数一样。如:</p><pre><code class="scss">$maxWidth:250px,300px,350px;
max-width: max($maxWidth...); // => 350px</code></pre><p>其它知识点同前面示例。</p><h3>20. button-ripple-effect</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509104" alt="20.png" title="20.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=h4P6%2FDnqWqKGi2hLlPm5dQ%3D%3D.oJUFe%2FBRzM6xIAYr2EvMTeESjuwKWZsgJb18jq0hkOxDr9E5Shd7Bd4cQ4ztMahrxecywuQYJsN8%2FlbW2z3dXMma3f73xGX2XTuta2PXORg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=SzsCTqcRMCzr751N2X9bPw%3D%3D.opgEuTU2huusGVPwdP5yGFsN5evSy4si%2FZ%2F05tys5jOO4CXzxmk%2BDE2Lx4MQy2u4yqeOSb8FFkEPGBUOw%2BD9Jg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>没什么知识点好说的,有个MouseEvent表示鼠标事件对象的类型,以及HTMLButtonElement表示button元素的类型。</p><h4>scss</h4><p>可以在scss当中引入命名空间,然后使用sass内置的函数。如:</p><pre><code class="scss">@use "sass:math";
//可以使用math函数
//如math.atan2,math.pow</code></pre><p>其它知识点同前面示例。</p><h3>21. drawing-app</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509105" alt="21.png" title="21.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=6mKLgu47sVxHxPqMnFIong%3D%3D.4Mv5jyEkdjGsOjqV%2BUgNa17HpPsB3dHmJnn%2FET3Nc82gAKdxkE%2BkFVVwoSHt7MWLJYxBzw4msD8j6c7Zj%2F%2BtNgtgQM3f6TEnkelvM8ARzK8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=%2BzCrkghxd2hXmRNJWKKPYw%3D%3D.9Yhh6HWRXjMzM3EF48rEXtE%2BxF5C2RA4LVUy8RL5vyd%2BGE8v9kyK90llOJD8edbXEDixjeLIEKSA7X4gUhyQWQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>动态组件,component,传入一个is关键字,is关键字作为组件名。如:</p><pre><code class="vue"><component is="div" ref="container"></component> // => 渲染一个div元素</code></pre><p>本示例中封装了一个基于我的<a href="https://link.segmentfault.com/?enc=%2BueeE5lF6vCSiaRM0H%2FDHg%3D%3D.BfHMc66l5qhiQtQGyJq8h7cPEdSxGmgnY4go1akC0o4ARJ6kXRhNN7KzccCGKSqG" rel="nofollow">js实现的颜色选择器</a>颜色选择器组件,代码如下:</p><pre><code class="vue"><script setup lang="ts">
import { nextTick, onMounted, PropType } from '@vue/runtime-core';
import { ref } from '@vue/reactivity';
import ewColorPicker from 'ew-color-picker';
import "ew-color-picker/dist/ew-color-picker.min.css";
const props = defineProps({
name:{
type:String as PropType<string>,
default:"div"
},
option:{
type:Object as PropType<object>,
default:() => ({})
}
});
const container = ref(null);
onMounted(() => {
nextTick(() => {
new ewColorPicker({ el:container.value,...props.option }) as any;
})
})
</script>
<template>
<component :is="props.name" ref="container"></component>
</template></code></pre><p>自定义组件v-model指令的实现,首先是定义一个叫modelValue的props,接着在组件内部提交自定义事件emit('update:modelValue')即可,当然也可以修改名字,不过使用方式就要类似:v-model:[属性名]。例如:</p><pre><code class="ts">const props = defineProps({
modelValue:[String,Number] as PropType<string | number>
});
const emit = defineEmits(["update:modelValue"]);
//模板元素
// <input @input="onInputHandler" :value="props.modelValue" /></code></pre><h4>scss</h4><p>list.nth可以获取列表中对应渲染的值,传入2个参数,第一个是列表,第二个则是列表索引值。如:</p><pre><code class="scss">//导入list命名空间
@use "sass:list";
//定义列表
$display:block,flex,inline-block,inline,inline-flex,none;
.el-block {
display: list.nth($display,1); //=> 渲染block
}</code></pre><p>以上代码定义一个类名为el-block,渲染display:block的样式。</p><p>其它知识点同前面示例。</p><h3>22. drag-n-drop</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509106" alt="22.png" title="22.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=nlokMGmAXFC13SVEhykBRg%3D%3D.WDQItQ5F5w0%2FIqiO34eLIVlrfqmVlJ1MUqRdGjyNxm%2B22zwrZEa6i9mBI4b3IPntsRbVkOq1gvtW7nCgVXxUfnLGmopJp9ZaluVh5L%2B2Ru8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ZNQxY37RlDzjpkb16EV3%2BA%3D%3D.uXqsgHaNoXzulgwzJ2F%2B7r%2BKW1dZiKuKqg6%2F2s8ZzOD5gj4wGr9wit0hjOMIQQ41kP8ohmhaNX9m%2BhEruE6jLA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>拖拽事件的使用。</p><h4>scss</h4><p>@each...in循环,同@for循环类似,如:</p><pre><code class="scss">$font-weight:normal,lighter,bold,bolder,100,200,300,400,500,600,700,800,900;
@each $value in $font-weight {
.fw-#{$value} {
font-weight: $value;
}
}</code></pre><p>以下这个函数也有点意思:</p><pre><code class="scss">@use "sass:string";
/*
* 截取属性字符
*/
@function propSlice($prop, $start, $end) {
@return string.unquote(string.slice(string.quote($prop), $start, $end));
}</code></pre><p>其它知识点同前面示例。</p><h3>23. content-placeholder</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509107" alt="23.png" title="23.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=TkbORx0BfGaHxmcrHUS9DQ%3D%3D.ZJ0i9zAqP1RdAaiw5YtruFdM2k2yJNZPwRYx8D6K3%2B5q7pfzGvKkEUZuD1foV1StT6xrxVFvE4mIffbVw%2F4pArE4r7G%2BeFLlIi98SXYpdlc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=h7WrRN9CtKHLMtT3yljPzQ%3D%3D.LAklsPeCR4%2B%2Bznp5%2Be%2FeptyvtdxgSwm3yoDlyJty7ZKJAUSDgAHZnmNBY6Rc7PRPWYpRpYj%2BjVwqX4UjOgErtQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例展示了如何封装一个骨架屏卡片组件,其中涉及到的知识点前面总结过,所以这里不再总结。</p><h3>24. kinetic-loader</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509108" alt="24.png" title="24.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=MiLAv%2BIaNZwTPjY6WSaiyQ%3D%3D.teJ4%2BzPk8OTFltUkzD6y5HbMJcxD2svjoQoVfQgT5VDQcfmMMXCthjRDyHbJwMWp3CBDmDwPv1hh%2Bri%2BaxaR4%2BVXDXWAxjGIWudMmAx3EQc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=AsI%2BSfF190O%2B8Mbx%2Fu3f7g%3D%3D.WTbc5a8y%2FYAKhVsCxWyLr7RpZ9p7BRB2k5p%2BZzB8dyi8OGZJFVcHfOrLBPVgLcOQMkEOOTU%2FAKGL8sHcV7XvNA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>25. sticky-navbar</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509109" alt="25.png" title="25.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=mMA535iFBhQLpmJmMXRZtw%3D%3D.6BP5KB9VRDjERgoLAtSbJjxH9Gf1ytEKQoEV3d9E6RFTIOhkshGlR%2BmGFsx%2BQ1aLyfT%2Fy4iMjKyn37B0JEiSUgjfsSoYHNGxjWOybj2G30c%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=cWfZDnQj%2BchzkGstu%2FmUgw%3D%3D.hDQm1kUdPMKitrIKUY7GrrFY3mHtUpJkeg79%2FbSHO1HtwffVClXnj%2F3ESAGVks%2BeDv%2BsmMzTYzx3qeziBFDHmw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>一个SVG Icon组件的写法:</h4><pre><code class="vue"><template>
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
>
<template v-if="Array.isArray(iconComponent)">
<path
v-for="(item,index) in iconComponent"
:key="item.prop + index"
:fill="item.color ? item.color : color"
:d="item.prop"
></path>
</template>
<template v-else-if="isString(iconComponent)">
<path :fill="color" :d="iconComponent"></path>
</template>
</svg>
</template>
<script lang="ts" setup>
import { iconPathData } from "@/utils/iconPathData";
import type { IconType } from "@/utils/iconPathData";
import { isString } from "@/utils/utils";
import { computed } from "vue";
const props = defineProps({
width:{
type:Number,
default:30
},
height:{
type:Number,
default:30
},
type:String,
color:{
type:String,
default:"#fff"
}
});
const iconComponent = computed(() => iconPathData[props.type as keyof IconType]);
</script></code></pre><p>似乎这样封装不太好。</p><p>transition组件,用于添加过渡效果的组件。</p><p>Promise.allSettled这个可以参考<a href="https://link.segmentfault.com/?enc=Mz6TgQpgpDmrten3eD4ObA%3D%3D.oSxcHPgKOMvwr7T9PR%2F%2FNZuESfdFZ6RnYaye9VUSGHS%2F305Gh5f%2F33K%2FZdsJXFApuv9RT67vyur9t8B1M4NNZMrr50GzpHqhDlNl1B6z7jBo42B4W6Ss4RCMTIX%2BIF5eisRzjBhO3NLeBhpdy0t9TA%3D%3D" rel="nofollow">文档</a>。</p><p>其它知识点同前面示例。</p><h3>26. double-slider</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509110" alt="26.png" title="26.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=cgYHlyexX0GO%2FSGDJHQ47Q%3D%3D.7VNtguk75BBHRRsDaFNkpOxjKfKhp3E5kgU8GJoSiuTeqU4R4Y4lwfCn%2BGkK1ransPoz0Ko3Qvb9J12B5Ycefs6mAprdx1Esrvdl2NF7XS0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=9Wzla4znrtoNcoLdbsbgKQ%3D%3D.B1MqKd05WnZtYWPnITlDM%2BzoDRIROoUcL5m0cFfU8RhlpGIDm9RfyE6Swvw9b3Jwsd3L6kHdnqui2ZGmXBpTbQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>27. toast-notification</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509111" alt="27.png" title="27.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=dTA0Wetd2eZAKA1ncQlznw%3D%3D.KRGGyiqSQUo4mzwW4sYX6SAJ4Eb0HOAD7216A6IrWB7If6W8opPPm7ucqUgkQYRQjxUe4NMBB2ZO7DsdfGM03c3XmdUYvWpYCLG9MHZ3yBw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=JmNGzTkvdSKnm2dsgML4LQ%3D%3D.2798KlVZOtAcpLV9%2BNZ1Guykcy2m9a6C5yMBGjANrLuKwDakUFhQ71vRG%2FpzKBPUjBBwrpFf2jqWsRScJbaRfA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>typescript</h4><p>这个函数有点意思:</p><pre><code class="ts">interface ConsoleType {
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
bgBlack:string;
bgRed: string;
bgGreen:string;
bgYellow:string;
bgBlue: string;
bgMagenta:string;
bgCyan: string;
bgWhite:string;
}
export function colorize<T>(...args:Array<T>){
return {
black: `\x1b[30m${args.join(' ')}`,
red: `\x1b[31m${args.join(' ')}`,
green: `\x1b[32m${args.join(' ')}`,
yellow: `\x1b[33m${args.join(' ')}`,
blue: `\x1b[34m${args.join(' ')}`,
magenta: `\x1b[35m${args.join(' ')}`,
cyan: `\x1b[36m${args.join(' ')}`,
white: `\x1b[37m${args.join(' ')}`,
bgBlack: `\x1b[40m${args.join(' ')}\x1b[0m`,
bgRed: `\x1b[41m${args.join(' ')}\x1b[0m`,
bgGreen: `\x1b[42m${args.join(' ')}\x1b[0m`,
bgYellow: `\x1b[43m${args.join(' ')}\x1b[0m`,
bgBlue: `\x1b[44m${args.join(' ')}\x1b[0m`,
bgMagenta: `\x1b[45m${args.join(' ')}\x1b[0m`,
bgCyan: `\x1b[46m${args.join(' ')}\x1b[0m`,
bgWhite: `\x1b[47m${args.join(' ')}\x1b[0m`
};
}</code></pre><p>可以参考<a href="https://link.segmentfault.com/?enc=3I0S%2FhAhwWA88Pn5ApC8Pw%3D%3D.VDSpOeXx%2F2pJS6A9rcEypjwb3yNatF47Q5gcESTqSCOLWxZAT2xACtTWBPWTy8yoZU%2FTxqU2GL%2F5MISpiWv8WF5sf80id2lrjPAF87ZAItM%3D" rel="nofollow">这里</a>详细了解这个函数。</p><p>接口的继承,如:</p><pre><code class="ts">export function consoleByColorKey<T,U extends keyof ConsoleType>(str:string,key="black"){
return console.log(colorize(str)[key as U]);
}</code></pre><p>createVNode方法,vue内部方法,提供一个创建vNode节点的方法,这也是这个示例的难点,然后是render函数。</p><p>其它知识点同前面示例。</p><h3>28. github-profiles</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509112" alt="28.png" title="28.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=m4fK4VaFtqf9IVQSZg%2BZjA%3D%3D.dmbN5wvjZPfIVALz4OpvdzhOnmqv80TW3iOgh17iDG0onG41%2F6065DVMje0mHxtkfOpz6%2FryJXunyQbqpWoVPkC0d2BsgAUia9qeQtTzHWQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=37Sj3yLQhLFF5Fc1By07%2Bg%3D%3D.L4s46EIzduXqTTdbpG3%2FhE3smb2lerLXeuuwuL5tqZddQ9dw9Y6C801qfEwnlWrBbs2aCyn9ccq20NSwOjnJaQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>29. double-click-heart</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509114" alt="29.png" title="29.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=aGenKHXeo1uzPXVgevVQTg%3D%3D.gr6tRbm5yx05x1SJkRne89Ip9VPomkjyDE6HUMeIsdSX%2FX4UfdpyF8P2WI%2Be6At1yXf9gP39%2BePP1HvYORzkrhS7IHdZB8azr%2Fj6bF6QGE4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=5Ymayie8KNiTivKQeiKa8g%3D%3D.3sjJEVXqVGcXQetQPRb%2BlXtsv41VXnOTWocAcP2Bcz6X6h56SdaxoHG%2BGW0f%2Fe%2BS7kA1bMWV11h0tC0gay1OPw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>30. auto-text-effect</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509115" alt="30.png" title="30.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=VhvInYU%2FUNP%2B9AaLY%2FUQ9Q%3D%3D.IZjSWdGWTp2Q%2F0yRNahQXWEJA%2BVkIdeXLjmNFxofiUWlBjqoxPH8RoMPh3viY2xawyBZgGAAshwExKucCQAgGA0Ij4k%2BdPy%2FVrqzj%2FyFXG8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ykXJ7c3vWnB7Lr%2BjwT9zHg%3D%3D.WDzSaA9MZRblij7DpdxwaQOaI8lFhBwtZpCsM5SLdS9UaYJAwVpS%2BGPktDfsCPJb1%2F77i3vfLqjweswqzW8XPw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>31. password generator</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509116" alt="31.png" title="31.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=3wRh2RjS6I6dSJBHko%2B%2FPg%3D%3D.hPk4dNg7PW4F3MTB2hp%2FdTTuy6XOe00NaJiZ56BprFTKf%2Bjr60PlYHGhNfz%2FZ4a9Hvm2caZuiV7gMTtG4BXP0%2FhB88%2Bzo9QkhBuOvFwORaY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=B9XE4c4YpKX5V4iQkzeeaQ%3D%3D.M8XZqAWVMJ4V4rizJennSJoVzOG8fWAuawUWtlci1mpDnidqKsto2YPefJfjxXRpKzruS%2FYcL4rtHXJ59YEEUA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>获取自定义属性</h4><pre><code class="js">const target = e.target as HTMLDivElement;
const lang = target.dataset.lang;</code></pre><p>实现复制文本的方法:</p><pre><code class="js">const confirm = () => {
(window as any).ewConfirm({
title:data.value.confirmTitle,
content:data.value.confirmContent,
showCancel:false
})
}
// `navigator.clipboard.writeText` not working in wechat browser.
if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
navigator.clipboard.writeText(password.value).then(() => confirm())
}else{
const input = document.createElement("input");
input.value = password.value;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
confirm();
}</code></pre><p>这里用了两个插件。</p><p>其它知识点同前面示例。</p><h3>32. good-cheap-fast</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509117" alt="32.png" title="32.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=6djRQv4nco42t6XSYNlYlQ%3D%3D.G9vDrd0SF4yVgNJ8hdkbyG%2B5jemTfNqxNFHPNNHPvh1FxXLARg%2BbWF49oSGPe7C6isz9oRfqF073QBQRyk3gs46VxuriFJ2%2F0kqCAB19FZc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=IY4n3UvyPn%2FcEGlyaiMCFA%3D%3D.o2itt%2F6Foqiw5%2FX5QsR%2FXBg6O3NAQRH6pIeZ%2FjKBQkH3%2FcHAm9mtNZY2EFsucLfE45ab8SrXw6kTN6ugqNR07w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>33. Notes App</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509118" alt="33.png" title="33.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=1CnD%2FQPz0CTWje07vDTp3g%3D%3D.FXzTTV82%2BisGNNrJkari6PiA%2BfpfYCeDATrfMGqFDc6ZEnU5VA%2FRHVK7bXIEttq4KEKNFDOzCVFkGHSB%2FGvVNBIj%2FSFeijaJCwZqnQExLfM%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=8%2FsVaq%2BfEHZT3FPRDGSZ4g%3D%3D.QwaOWlciEr7KxgwTiqf3Ogs0WD9ODqZLsmZ0ETlOlaj5DBKLQQ8Ok%2FtyR%2FW6YuBslDor%2FuG4WR9OBlWUebjy8g%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>classnames函数的实现,源码值得细细品读:</p><pre><code class="ts">export type Value = boolean | string | number | undefined | null | Symbol;
export type Mapping = Record<string,unknown>;
export interface ArgumentArray extends Array<Argument> {}
export type Argument = Mapping | Mapping | ArgumentArray;
const hasOwn = Object.prototype.hasOwnProperty;
export default function classnames(...args:ArgumentArray):string{
const classes = [];
for(let i = 0,len = args.length;i < len;i++){
const arg = args[i];
if(!arg){
continue;
}
if(typeof arg === "string" || typeof arg === "number"){
classes.push(arg);
}else if(Array.isArray(arg)){
if(arg.length){
const __class = classnames.apply(null,arg);
if(__class){
classes.push(__class)
}
}
}else if(typeof arg === "object"){
if(arg.toString === Object.prototype.toString){
for(let key in arg){
if(hasOwn.call(arg,key) && arg[key]){
classes.push(key);
}
}
}else{
classes.push(String(arg));
}
}
}
return classes.join(" ");
}</code></pre><p>然后就是这3个工具函数,如下:</p><pre><code class="ts">export function createUUID(){
return Math.floor(Math.random() * 10000) + '-' + Date.now().toString().slice(0, 4) + '-' + Math.floor(Math.random() * 10000);
}
export function getNotesData(key:string = "notes"){
try {
return JSON.parse(localStorage.getItem(key) as string);
} catch (error) {
return [];
}
}
export function setNotesData(key:string,value:Array<any>){
return localStorage.setItem(key,JSON.stringify(value));
}</code></pre><p>创建一个uuid以及存储笔记数据的会话存储方法的一个封装。</p><p>其它知识点同前面示例。</p><h3>34. animated-countdown</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509119" alt="34.png" title="34.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=DZrSwsGx%2Bdb7PmuvwdTRWA%3D%3D.dqb8blLwX8WJqN3cAJm%2FytbGRS%2FOhInvi%2FiylYVrqmFd3yciiNXHagyk3zVuGwWP30uwEkIlXUU906XOwp4CbqWB6izozp%2FatC6Tqjqrgjw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=gXIkjwRAFgU6gnZLUsiqhA%3D%3D.wlvXh%2BhXPFoMk8K%2BA4MRi%2BU688b9gAO6xPCmqrpAxOIGMvYUUfYkWdlpaIC5Ke1y4S%2BppsHYbqgRLoqaPCr%2F1w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><h4>javascript</h4><p>defineExpose方法,用于向父组件传递方法,可以允许父组件通过ref来访问子组件的方法。如:</p><pre><code class="ts">defineExpose({
startRunAnimation
});
const startRunAnimation = () => {
//...
}</code></pre><p>InstanceType类型为typescript内置的一个类型,如下:</p><pre><code class="ts">InstanceType<typeof AsyncNumberGroup> | null> // => 代表返回这个组件AsyncNumberGroup的实例类型</code></pre><p>其它知识点同前面示例。</p><h3>35. image-carousel</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509120" alt="35.png" title="35.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=sRX2Vd6zFZZ4fiEXF2pq5A%3D%3D.9brr5E8C3DYOSekOeMC2QgF3GP4Yb8iyrI3uWgNTZNYKOfeo%2Fqg4Ru8Xs5%2F3yGtA4Yy%2B3Sg5iSn5Jv1OKwsxQve8lhsFvhDs%2Fpthd%2Fk5Pj0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=zR0SlAi5Bx69BQB%2FyfIGXQ%3D%3D.UW7hu%2B%2FfqUOUZlYylWCO4xZK9E53aJikk38Qa360rNtkeeHPZBRyXXK7ylqEbcCV6XcpV4kqLgXOAsXYoeTFpQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例展示了一个轮播组件的封装,但这个示例似乎还有点小bug,源码涉及到的知识点在前面的示例当中也能够找到,不必赘述。</p><h3>36. hover board</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509121" alt="36.png" title="36.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=hvycJl%2FAbHncj2G0uRufzw%3D%3D.ocpObniqr2Cc0ADLQJYXFgJlQLJNbnOarfTwGwxGbXqa1b8VOheN4ei3yfF4kDcn3EPxKKlamehfLYpnPBXTWl6ikQQmBro9Pgooh7y0MMw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ZsfLOP60zI6xRO3pV9sOFA%3D%3D.r3c8ATh81lbTeoL42rW6dsblaNYDhHQQxQvHeVu9i0Q1ZuqexyFlLNGtxlyrIxitUkddrRaqF2tlmR1S2faEbw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>37. pokedex</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509122" alt="37.png" title="37.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=7dalD4GVL5t69YIbAtvuVA%3D%3D.oPK38HFwquI2EQVFZ%2Fj6ixxERvgKZ76lWM%2BWd%2BuaJsNhEL%2BMASSYUaC0797VHNg04Ng%2FNYmr5ApXWDiH%2Bg3L9US5mwXMMVQkNRvDQym%2FIdA%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Byf%2BysHlQ6%2BzAOcTMsqUvQ%3D%3D.%2BBmFsvD6U42XQMkSLR3FHFKoz8xGclJ%2Fanu5wF1RGoQffF2C3zz%2FMuG1Wgte8t78KZAlmSIUtVLE5xlGNVhh1A%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>创建一个index.d.ts,可以定义命名空间,然后定义接口返回的类型,如下:</p><pre><code class="ts">declare namespace 命名空间名 {
//定义接口
}</code></pre><p>这样做的好处就是我可以直接在组件当中定义该类型,vsCode会自动帮我们显示访问的属性,然后我们就不必要去打印数据看看数据结构再来决定取得什么属性了,这在实际开发当中是一个很常用的提升开发效率的技巧,这也是为什么typescript在后续维护当中非常好维护的原因之一。</p><p>其它知识点同前面示例。</p><h3>38. mobile-tab-navigation</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509123" alt="38.png" title="38.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=yvs7nuRI3OzEq%2Fy4v7uhIQ%3D%3D.jARNXUQFxN3EKJP6tDHshxBC2lMZIOvZVg1eopPaTExSGtksZFEiUWXngkf%2BoDMKvdYkSbd1KGKVqvPykeAqVsGwSGNamYHi64aAoqlBmNU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=g7QHsoZ8NuxV2bp7j4z%2BEQ%3D%3D.Bzi64wKCV3nMZmNy9Q%2BWPIBVrkwtK%2F1d2qGvhJdzVzq4fcD8A1gJWNcYz%2Fq%2FeNP%2B5cQ2y%2FXFtWe0Uk0hBJ47Ww%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>39. password-strength-background</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509124" alt="39.png" title="39.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=zBjOX%2FxnKei%2FkCqkU4mRYg%3D%3D.4ZqgreP%2BCRqZs88M4pz1SeUGTm7vp4x9nChsXJbT9vEOiKUKlQ2gTHj2N9gvRVz%2Fs5gREaW6XmUbogGs9dVv694rhRvongb5RtS2jpB2r1o%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Hlm0Gd4zoaQHF7WJXKQaYg%3D%3D.vcckxMsZLQMa4kFwBtu1Q9WZbYx4MFkWaS7Qn6JqigJIqiNbmF6mcSUmz5wftM36o%2B%2FevgeIdVO%2FbqSoElFSHw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例主要展示了如何将<a href="https://link.segmentfault.com/?enc=NHTrENLjtvFqPzNWNOatfA%3D%3D.O2sHe8zhmKdPNAQCrDa3VoiD%2Bocez%2F1h16lQ8f%2BDKSg%3D" rel="nofollow">tailwindcss</a>添加到vue项目当中去,跟着官方提供的<a href="https://link.segmentfault.com/?enc=r%2F1uhxCldHVqSQ0L7keAaQ%3D%3D.sHYxGarwWGxWQznoczkID8gAvdxhh0YJaotvoAmO323jvhDadIuZ0VrGg%2FhRB4Ke4Y%2Bk0rdcBuNCRHEGX2S%2BeQ%3D%3D" rel="nofollow">教程</a>就可以了。</p><p>知识点同前面示例。</p><h3>40. 3d-background-boxes</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509125" alt="40.png" title="40.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=aKNrkC4rm607a2F9LH8HxA%3D%3D.6IuQqHYdMzlT8IpDnxcG%2FETnCdb%2B3j%2FOM0zByAAJRrBbg601xAUSBGpaoz1%2FGV8AaYy%2BnoczjKMbKx4dZOGZ%2Bunkcn2nWTv30UQgma6rIko%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=wVNJk9F9k1FHWeTWnt3pLg%3D%3D.dGnxWqQND4cPPKriBhi9OOv3eVNqx8zIs0bRWyxF94j103zD0JXVwDmWjY1sr%2Fuj7PBxL%2BUQp8sBLJxwWD280w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>41. verify-account-ui</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509126" alt="41.png" title="41.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=HZLnN3oa7o6Yk%2BSdfaf0xg%3D%3D.bkWrmWNsTfYebDZ0%2BEDpcB8j%2BgLOEFZO3%2FBpeQLHA8ZZyPOPzZJIv7y3%2BHse5h7fnmkYvxShdp6CU%2F6cUnVajkif8xxLhcAE7%2FNBi6nVI%2Bk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=154wvTxkXXZjOB9%2Baqx6Fw%3D%3D.yxUB1EuAxtqrnNvsDmol7Ke2%2Bt7SXOqoh%2FMWfnywHSkza5oeeAsv14hysv5dWXmVrMU3kezr5dodU0SNYRumPg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>42. live-user-filter</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509127" alt="42.png" title="42.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=F5DpMy3xDMGD%2BlYnVExj2g%3D%3D.zhf3Ck5vpBJ%2FBXFUyfu%2B3QNquQHbkuOLKOWwOax1E2XL6Pp5PaHgUKK9LspfXk3iN1tiuEMg7M7asywyXtL%2Fk3GTYkPo4LX6OfO2DLLsQA0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=8NfEa09Bm58VDCP8llF6vQ%3D%3D.7pMLRzRM2jNeqrDVuQwfFbVDxNZlIeQtX0%2Bclg%2BKX%2BiDFAya5F8I0csYFyGeGu8XQEQNCyYbAEHsZdFWuPUHaw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这里有个预加载组件和加载组件值得研究一下,预加载组件的实现思路就是利用Image构造函数来提前加载图片,如果图片未请求完成,则显示loader组件。关于loader组件的实现可以参看<a href="https://link.segmentfault.com/?enc=fOr%2BNn38kW6iLUBV8ddg2g%3D%3D.Lfpy9Sw1m6CYDr2es%2BDJ0pSLfPvL0etfdwHJLPaAcMEUwMMqCr%2Fv4ymdGTr6c9JGuhYWKMCVqwJoOjUliCm%2BfA1xXmDBegsWVWH4kEJsUmCLzD0oo8LlaUGOaf4I4Y%2FP" rel="nofollow">源码</a>。</p><p>watchEffect函数,与watch函数有所不同,该函数是一个副作用函数,可以直接传入一个回调函数,内部会自动监听数据的变动,当数据改变后,该函数内部就会执行。</p><p>其它知识点同前面示例。</p><h3>43. feedback-ui-design</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509128" alt="43.png" title="43.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=ccMVCJORO6PP9jnOCSTRKQ%3D%3D.aTHuAKvkj%2F08vtnUEYpu%2FboR9warYRNeG5UYruqimB9MclM0yZqwJqZkYrOZ%2FjFwPLh7XTNYKIByWbH59pHjkTHfOMyE4Gv6nxxeIogxyYk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Bha4xwu%2FqYD5Rt8cue317w%3D%3D.oTRs2XGxofgUWOwwoXzPxdJ4MdYG6KodNIADd36nAmriJdTFl4WKYEr0b0iX4vH1BKqDZ%2BKcvHrVghejhijieA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例值得学习的是使用css做了一个翻转效果,其实也就是利用了rotate函数。</p><p>其它知识点同前面示例。</p><h3>44. custom-range-slider</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509129" alt="44.png" title="44.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=vL34eIgMkJK5fZKeof5TEA%3D%3D.jwCf9Zie6g6r7oscXIltV37V5K9K%2Fiud%2F6aOC6H2CvGLZp3qBSjSDXouEwaKx2%2BIRv1fKRaeoj1uAWJ6%2FkEi66SfH9gh5v6bmRbGGjH6UuQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ECYoKj6tG6bIFMLYL2OKWQ%3D%3D.XdOSCJ3rZ5fQ4PSz4BhRXtC%2BJ8%2FwNXvCaOgX%2Bg3Xwr6pHOf5cFqkvIXqDkwXYycE7q4rbY3We4XyE%2FSQaAm80Q%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>45. netflix-mobile-navigation</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509130" alt="45.png" title="45.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=o6%2Fbt7x%2BlF6pBT%2Ff6Y%2FEtQ%3D%3D.yECEbAyNtqV%2BXAEJ1lXfkSkJtK3rUaE3vsT052VKNp2b8TAjwitbJ4MQ7tXrBBPtRZX2yOFY8jqTo49T0C1vblwOBHPyTTYSJchhI3Y00MA%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=T9F51DolVT9LOCCArgm7FQ%3D%3D.9x7bGWPV0E%2BrpuXaO4whUWxXZozLvEepno1yU7CURzUS2M7%2BmlKWIDgIFX0ymTPgOFLm%2FSIQJz0d7Au9OMvrCQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例展示了一个递归组件的实现,其实参考官方<a href="https://link.segmentfault.com/?enc=BR%2F8SzSePOdY%2BockQP8ugg%3D%3D.BobNLZfneOPe4Lx5zLwOEB%2Bf0FcSO%2Bd803cuco1zMm97EVoe3xjEpIZFfeJy%2BGu2" rel="nofollow">示例代码</a>就可以学习到思路。</p><p>其它知识点同前面示例。</p><h3>46. quiz-app</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509131" alt="46.png" title="46.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=rEZoSzMSB%2FHym2eZ6%2FYOvA%3D%3D.KbpwltZtq9SoQXZvpie%2BieOZ%2Ff1clD%2FS6xIdksDcabOJkklbHNh%2BLSPPvncp1WGO2pgR0BCQNr3unfDZWcSZpM4NgRAoIS7ncKHmvhO8x%2BU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=oigePyUMf9xqHKdtQRngrg%3D%3D.jXQZOpRoEucx%2BKx4xOeXyK6BSU4OUPTJSxRqdoALGpKRUbhQOCUoxWpyI5mQsY5%2B%2FUUntmiZF%2BZAj%2Fe8Zm5xUw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>47. testimonial-box-switcher</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509132" alt="47.png" title="47.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=yr0XIA1O8ILRm1GCuEjIZg%3D%3D.IOFaFQ8A5ydMOStZFDlXGAgAqFAZNw4Tlzj%2BFbYWUCAtSrFQT%2FftMXNAU4CE0PPSdS8MONz411fZ%2B%2FRiTRLZi07dT2Kx52W8naVp67%2BaPsI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=7eSMZ4%2F5cwvWpoIYS9e5Mg%3D%3D.4xlzHzWvaIjNW9XAlrfHbj0bG2PlwqAtUkCrg7c3YopvZVqEunNdBKSv%2FDv9ALZkCQMwC5ohOOAUnusXzK79jA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>知识点同前面示例。</p><h3>48. random-image-feed</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509133" alt="48.png" title="48.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=SnE97FPQnfdVjFrr5FeLDw%3D%3D.kca9QQh%2BWyAqvUbkaLVVaMjMSF4%2B7oZogj5JD8WzCZTP08LCRh7Zci%2FRdhpFyjtOEi46kn28lwbe1Di9PczXZGUaV2uZljbo5syNAPfjNzI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=mrHqzjQX8RIor1KmlZw0Fg%3D%3D.riD4Mgo1JwaJQfwTXZXVYO0ynOYiLzHFa3yRbtxHM5FZPdDNFLGRm1ZCaYmyoD9mDftVf8KXJCZQbwZAvqBXlw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例预览组件的实现还是值得学习的,如下:</p><pre><code class="vue"><template>
<Teleport to="body">
<div class="preview-mask" :class="{ show: modelValue }" @click="emit('update:modelValue', false)">
<async-pre-load-image :src="src" class="preview-mask-image"></async-pre-load-image>
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, PropType, toRefs } from 'vue';
const AsyncPreLoadImage = defineAsyncComponent(() => import('./PreLoadImage.vue'));
const props = defineProps({
src: String as PropType<string>,
modelValue: Boolean as PropType<boolean>
});
const emit = defineEmits(['update:modelValue']);
const { src, modelValue } = toRefs(props);
emit('update:modelValue');
</script></code></pre><p>涉及到了Teleport组件的使用。</p><p>其它知识点同前面示例。</p><h3>49. todo-list</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509134" alt="49.png" title="49.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=krHHKh9a3BSxVpyiZUXZgA%3D%3D.K%2BfXLv5WbEvIharpktgDRNiQbA7wt769TedfA7K4ZwzfWdt27CCGHWg7agAOyfndcn0lg83CJcGGnmXLn%2B206eGj1TQ7%2F9nuCSo7XzsEa4k%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=J8cYsXit1qtR0XxW7ekbew%3D%3D.VpkUHH1%2Bw19AfUhKoZlFlaQGtInAQGqD2mSoM19%2FPKNkldrmjZNTWkmo2zIgifRBP4J7iC3p0PVnRleqVwmkkw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>本示例实现了一个<a href="https://link.segmentfault.com/?enc=Zlkwq206mMG%2F7IS%2BI7Mj2Q%3D%3D.qkoprK2NOvYGDrsPjelhxI4Pt1eOjT2tAU9t2HnfYlUtGR2PzSXCDcSBoe07jD4218KCf1YwZ3vukYcKfqqgz1OtzfloKSE6cDvLDUd3micVaHnLq1k51gxZf%2BXdig6S" rel="nofollow">Modal弹框组件</a>,并且实现了一个自定义<a href="https://link.segmentfault.com/?enc=8lQfSqgTDyWBJD5zZd14Zg%3D%3D.c9ClOmFDLTyx2aiGCWd4kARgp%2BJ2UIXqH%2BI8kGHO0uxaBBXbePKj55vQKj3GNuJOIuN3LyVvcEgvNQzkkfEcJU1sz7n7nQ2KDbwAourL4NdI7Gv0d7rmVIh79Qo8dEYqSjPNFxpkZ9DkB40T2NGmkg%3D%3D" rel="nofollow">clickoutside指令</a>。</p><p>其它知识点同前面示例。</p><h3>50. insect-catch-game</h3><p>效果如图所示:</p><p><img src="/img/remote/1460000042509135" alt="50.png" title="50.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=urFvOzpmcO7Np3cXqHxRwA%3D%3D.vizBs4kkxgeAMIkVBFWGDk%2FAha3Jf6kfVDw69Iw1nKxjj0G5b6iLdFgF4G7yLRHYi%2FwWySnP6Aua2oZJGLH5XC8AZyUidC2E4AVnxWERjN0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Rw79mlhH1BeXDB8wrN4u4A%3D%3D.7LExLa%2BC68zjpOSC7EWR0QC1QN9CHIsnueytRO%2FnZBxaRHlMuzshfZERZYkTvjKaJAqdKFT%2FsXTh%2Ba3mso79LA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这是一个比较综合的示例了,几乎涵盖了前面所有示例的语法,本人个人也觉得这个示例值得学习,细细品味一番。</p><p>看完本文,你对vue3以及scss和less是否有了更深入的了解呢?当然也许还有我没有总结到的知识点,更细致的就要去通读源码了,我个人是认为每一个示例都涉及到了相应的知识点,至少亲手完成了这50个示例,我对vue3的使用更加熟练了。</p><p>如果觉得本文有任何错误,欢迎批评指正。</p>
实现《羊了个羊》的小游戏
https://segmentfault.com/a/1190000042502121
2022-09-16T19:15:14+08:00
2022-09-16T19:15:14+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
16
<h3>实现《羊了个羊-动物版》的小游戏</h3><p>这两天火爆全场的《羊了个羊》游戏,相信大家都玩过了,那么在玩这个游戏的同时,我想大家都会好奇这个游戏的实现,本文就带大家使用css,html,js来实现一个动物版的游戏。</p><p>首先我用到了2个插件,第一个插件就是<a href="https://link.segmentfault.com/?enc=YTDxGtIHWpiKtKKgN7WEGg%3D%3D.VQy%2BTz5%2BcaIUakk%2B1cJgHW4nM2TW1Zl%2BZOczVJeEFE8adm%2F8A8j%2BzC%2Bmorc1hANP" rel="nofollow">flexible.js</a>,这个插件就是对不同设备设置根元素字体大小,也就是一个移动端的适配方案。</p><p>因为这里使用了rem布局,针对移动端做了自适应,所以这里选择采用rem布局方案。</p><p>还有一个弹框插件,我很早自行实现的,就是<a href="https://link.segmentfault.com/?enc=otdEi587LFQYTZ2CDagQHw%3D%3D.SL%2FUwvKdGZ3dZOB%2FBLrtGatnALXAzuLtKG6DpovGSr%2FDKFLROYDtYAJpgqROLKmccZdjFZ%2Fz442ZscYBLLyq5Q%3D%3D" rel="nofollow">popbox.js</a>,关于这个插件,本文不打算讲解实现原理,只讲解一下使用原理:</p><pre><code class="js">ewConfirm({
title: "温馨提示", //弹框标题
content: "游戏结束,别灰心,你能行的!", //弹框内容
sureText: "重新开始", //确认按钮文本
isClickModal:false, //点击遮罩层是否关闭弹框
sure(context) {
context.close();
//点击确认按钮执行的逻辑
},//点击确认的事件回调
})</code></pre><p>引入了这个js之后,会在window对象上绑定一个ewConfirm方法,这个方法传入一个自定义对象,对象的属性有<code>title,content,sureText,cancelText,cancel,sure,isClickModal</code>这几个属性,当然这里没有用到cancel按钮,所以不细讲。</p><p>正如注释所说,每个属性代表的意思,这里不做赘述。</p><p>然后html代码是这样的:</p><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>羊了个羊《动物版》</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
</body>
<script src="https://www.eveningwater.com/static/plugin/popbox.min.js"></script>
<script src="https://www.eveningwater.com/test/demo/flexible.js"></script>
<script src="./script.js"></script>
</html></code></pre><p>可以看到html代码是什么都没有的,因为里面的DOM元素,我们都放在js代码里面动态生成了,所以script.js这里的代码是核心,这个后续会讲到,接下来看样式代码,也比较简单。</p><pre><code class="css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
background: url('https://www.eveningwater.com/my-web-projects/js/21/img/2.gif') no-repeat center / cover;
display: flex;
justify-content: center;
align-items: center;
}
.ew-box {
position: absolute;
width: 8rem;
height: 8rem;
}
.ew-box-item {
width: 1.6rem;
height: 1.6rem;
border-radius: 4px;
border: 1px solid #535455;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
cursor: pointer;
transition: all .4s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ew-collection {
width: 8rem;
height: 2.4rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
background: url('https://www.eveningwater.com/static/dist/20d6c430c2496590f224.jpg') no-repeat center/cover;
position: fixed;
margin: auto;
overflow: auto;
bottom: 10px;
}
.ew-collection > .ew-box-item {
margin-right: 0.3rem;
}
.ew-left-source,
.ew-right-source {
width: 2.6rem;
height: 1.2rem;
position: absolute;
top: 0;
}
.ew-left-source {
left: 0;
}
.ew-right-source {
right: 0;
}
.ew-shadow {
box-shadow: 0 0 50px 10px #535455 inset;
}</code></pre><p>首先是通配选择器'*'代表匹配所有的元素,并且设置样式初始化,然后是html和body元素设置宽高为100%,并且隐藏溢出的内容,然后给body元素设置了一个背景图,并且body元素采用弹性盒子布局,水平垂直居中。</p><p>接下来是中间消除的盒子元素box,也很简单就是设置定位,和固定宽高为8rem。</p><p>接下来是box-item,代表每一个块元素,也就是消消乐的每一块元素,接着羊了个羊底部有一个存储选中块元素的收集盒子元素,也就是ew-collection,然后是左右的看不到层级的卡牌容器元素。</p><p>最后就是为了让块元素看起来有层叠效果而添加的阴影效果。</p><p>css核心代码也就比较简单,接下来我们来看javascript代码。</p><p>在开始之前,我们需要先导入图片素材列表,这里如下:</p><pre><code class="js">const globalImageList = [
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/1.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/2.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/3.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/4.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/5.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/6.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/7.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/8.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/9.jpg',
'https://www.eveningwater.com/my-web-projects/jQuery/7/img/10.jpg'
]</code></pre><p>然后在onload也就是页面加载时调用我们封装好的Game类,将这个素材列表传入其中。如下:</p><pre><code class="js">window.onload = () => {
const game = new Game(globalImageList);
}</code></pre><p>接下来,我们来看Game类的核心代码吧,首先定义这个类:</p><pre><code class="js">class Game {
constructor(originSource, bindElement){
//核心代码
}
}</code></pre><p>这个类名有2个参数,第一个参数就是素材列表,第二个参数则是绑定的DOM元素,默认如果不传入的话,就绑定到document.body上。也因此,我们在构造函数里面初始化一些后续需要用到的变量。如下:</p><pre><code class="js">//constructor内部
this.doc = document;
this.originSource = originSource;
this.bindElement = bindElement || this.doc.body;
// 存储随机打乱的元素
this.source = [];
// 存储点击的元素
this.temp = {};
// dom元素
this.box = null; //存储消消乐块元素的容器盒子元素
this.leftSource = null; //左边素材容器元素
this.rightSource = null; //右边素材容器元素
this.collection = null; //收集素材的容器元素
// 需要调用bind方法修改this指向
this.init().then(this.startHandler.bind(this)); //startHandler为游戏开始的核心逻辑函数,init初始化方法</code></pre><p>这里存储了document对象,存储了原始素材列表,以及绑定的dom元素,然后还定义了source用来存储被打乱后的素材列表,以及temp用来存储点击的元素,方便做消除,添加阴影这些操作。</p><p>还有四个变量,其实也就是存储dom元素的,如注释所述。</p><p>接下来init方法就是做初始化的一些操作,这个方法返回一个Promise所以才能调用then方法,然后startHandler是游戏开始的核心逻辑函数,这个后面会讲到,注意这里有一个有意思的点,那就是bind(this),因为在then方法内部的this并不是指Game这个实例,所以需要调用bind方法修改this绑定,接下来我们来看init方法做了什么。</p><pre><code class="js">init() {
return new Promise(resolve => {
const template = `<div class="ew-box" id="ew-box"></div>
<div class="ew-left-source" id="ew-left-source"></div>
<div class="ew-right-source" id="ew-right-source"></div>
<div class="ew-collection" id="ew-collection"></div>`;
const div = this.create('div');
this.bindElement.insertBefore(div, document.body.firstChild);
this.createElement(div, template);
div.remove();
resolve();
})
}</code></pre><p>很显然这个方法如前所述返回了一个Promise,内部定义了template模板代码,也就是页面的结构,然后调用create方法创建一个容器元素,并且向body元素的首个子元素之前插入这个元素,然后在这个容器元素之前插入创建好的页面结构,删除这个容器元素,并且resolve出去,从而达到将页面元素添加到body元素内部。这里涉及到了两个工具函数,我们分别来看看它们,如下:</p><pre><code class="js">create(name) {
return this.doc.createElement(name);
}</code></pre><p>create方法其实也就是调用createElement方法来创建一个DOM元素,this.doc指的就是document文件对象,也就是说,create方法只是document.createElement的一个封装而已。来看createElement方法。</p><pre><code class="js">createElement(el, str) {
return el.insertAdjacentHTML('beforebegin', str);
}</code></pre><p>createElement方法传入2个参数,第一个参数是一个DOM元素,第二个参数是一个DOM元素字符串,表示在第一个DOM元素之前插入传入的模板元素。这个方法可以参考<a href="https://link.segmentfault.com/?enc=pc4vIqhHCtW39RZaED7dqw%3D%3D.M1TMukMbo1dZYz4eQwLZU94qoxtjuAyCrNki0hPfFPydsiEzsvvDVoO0f%2FxH6tL8Ymy2fpbqvuGDLz1%2B0huByUGh4ixrTeaI1Q5aky8L00g%3D" rel="nofollow">code-segment</a>。</p><p>init方法说白了就是动态创建元素的一个实现,接下来就是startHandler函数的实现。</p><pre><code class="js">startHandler() {
this.box = this.$('#ew-box');
this.leftSource = this.$('#ew-left-source');
this.rightSource = this.$('#ew-right-source');
this.collection = this.$('#ew-collection');
this.resetHandler();
//后续还有逻辑
}</code></pre><p>startHandler是核心实现,所以不可能只有上面那么点代码,但是我们要写一步步的拆分,以上的代码就做了2个逻辑,获取DOM元素和重置。这里涉及到了一个$方法,如下:</p><pre><code class="js">$(selector, el = this.doc) {
return el.querySelector(selector);
}</code></pre><p>$方法传入2个参数,第一个参数为选择器,是一个字符串,第二个参数为DOM元素,实际上就是document.querySelector的一个封装。当然还有一个$$方法,类似,如下:</p><pre><code class="js">$$(selector, el = this.doc) {
return el.querySelectorAll(selector);
}</code></pre><p>接下来是resetHandler方法,如下:</p><pre><code class="js">resetHandler() {
this.box.innerHTML = '';
this.leftSource.innerHTML = '';
this.rightSource.innerHTML = '';
this.collection.innerHTML = '';
this.temp = {};
this.source = [];
}</code></pre><p>可以看到resetHandler方法确实是如其定义的那样,就是做重置的,我们要重置用到的数据以及DOM元素的子节点。</p><p>让我们继续,在startHandler也就是resetHandler方法的后面,添加这样的代码:</p><pre><code class="js">startHandler() {
this.box = this.$('#ew-box');
this.leftSource = this.$('#ew-left-source');
this.rightSource = this.$('#ew-right-source');
this.collection = this.$('#ew-collection');
this.resetHandler();
for (let i = 0; i < 12; i++) {
this.originSource.forEach((src, index) => {
this.source.push({
src,
index
})
})
}
this.source = this.randomList(this.source);
//后续还有逻辑
}</code></pre><p>可以看到这里实际上就是对素材数据做了一个添加和转换操作,randomList方法顾名思义,就是打乱素材列表的顺序。让我们来看这个工具方法的源码:</p><pre><code class="js">/**
* 打乱顺序
* @param {*} arr
* @returns
*/
randomList(arr) {
const newArr = [...arr];
for (let i = newArr.length - 1; i >= 0; i--) {
const index = Math.floor(Math.random() * i + 1);
const next = newArr[index];
newArr[index] = newArr[i];
newArr[i] = next;
}
return newArr;
}</code></pre><p>这个函数的作用就是将素材列表随机打乱以达到随机的目的,接下来,让我们继续。</p><pre><code class="js">startHandler() {
this.box = this.$('#ew-box');
this.leftSource = this.$('#ew-left-source');
this.rightSource = this.$('#ew-right-source');
this.collection = this.$('#ew-collection');
this.resetHandler();
for (let i = 0; i < 12; i++) {
this.originSource.forEach((src, index) => {
this.source.push({
src,
index
})
})
}
this.source = this.randomList(this.source);
//后续还有逻辑
for (let k = 5; k > 0; k--) {
for (let i = 0; i < 5; i++) {
for (let j = 0; j < k; j++) {
const item = this.create('div');
item.setAttribute('x', i);
item.setAttribute('y', j);
item.setAttribute('z', k);
item.className = `ew-box-item ew-box-${i}-${j}-${k}`;
item.style.position = 'absolute';
const image = this.source.splice(0, 1);
// 1.44为item设置的宽度与高度
item.style.left = 1.44 * j + Math.random() * .1 * k + 'rem';
item.style.top = 1.44 * i + Math.random() * .1 * k + 'rem';
item.setAttribute('index', image[0].index);
item.style.backgroundImage = `url(${image[0].src})`;
const clickHandler = () => {
// 如果是在收集框里是不能够点击的
if(item.parentElement.className === 'ew-collection'){
return;
}
// 没有阴影效果的元素才能够点击
if (!item.classList.contains('ew-shadow')) {
const currentIndex = item.getAttribute('index');
if (this.temp[currentIndex]) {
this.temp[currentIndex] += 1;
} else {
this.temp[currentIndex] = 1;
}
item.style.position = 'static';
this.collection.appendChild(item);
// 重置阴影效果
this.$$('.ew-box-item',this.box).forEach(item => item.classList.remove('ew-shadow'));
this.createShadow();
// 等于3个就消除掉
if (this.temp[currentIndex] === 3) {
this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());
this.temp[currentIndex] = 0;
}
let num = 0;
for (let i in this.temp) {
num += this.temp[i];
}
if (num > 7) {
item.removeEventListener('click', clickHandler);
this.gameOver();
}
}
}
item.addEventListener('click', clickHandler)
this.box.append(item);
}
}
}
}</code></pre><p>这里的代码很长,但是总结下来就二点,添加块元素,并为每个块元素绑定点击事件。我们知道羊了个羊每一个消除的块元素都会有层叠的效果,那么我们这里也要实现同样的效果,如何实现呢?</p><p>答案就是定位,我们应该知道定位会分为层级关系,层级越高就会占上面,这里也是采用同样的道理,这里之所以用3个循环,就是盒子元素是分成5行5列的,所以也就是为什么循环是5的原因。</p><p>然后在循环内部,我们就是创建每一个块元素,每个元素都设置了x,y,z三个属性,并且还添加了<code>ew-box-${i}-${j}-${k}</code>类名,很显然这里的x,y,z属性和这个类名关联上了,这方便我们后续对元素进行操作。</p><p>同样的每个块元素我们也设置了样式,类名是'ew-box-item',同样的每个块元素也设置为绝对定位。</p><blockquote>PS: 大家可能有些好奇为什么每个元素我都加一个'ew-'的前缀,其实也就是我个人喜欢给自己写的代码加的一个前缀,代表这是我自己写的代码的一个标志。</blockquote><p>接下来从素材列表中取出单个素材,取出的数据结构应该是{ src:'图片路径',index:'索引值' }这样。然后将该元素设置背景图,就是素材列表的图片路径,以及index属性,还有left和top偏移值,这里的left和top偏移值之所以是随机的,也就是因为每一个块元素都是随机的。</p><p>接下来是clickHandler也就是点击块元素执行的回调,这个我们先不详细叙述,我们继续往后看,就是为该元素添加事件,利用addEventListener方法,并且将块元素添加到box盒子元素中。</p><p>让我们继续来看clickHandler函数内部。</p><p>首先这里有这样一个判断:</p><pre><code class="js">if(item.parentElement.className === 'ew-collection'){
return;
}</code></pre><p>很简单,当我们的收集框里面点击该元素,是不能触发点击事件的,所以这里要做判断。</p><p>然后又是一个判断,有阴影效果的都是多了一个类名'ew-shadow',有阴影效果代表它的层级最小,被叠加遮盖住了,所以无法被点击。</p><p>接下来获取当前点击块元素的index索引值,这也是为什么在添加块元素之前会设置一个index属性的原因。</p><p>然后判断点击的次数,如果点击的是同一个,则在temp对象里面存储点击的索引值,否则点击的是不同的块元素,索引值就是1。</p><p>然后将该元素的定位设置为静态定位,也就是默认值,并且添加到收集框容器元素当中去。</p><p>接下来就是重置阴影效果,并且重新添加阴影效果。这里有一个createShadow方法,让我们来揭开它的神秘面纱。如下:</p><pre><code class="js">createShadow(){
this.$$('.ew-box-item',this.box).forEach((item,index) => {
let x = item.getAttribute('x'),
y = item.getAttribute('y'),
z = item.getAttribute('z'),
ele = this.$$(`.ew-box-${x}-${y}-${z - 1}`),
eleOther = this.$$(`.ew-box-${x + 1}-${y + 1}-${z - 1}`);
if (ele.length || eleOther.length) {
item.classList.add('ew-shadow');
}
})
}</code></pre><p>这里很显然通过获取x,y,z属性设置的类名来确定是否需要添加阴影,因为通过这三个属性值可以确定元素的层级,如果不是在最上方,就能够获取到该元素,所以就添加阴影。注意$$方法返回的是一个NodeList集合,所以可以拿到length属性。</p><p>接下来就是通过存储的索引值等于3个,代表选中了3个相同的块,那就要从收集框里面移除掉该三个块元素,并且重置对应的index索引值为0。</p><p>接下来的for...in循环所做的操作当然是统计收集框里面的块元素,如果达到了7个代表槽位满了,然后游戏结束,并且移除块元素的点击事件。我们来看游戏结束这个方法的实现:</p><pre><code class="js">gameOver() {
const self = this;
ewConfirm({
title: "温馨提示",
content: "游戏结束,别灰心,你能行的!",
sureText: "重新开始",
isClickModal:false,
sure(context) {
context.close();
self.startHandler();
}
})
}</code></pre><p>这也是最开始提到的弹框插件的用法,在点击确认的回调里面调用startHandler方法表示重新开始游戏,这没什么好说的。</p><p>到这里,我们实现了中间盒子元素的每一个块元素与槽位容器元素的对应逻辑,接下来还有2点,那就是被遮盖看不到层级的两边块元素集合。所以继续看startHandler后续的逻辑。</p><pre><code class="js">startHandler() {
this.box = this.$('#ew-box');
this.leftSource = this.$('#ew-left-source');
this.rightSource = this.$('#ew-right-source');
this.collection = this.$('#ew-collection');
this.resetHandler();
for (let i = 0; i < 12; i++) {
this.originSource.forEach((src, index) => {
this.source.push({
src,
index
})
})
}
this.source = this.randomList(this.source);
for (let k = 5; k > 0; k--) {
for (let i = 0; i < 5; i++) {
for (let j = 0; j < k; j++) {
const item = this.create('div');
item.setAttribute('x', i);
item.setAttribute('y', j);
item.setAttribute('z', k);
item.className = `ew-box-item ew-box-${i}-${j}-${k}`;
item.style.position = 'absolute';
const image = this.source.splice(0, 1);
// 1.44为item设置的宽度与高度
item.style.left = 1.44 * j + Math.random() * .1 * k + 'rem';
item.style.top = 1.44 * i + Math.random() * .1 * k + 'rem';
item.setAttribute('index', image[0].index);
item.style.backgroundImage = `url(${image[0].src})`;
const clickHandler = () => {
// 如果是在收集框里是不能够点击的
if(item.parentElement.className === 'ew-collection'){
return;
}
// 没有阴影效果的元素才能够点击
if (!item.classList.contains('ew-shadow')) {
const currentIndex = item.getAttribute('index');
if (this.temp[currentIndex]) {
this.temp[currentIndex] += 1;
} else {
this.temp[currentIndex] = 1;
}
item.style.position = 'static';
this.collection.appendChild(item);
// 重置阴影效果
this.$$('.ew-box-item',this.box).forEach(item => item.classList.remove('ew-shadow'));
this.createShadow();
// 等于3个就消除掉
if (this.temp[currentIndex] === 3) {
this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());
this.temp[currentIndex] = 0;
}
let num = 0;
for (let i in this.temp) {
num += this.temp[i];
}
if (num >= 7) {
item.removeEventListener('click', clickHandler);
this.gameOver();
}
}
}
item.addEventListener('click', clickHandler)
this.box.append(item);
}
}
}
//从这里开始分析
let len = Math.ceil(this.source.length / 2);
this.source.forEach((item, index) => {
let div = this.create('div');
div.classList.add('ew-box-item')
div.setAttribute('index', item.index);
div.style.backgroundImage = `url(${item.src})`;
div.style.position = 'absolute';
div.style.top = 0;
if (index > len) {
div.style.right = `${(5 * (index - len)) / 100}rem`;
this.rightSource.appendChild(div);
} else {
div.style.left = `${(5 * index) / 100}rem`;
this.leftSource.appendChild(div)
}
const clickHandler = () => {
if(div.parentElement.className === 'ew-collection'){
return;
}
const currentIndex = div.getAttribute('index');
if (this.temp[currentIndex]) {
this.temp[currentIndex] += 1;
} else {
this.temp[currentIndex] = 1;
}
div.style.position = 'static';
this.collection.appendChild(div);
if (this.temp[currentIndex] === 3) {
this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());
this.temp[currentIndex] = 0;
}
let num = 0;
for (let i in this.temp) {
num += this.temp[i];
}
if (num >= 7) {
div.removeEventListener('click', clickHandler);
this.gameOver();
}
}
div.addEventListener('click', clickHandler);
});
this.createShadow();
}</code></pre><p>这里很显然取的是source素材列表的一般来分别生成对应的左右素材列表,同理,这里面的块元素点击事件逻辑应该是和块容器元素里面的逻辑是很相似的,所以没什么好说的。我们主要看以下这段代码:</p><pre><code class="js">let div = this.create('div');
div.classList.add('ew-box-item');
div.setAttribute('index', item.index);
div.style.backgroundImage = `url(${item.src})`;
div.style.position = 'absolute';
div.style.top = 0;
if (index > len) {
div.style.right = `${(5 * (index - len)) / 100}rem`;
this.rightSource.appendChild(div);
} else {
div.style.left = `${(5 * index) / 100}rem`;
this.leftSource.appendChild(div)
}</code></pre><p>其实这里也很好理解,也就是同样的创建块元素,这里根据index > len来确定是添加到右边素材容器元素还是左边素材元素,并且它们的top偏移量应该是一致的,主要不同在left和right而已,计算方式也很简单。</p><p>注意这里是不需要设置x,y,z属性的,因为不需要用到设置阴影的函数。</p><p>到此为止,我们一个《羊了个羊——动物版》的小游戏就完成了。</p><h2>最后</h2><p>如有兴趣可以参考<a href="https://link.segmentfault.com/?enc=9%2FjmDdJbiNG78iEx2gp%2Baw%3D%3D.LnNabbt9O6QKiFBd6MoWdvpmECo8Tem884QeoUY3npJEfQo5V7%2BuiCaazGZYNlcVhvh19wIDq5HYGPQvDPUuPxsSyVU3B7tnzO1WDKqP%2B6Y%3D" rel="nofollow">源码</a>。</p><p><a href="https://link.segmentfault.com/?enc=Hyu14RQzpuwT3wzaKyvnIg%3D%3D.sfQDoUK%2F8f2xT1L25js57P4VLK0QC6wuEVKLwQe%2F3dA%2F3jEkRmMsXAi4O7KmF30gG2zYM8%2F9nWj77q%2BZcswCkA%3D%3D" rel="nofollow">在线示例</a></p><blockquote>ps: 我这个版本的代码确实实现的不怎么样,很垃圾,推荐鱼皮大佬的<a href="https://link.segmentfault.com/?enc=31v59RJCj39Mo2jSO%2BVotA%3D%3D.08x8Z0%2BzAjEaBwBsu8bzvjBFVpzgSYfUmHpN0h5jI4wJN0ZrDNQFObTz9%2Bp3U7hV" rel="nofollow">鱼了个鱼</a>。</blockquote><p>最后谢谢大家观看,如果觉得本文有帮助到你,望不吝啬点赞和收藏,动动小手点star,嘿嘿。</p>
手写一个有点意思的电梯小程序(React版本)
https://segmentfault.com/a/1190000042322957
2022-08-15T17:48:16+08:00
2022-08-15T17:48:16+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
5
<h2>查看效果</h2><p>我们先来看一下今天要实现的示例的效果,如下所示:<br><img src="/img/bVc1Kit" alt="" title=""></p><p>好,接下来我们也看到了这个示例的效果,让我们进入正题,开始愉快的编码吧。</p><h2>技术栈介绍</h2><p>这个小程序,我们将采用React + typescript + css in js语法编写,并且采用最新比较流行的工具vite来构建。</p><h3>初始化项目</h3><p>我们可以选择在电脑按住shift,然后右键,选择powershell,也就是默认的系统终端。然后输入命令:</p><pre><code class="shell">mkdir react-elevator</code></pre><p>创建一个目录,创建好之后,接着我们在vscode中打开这个目录,打开之后,在vscode中打开终端,输入以下命令:</p><pre><code class="shell">npm init vite@latest react-elevator -- --template react-ts</code></pre><p>注意在命令界面,我们要选择react,react-ts。初始化项目好了之后,我们在输入命令:</p><pre><code class="shell">cd react-elevator
npm install
npm run dev</code></pre><p>查看一下我们初始化项目是否成功。</p><blockquote>特别声明: 请注意安装了node.js和npm工具</blockquote><h3>css in js</h3><p>可以看到,我们的项目初始化已经完成,好,接下来,我们还要额外的装一些项目当中遇到的依赖,例如css in js,我们需要安装@emotion/styled,@emotion/react依赖。继续输入命令:</p><pre><code class="shell">npm install @emotion/styled @emotion/react --save-dev</code></pre><p>安装好之后,我们在项目里面使用一下该语法。</p><p>首先引入styled,如下:</p><pre><code class="tsx">import styled from "@emotion/styled"</code></pre><p>接着创建一个样式组件,css in js实际上就是把每个组件当成一个样式组件,我们可以通过styled后面跟html标签名,然后再跟模板字符串,结构如下:</p><pre><code class="tsx">const <组件名> = styled.<html标签名>`
//这里写样式代码
`</code></pre><p>例如:</p><pre><code class="tsx">const Link = styled.a`
color:#fff;
`</code></pre><p>以上代码就是写一个字体颜色为白色的超链接组件,然后我们就可以在jsx当中直接写link组件。如下所示:</p><pre><code class="tsx"><div>
<Link>这是一个超链接组件</Link>
</div></code></pre><p>当然emotion还支持对象写法,但是我们这里基本上只用模板字符串语法就够了。</p><p>接下来步入正题,我们首先删除初始化的一些代码,因为我们没有必要用到。</p><h3>分析程序的结构</h3><p>好删除之后,我们接下来看一下我们要实现的电梯小程序的结构:</p><ol><li>电梯井(也就是电梯上升或者下降的地方)</li><li>电梯</li><li>电梯门(分为左右门)</li><li>楼层<br>4.1 楼层数<br>4.2 楼层按钮(包含上升和下降按钮)</li></ol><p>结构好了之后,接下来我们来看看有哪些功能:</p><ol><li>点击楼层,催动电梯上升或者下降</li><li>电梯到达对应楼层,电梯左右门打开</li><li>门打开之后,里面的美女就出来啦</li><li>按钮会有一个点击选中的效果</li></ol><p>我们先来分析结构,根据以上的拆分,我们可以大致将整个小程序分成如下几个组件:</p><ol><li>楼房(容器组件)</li><li>电梯井组件<br>2.1 电梯组件<br> 2.1.1 电梯左边的门<br> 2.1.1 电梯右边的门</li><li>楼层组件<br>3.1 楼层控制组件<br> 3.1.1 楼层上升按钮组件<br> 3.1.2 楼层下降按钮组件<br>3.2 楼层数组件</li></ol><p>我们先来写好组件和样式,然后再完成功能。</p><h3>楼房组件</h3><p>首先是我们的楼房组件,我们新建一个components目录,再新建一个ElevatorBuild.tsx组件,里面写上如下代码:</p><pre><code class="tsx">import styled from "@emotion/styled"
const StyleBuild = styled.div`
width: 350px;
max-width: 100%;
min-height: 500px;
border: 6px solid var(--elevatorBorderColor--);
overflow: hidden;
display: flex;
margin: 3vh auto;
`
const ElevatorBuild = () => {
return (
<StyleBuild></StyleBuild>
)
}
export default ElevatorBuild</code></pre><p>这样,我们的一个楼房组件就算是完成了,然后我们在App.tsx当中引入,并使用它:</p><pre><code class="tsx">//这里是新增的代码
import ElevatorBuild from "./components/ElevatorBuild"
const App = () => (
<div className="App">
{/*这里是新增的代码 */}
<ElevatorBuild />
</div>
)
export default App
</code></pre><h3>全局样式</h3><p>在这里,我们定义了全局css变量样式,因此在当前目录下创建global.css,并在main.tsx中引入,然后在该样式文件中写上如下代码:</p><pre><code class="css">:root {
--elevatorBorderColor--: rgba(0,0,0.85);
--elevatorBtnBgColor--: #fff;
--elevatorBtnBgDisabledColor--: #898989;
--elevatorBtnDisabledColor--: #c2c3c4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}</code></pre><h3>电梯井组件</h3><p>接下来,让我们继续完成电梯井组件,同样在components目录下新建一个ElevatorShaft.tsx组件,里面写上如下代码:</p><pre><code class="tsx">import styled from "@emotion/styled"
const StyleShaft = styled.div`
width: 200px;
position: relative;
border-right: 2px solid var(--elevatorBorderColor--);
padding: 1px;
`
const ElevatorShaft = () => {
return (
<StyleShaft></StyleShaft>
)
}
export default ElevatorShaft</code></pre><p>然后我们在楼房组件中引入并使用它,如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled"
//这里是新增的代码
import ElevatorShaft from "./ElevatorShaft"
const StyleBuild = styled.div`
width: 350px;
max-width: 100%;
min-height: 500px;
border: 6px solid var(--elevatorBorderColor--);
overflow: hidden;
display: flex;
margin: 3vh auto;
`
const ElevatorBuild = () => {
return (
<StyleBuild>
{/*这里是新增的代码 */}
<ElevatorShaft></ElevatorShaft>
</StyleBuild>
)
}
export default ElevatorBuild</code></pre><h3>电梯门组件</h3><p>接着我们来完成电梯门组件,我们可以看到电梯门组件有一些公共的样式部分,所以我们可以抽取出来,新建一个Door.tsx,写上如下代码:</p><pre><code class="tsx">
import styled from '@emotion/styled';
const StyleDoor = styled.div`
width:50%;
position: absolute;
top: 0;
height: 100%;
background-color: var(--elevatorBorderColor--);
border: 1px solid var(--elevatorBtnBgColor--);
`;
const StyleLeftDoor = styled(StyleDoor)`
left: 0;
`;
const StyleRightDoor = styled(StyleDoor)`
right: 0;
`;
export { StyleLeftDoor,StyleRightDoor }</code></pre><p>由于我们功能会需要设置这两个组件的样式,并且我们这个样式是设置在style属性上的,因此我们可以通过props来传递,现在我们先写好typescript接口类,创建一个type目录,新建style.d.ts全局接口文件,并写上如下代码:</p><pre><code class="tsx">export interface StyleProps {
style: CSSProperties
}</code></pre><h3>电梯组件</h3><p>接下来,我们就可以开始写电梯组件,如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled"
const StyleElevator = styled.div`
height: 98px;
background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
border: 1px solid var(--elevatorBorderColor--);
width: calc(100% - 2px);
padding: 1px;
transition-timing-function: ease-in-out;
position: absolute;
left: 1px;
bottom: 1px;
`
const Elevator = (props: Partial<ElevatorProps>) => {
return (
<StyleElevator>
</StyleElevator>
)
}
export default Elevator</code></pre><p>接下来,我们来看两个电梯门组件,首先是左边的门,如下所示:</p><pre><code class="tsx">import { StyleProps } from "../type/style"
import { StyleLeftDoor } from "./Door"
const ElevatorLeftDoor = (props: Partial<StyleProps>) => {
const { style } = props
return (
<StyleLeftDoor style={style}></StyleLeftDoor>
)
}
export default ElevatorLeftDoor</code></pre><p>Partial是一个泛型,传入接口,代表将接口的每个属性变成可选属性,根据这个原理,我们可以得知右边门的组件代码也很类似。如下:</p><pre><code class="tsx">import { StyleProps } from '../type/style';
import { StyleRightDoor } from './Door'
const ElevatorRightDoor = (props: Partial<StyleProps>) => {
const { style } = props;
return (
<StyleRightDoor style={style}/>
)
}
export default ElevatorRightDoor;</code></pre><p>这两个组件写好之后,我们接下来要在电梯组件里引入并使用它们,由于功能逻辑会需要设置样式,因此,我们通过props再次传递style。如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"
const StyleElevator = styled.div`
height: 98px;
background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
border: 1px solid var(--elevatorBorderColor--);
width: calc(100% - 2px);
padding: 1px;
transition-timing-function: ease-in-out;
position: absolute;
left: 1px;
bottom: 1px;
`
export interface ElevatorProps {
leftDoorStyle: StyleProps['style'];
rightDoorStyle: StyleProps['style'];
}
const Elevator = (props: Partial<ElevatorProps>) => {
const { leftDoorStyle,rightDoorStyle } = props;
return (
<StyleElevator>
<ElevatorLeftDoor style={leftDoorStyle} />
<ElevatorRightDoor style={rightDoorStyle} />
</StyleElevator>
)
}
export default Elevator</code></pre><p>完成了电梯组件之后,接下来我们在电梯井组件里面引入电梯组件,注意这里后续逻辑我们会设置电梯组件和电梯门组件的样式,因此在电梯井组件中,我们需要通过props传递样式。</p><pre><code class="tsx">import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import Elevator from "./Elevator"
const StyleShaft = styled.div`
width: 200px;
position: relative;
border-right: 2px solid var(--elevatorBorderColor--);
padding: 1px;
`
export interface ElevatorProps {
leftDoorStyle: StyleProps['style'];
rightDoorStyle: StyleProps['style'];
elevatorStyle: StyleProps['style'];
}
const ElevatorShaft = (props: Partial<ElevatorProps>) => {
const { leftDoorStyle,rightDoorStyle,elevatorStyle } = props;
return (
<StyleShaft>
<Elevator style={elevatorStyle} leftDoorStyle={leftDoorStyle} rightDoorStyle={rightDoorStyle}></Elevator>
</StyleShaft>
)
}
export default ElevatorShaft</code></pre><h3>电梯门组件的开启动画</h3><p>我们可以看到,当到达一定时间,电梯门会有开启动画,这里我们显然没有加上,所以我们可以为电梯门各自加一个是否开启的props用来传递,继续修改Door.tsx如下:</p><pre><code class="tsx">
import styled from '@emotion/styled';
const StyleDoor = styled.div`
width:50%;
position: absolute;
top: 0;
height: 100%;
background-color: var(--elevatorBorderColor--);
border: 1px solid var(--elevatorBtnBgColor--);
`;
const StyleLeftDoor = styled(StyleDoor)<{ toggle?:boolean }>`
left: 0;
${({toggle}) => toggle ? 'animation: doorLeft 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' }
@keyframes doorLeft {
0% {
left: 0px;
}
25% {
left: -90px;
}
50% {
left: -90px;
}
100% {
left:0;
}
}
`;
const StyleRightDoor = styled(StyleDoor)<{ toggle?:boolean }>`
right: 0;
${({toggle}) => toggle ? 'animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' };
@keyframes doorRight {
0% {
right: 0px;
}
25% {
right: -90px;
}
50% {
right: -90px;
}
100% {
right:0;
}
}
`;
export { StyleLeftDoor,StyleRightDoor }</code></pre><p>emotion语法可以通过函数来返回一个css属性,从而达到动态设置属性的目的,一对尖括号,其实也就是typescript中的泛型,代表是否传入toggle数据,接下来修改ElevatorLeftDoor.tsx和ElevatorRightDoor.tsx。如下:</p><pre><code class="tsx">import { StyleProps } from "../type/style";
import { StyleLeftDoor } from "./Door"
export interface ElevatorLeftDoorProps extends StyleProps {
toggle: boolean
}
const ElevatorLeftDoor = (props: Partial<ElevatorLeftDoorProps>) => {
const { style,toggle } = props;
return (
<StyleLeftDoor style={style} toggle={toggle}></StyleLeftDoor>
)
}
export default ElevatorLeftDoor</code></pre><pre><code class="tsx">import { StyleProps } from '../type/style'
import { StyleRightDoor } from './Door'
export interface ElevatorRightDoorProps extends StyleProps {
toggle: boolean
}
const ElevatorRightDoor = (props: Partial<ElevatorRightDoorProps>) => {
const { style,toggle } = props;
return (
<StyleRightDoor style={style} toggle={toggle} />
)
}
export default ElevatorRightDoor</code></pre><h3>修改电梯和电梯井组件</h3><p>同样的我们也需要修改电梯组件和电梯井组件,如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"
const StyleElevator = styled.div`
height: 98px;
background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
border: 1px solid var(--elevatorBorderColor--);
width: calc(100% - 2px);
padding: 1px;
transition-timing-function: ease-in-out;
position: absolute;
left: 1px;
bottom: 1px;
`
export interface ElevatorProps extends StyleProps {
leftDoorStyle: StyleProps['style']
rightDoorStyle: StyleProps['style']
leftToggle: boolean
rightToggle: boolean
}
const Elevator = (props: Partial<ElevatorProps>) => {
const { leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } = props;
return (
<StyleElevator>
<ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
<ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
</StyleElevator>
)
}
export default Elevator</code></pre><pre><code class="tsx">import styled from "@emotion/styled";
import { StyleProps } from "../type/style";
import Elevator from "./Elevator";
const StyleShaft = styled.div`
width: 200px;
position: relative;
border-right: 2px solid var(--elevatorBorderColor--);
padding: 1px;
`;
export interface ElevatorProps {
leftDoorStyle: StyleProps["style"];
rightDoorStyle: StyleProps["style"];
elevatorStyle: StyleProps["style"];
leftToggle: boolean;
rightToggle: boolean;
}
const ElevatorShaft = (props: Partial<ElevatorProps>) => {
const {
leftDoorStyle,
rightDoorStyle,
elevatorStyle,
leftToggle,
rightToggle,
} = props;
return (
<StyleShaft>
<Elevator
style={elevatorStyle}
leftDoorStyle={leftDoorStyle}
rightDoorStyle={rightDoorStyle}
leftToggle={leftToggle}
rightToggle={rightToggle}
></Elevator>
</StyleShaft>
);
};
export default ElevatorShaft;</code></pre><p>但是别忘了我们这里的电梯组件因为需要上升和下降,因此还需要设置样式,再次修改一下电梯组件的代码如下:</p><pre><code class="tsx">import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"
const StyleElevator = styled.div`
height: 98px;
background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
border: 1px solid var(--elevatorBorderColor--);
width: calc(100% - 2px);
padding: 1px;
transition-timing-function: ease-in-out;
position: absolute;
left: 1px;
bottom: 1px;
`
export interface ElevatorProps extends StyleProps {
leftDoorStyle: StyleProps['style']
rightDoorStyle: StyleProps['style']
leftToggle: boolean
rightToggle: boolean
}
const Elevator = (props: Partial<ElevatorProps>) => {
const { style,leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } = props;
return (
<StyleElevator style={style}>
<ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
<ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
</StyleElevator>
)
}
export default Elevator</code></pre><h3>楼层容器组件</h3><p>到目前为止,我们的左半边部分已经完成了,接下来,我们来完成右半边部分的楼层数和控制按钮组件,我们的楼层是动态生成的,因此我们需要一个容器组件包裹起来,先写这个楼层容器组件,如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled"
const StyleStoreyZone = styled.div`
width: auto;
height: 100%;
`
const Storey = () => {
return (
<StyleStoreyZone>
</StyleStoreyZone>
)
}
export default Storey</code></pre><h3>楼层组件</h3><p>可以看到楼层容器组件还是比较简单的,接下来我们来看楼层组件。如下所示:</p><pre><code class="tsx">import styled from "@emotion/styled";
import { createRef, useEffect, useState } from "react";
import useComponentDidMount from "../hooks/useComponentDidMount";
const StyleStorey = styled.div`
display: flex;
align-items: center;
height: 98px;
border-bottom: 1px solid var(--elevatorBorderColor--);
`;
const StyleStoreyController = styled.div`
width: 70px;
height: 98px;
padding: 8px 0;
display: inline-flex;
align-items: center;
justify-content: center;
flex-direction: column;
`;
const StyleStoreyCount = styled.div`
width: 80px;
height: 98px;
text-align: center;
font: 56px / 98px 微软雅黑, 楷体;
`;
const StyleButton = styled.button`
width: 36px;
height: 36px;
border: 1px solid var(--elevatorBorderColor--);
border-radius: 50%;
outline: none;
cursor: pointer;
background-color: var(--elevatorBtnBgColor--);
&:last-of-type {
margin-top: 8px;
}
&.checked {
background-color: var(--elevatorBorderColor--);
color: var(--elevatorBtnBgColor--);
}
&[disabled] {
cursor: not-allowed;
background-color: var(--elevatorBtnBgDisabledColor--);
color: var(--elevatorBtnDisabledColor--);
}
`;
export interface MethodProps {
onUp(v: number, t: number, h?: number): void;
onDown(v: number, t: number, h?: number): void;
}
export interface StoreyProps extends MethodProps{
count: number
}
export interface StoreyItem {
key: string
disabled: boolean
}
const Storey = (props: Partial<StoreyProps>) => {
const { count = 6 } = props;
const storeyRef = createRef<HTMLDivElement>();
const [storeyList, setStoreyList] = useState<StoreyItem []>();
const [checked, setChecked] = useState<string>();
const [type, setType] = useState<keyof MethodProps>();
const [offset,setOffset] = useState(0)
const [currentFloor, setCurrentFloor] = useState(1);
useComponentDidMount(() => {
let res: StoreyItem [] = [];
for (let i = count - 1; i >= 0; i--) {
res.push({
key: String(i + 1),
disabled: false
});
}
setStoreyList(res);
});
useEffect(() => {
if(storeyRef){
setOffset(storeyRef.current?.offsetHeight as number)
}
},[storeyRef])
const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
setChecked(key)
setType(method)
const moveFloor = count - index
const diffFloor = Math.abs(moveFloor - currentFloor)
setCurrentFloor(moveFloor)
props[method]?.(diffFloor, offset * (moveFloor - 1))
// 也许这不是一个好的方法
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
}
setTimeout(() => {
setChecked(void 0);
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
}
}, diffFloor * 1000);
};
return (
<>
{storeyList?.map((item,index) => (
<StyleStorey key={item.key} ref={storeyRef}>
<StyleStoreyController>
<StyleButton
disabled={Number(item.key) === storeyList.length || item.disabled}
onClick={() => onClickHandler(item.key,index,'onUp')}
className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
>
↑
</StyleButton>
<StyleButton
disabled={Number(item.key) === 1 || item.disabled}
onClick={() => onClickHandler(item.key,index,'onDown')}
className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
>
↓
</StyleButton>
</StyleStoreyController>
<StyleStoreyCount>{item.key}</StyleStoreyCount>
</StyleStorey>
))}
</>
);
};
export default Storey;</code></pre><p>可以看到楼层组件的逻辑非常多,但其实一项一项的分析下来也并不难。</p><p>接下来,我们在该容器组件中引入,并且将该组件在楼房组件中引入,就可以得到我们整个电梯小程序的结构了。</p><p>在这里我们来一步一步的分析楼层组件的逻辑,</p><h4>楼层数</h4><p>首先楼层是动态生成的,通过父组件传递,因此我们在props当中定义一个count,默认值是6,代表默认生成的楼层数。这也就是我们这行代码的意义:</p><pre><code class="tsx">export interface StoreyProps extends MethodProps{
count: number
}
const { count = 6 } = props;</code></pre><h4>楼层的上升与下降</h4><p>其次我们在对电梯进行上升和下降的时候,需要获取到每一层楼高,实际上也就是楼层容器元素的高度,如何获取DOM元素的实际高度?我们先想一下,如果是一个真实的DOM元素,我们只需要获取offsetHeight就行了,即:</p><pre><code class="ts">//这里的el显然是一个dom元素
const offset: number = el.offsetHeight;</code></pre><p>在react中,我们应该如何获取真实的DOM元素呢?利用ref属性,首先导入createRef方法,创建一个storeyRef,然后将该storeyRef绑定到组件容器元素上,即:</p><pre><code class="tsx">const storeyRef = createRef<HTMLDivElement>();
//...
<StyleStorey ref={storeyRef}></StyleStorey></code></pre><p>然后我们就可以使用useEffect方法,也就是react hooks中的一个生命周期钩子函数,监听这个storeyRef,如果监听到了,就可以直接拿到dom元素,并且使用一个状态来存储高度值。即:</p><pre><code class="tsx">const [offset,setOffset] = useState(0)
//...
useEffect(() => {
//storeyRef.current显然就是我们实际拿到的DOM元素
if(storeyRef){
setOffset(storeyRef.current?.offsetHeight as number)
}
},[storeyRef])</code></pre><h4>楼层列表渲染</h4><p>接下来,我们来看楼层数的动态生成,我们知道在react中动态生成列表元素,实际上就是使用数组的map方法,因此我们要根据count来生成一个数组,在这里,我们可以生成一个key数组,但是由于我们要控制按钮的禁用,因此额外添加一个disabled属性,因此这也是以下代码的意义:</p><pre><code class="tsx">export interface StoreyItem {
key: string
disabled: boolean
}
const [storeyList, setStoreyList] = useState<StoreyItem []>();
useComponentDidMount(() => {
let res: StoreyItem [] = [];
for (let i = count - 1; i >= 0; i--) {
res.push({
key: String(i + 1),
disabled: false
});
}
setStoreyList(res);
});</code></pre><p>这里涉及到一个模拟useComponentDidMount钩子函数,很简单,在hooks目录下新建一个useComponentDidMount.ts,然后写上如下代码:</p><pre><code class="ts">import { useEffect } from 'react';
const useComponentDidMount = (onMountHandler: (...args:any) => any) => {
useEffect(() => {
onMountHandler();
}, []);
};
export default useComponentDidMount;</code></pre><p>然后就是渲染楼层组件,如下:</p><pre><code class="tsx">(
<>
{storeyList?.map((item,index) => (
<StyleStorey key={item.key} ref={storeyRef}>
<StyleStoreyController>
<StyleButton
disabled={Number(item.key) === storeyList.length || item.disabled}
onClick={() => onClickHandler(item.key,index,'onUp')}
className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
>
↑
</StyleButton>
<StyleButton
disabled={Number(item.key) === 1 || item.disabled}
onClick={() => onClickHandler(item.key,index,'onDown')}
className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
>
↓
</StyleButton>
</StyleStoreyController>
<StyleStoreyCount>{item.key}</StyleStoreyCount>
</StyleStorey>
))}
</>
);</code></pre><p><code><></></code>是React.Fragment的一种写法,可以理解它就是一个占位标签,没有什么实际含义,storeyList默认值是undefined,因此需要加?代表可选链,这里包含了两个部分,第一个部分就是控制按钮,第二部分就是显示楼层数。</p><p>实际上我们生成的元素数组中的key就是楼层数,这也是这行代码的意义:</p><pre><code class="tsx"><StyleStoreyCount>{item.key}</StyleStoreyCount></code></pre><p>还有就是react在生成列表的时候,需要绑定一个key属性,方便虚拟DOM,diff算法的计算,这里不用多讲。接下来我们来看按钮组件的逻辑。</p><h4>楼层按钮组件</h4><p>按钮组件的逻辑包含三个部分:</p><ol><li>禁用效果</li><li>点击使得电梯上升和下降</li><li>选中效果</li></ol><p>我们知道最高楼的上升是无法上升的,所以需要禁用,同样的,底楼的下降也是需要禁用的,所以这两行代码就是这个意思:</p><pre><code class="tsx">Number(item.key) === storeyList.length
Number(item.key) === 1</code></pre><p>接下来还有一个条件,这个item.disabled其实主要是防止重复点击的问题,当然这并不是一个好的解决办法,但目前来说我们先这样做,定义一个type状态,代表是点击的上升还是下降:</p><pre><code class="tsx">//接口类型,type应只能是onUp或者onDown,代表只能是上升还是下降
export interface MethodProps {
onUp(v: number, t: number, h?: number): void;
onDown(v: number, t: number, h?: number): void;
}
const [type, setType] = useState<keyof MethodProps>();</code></pre><p>然后定义一个checked状态,代表当前按钮是否选中:</p><pre><code class="tsx">//checked存储key值,所以
const [checked, setChecked] = useState<string>();
className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}</code></pre><p>需要注意的就是这里的判断:</p><pre><code class="tsx">item.key === checked && type === 'onUp' //以及 ${item.key === checked && type === 'onDown'</code></pre><p>我们样式当中是添加了checked的,这个没什么好说的。</p><p>然后,我们还需要缓存当前楼层是哪一楼,因为下次点击的时候,我们就需要根据当前楼层来计算,而不是重头开始。</p><pre><code class="tsx">const [currentFloor, setCurrentFloor] = useState(1);</code></pre><p>最后,就是我们的点击上升和下降逻辑,还是有点复杂的:</p><pre><code class="tsx">const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
setChecked(key)
setType(method)
const moveFloor = count - index
const diffFloor = Math.abs(moveFloor - currentFloor)
setCurrentFloor(moveFloor)
props[method]?.(diffFloor, offset * (moveFloor - 1))
// 也许这不是一个好的方法
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
}
setTimeout(() => {
setChecked(void 0);
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
}
}, diffFloor * 1000);
};</code></pre><p>该函数有三个参数,第一个代表当前楼层的key值,也就是楼层数,第二个代表当前楼的索引,注意索引和楼层数是不一样的,第三个就是点击的是上升还是下降。我们的第一个参数和第三个参数是用来设置按钮的选中效果,即:</p><pre><code class="tsx">setChecked(key)
setType(method)</code></pre><p>接下来,我们需要计算动画的执行时间,例如我们从第一层到第五层,如果按每秒到一层来计算,那么第一层到第五层就需要4s的时间,同理我们的偏移量就应该是每层楼高与需要移动的楼高在减去1。因此,计算需要移动的楼高我们是:</p><pre><code class="tsx">const moveFloor = count - index
const diffFloor = Math.abs(moveFloor - currentFloor)
//设置当前楼层
setCurrentFloor(moveFloor)
props[method]?.(diffFloor, offset * (moveFloor - 1))</code></pre><p>注意我们是将事件抛给父组件的,因为我们的电梯组件和楼层容器组件在同一层级,只有这样,才能设置电梯组件的样式。即:</p><pre><code class="tsx">//传入两个参数,代表动画的执行时间和偏移量,props[method]其实就是动态获取props中的属性
props[method]?.(diffFloor, offset * (moveFloor - 1))</code></pre><p>然后这里的逻辑就是防止重复点击的代码,这并不是一个好的解决方式:</p><pre><code class="tsx">if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
}
setTimeout(() => {
//...
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
}
}, diffFloor * 1000);</code></pre><h4>修改楼层容器组件</h4><p>好了,这个组件的分析就到此为止了,我们既然把事件抛给了父组件,因此我们还需要修改一下它的父组件StoreyZone.tsx的代码,如下:</p><pre><code class="tsx">import styled from "@emotion/styled";
import Storey from "./Storey";
const StyleStoreyZone = styled.div`
width: auto;
height: 100%;
`;
export interface StoreyZoneProps {
onUp(v: number, h?: number): void;
onDown(v: number, h?: number): void;
}
const StoreyZone = (props: Partial<StoreyZoneProps>) => {
const { onUp, onDown } = props;
return (
<StyleStoreyZone>
<Storey
onUp={(k: number, h: number) => onUp?.(k, h)}
onDown={(k: number, h: number) => onDown?.(k, h)}
/>
</StyleStoreyZone>
);
};
export default StoreyZone;</code></pre><p>然后就是最后的ElevatorBuild.tsx组件的修改,如下:</p><pre><code class="tsx">import styled from "@emotion/styled";
import { useState } from "react";
import { StyleProps } from "../type/style";
import ElevatorShaft from "./ElevatorShaft";
import StoreyZone from "./StoreyZone";
const StyleBuild = styled.div`
width: 350px;
max-width: 100%;
min-height: 500px;
border: 6px solid var(--elevatorBorderColor--);
overflow: hidden;
display: flex;
margin: 3vh auto;
`;
const ElevatorBuild = () => {
const [elevatorStyle, setElevatorStyle] = useState<StyleProps["style"]>();
const [doorStyle, setDoorStyle] = useState<StyleProps["style"]>();
const [open,setOpen] = useState(false)
const move = (diffFloor: number, offset: number) => {
setElevatorStyle({
transitionDuration: diffFloor + 's',
bottom: offset,
});
setOpen(true)
setDoorStyle({
animationDelay: diffFloor + 's'
});
setTimeout(() => {
setOpen(false)
},diffFloor * 1000 + 3000)
};
return (
<StyleBuild>
<ElevatorShaft
elevatorStyle={elevatorStyle}
leftDoorStyle={doorStyle}
rightDoorStyle={doorStyle}
leftToggle={open}
rightToggle={open}
></ElevatorShaft>
<StoreyZone onUp={(k: number,h: number) => move(k,h)} onDown={(k: number,h: number) => move(k,h)}></StoreyZone>
</StyleBuild>
);
};
export default ElevatorBuild;</code></pre><p>最后,我们来检查一下代码,看看还有没有什么可以优化的地方,可以看到我们的按钮禁用逻辑是可以复用的,我们再重新创建一个函数,即:</p><pre><code class="tsx">const changeButtonDisabled = (key:string,status: boolean) => {
if(+key !== storeyList?.length && +key !== 1){
setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: status })))
}
}
const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
//...
changeButtonDisabled(key,true)
setTimeout(() => {
//...
changeButtonDisabled(key,false)
}, diffFloor * 1000);
};</code></pre><p>到此为止,我们的一个电梯小程序就算是完成了,也不算是复杂,总结一下我们学到的知识点:</p><ol><li>css in js 我们使用的是@emotion这个库</li><li>父子组件的通信,使用props</li><li>操作DOM,使用ref</li><li>组件内的状态通信,使用useState,以及如何修改状态,有两种方式</li><li>钩子函数useEffect</li><li>类名的操作与事件还有就是样式的设置</li><li>React列表渲染</li><li>typescript接口的定义,以及一些内置的类型</li></ol><h2>最后</h2><p>当然这里我们还可以扩展,比如楼层数的限制,再比如添加门开启后,里面的美女真的走出来的动画效果,如有兴趣可以参考<a href="https://link.segmentfault.com/?enc=czCknDyiIi23LadIQ5fPXQ%3D%3D.oasWJf75KH9SPM2OU2WvsEAarijbP7MX6Kfqbe1p6eE1ZIp4a3hnh1GrPnmwWLOT6kzXN4zgeqgqi5SpIfVXquAnFFhmcQC%2FckKqjkyRJOM%3D" rel="nofollow">源码</a>自行扩展。</p><p><a href="https://link.segmentfault.com/?enc=9sHWJ3Msz7O4P33%2BYFhxpQ%3D%3D.rAq9tu01GnpONUYLJLyekCyJquO5RouPRfBSf0BD5YDaI5irrXWwhhNeCWdj%2BUntTjrToMrGUOxI%2FtQcFRd%2B8Q%3D%3D" rel="nofollow">在线示例</a></p><p>最后谢谢大家观看,如果觉得本文有帮助到你,望不吝啬点赞和收藏,动动小手点star,嘿嘿。</p><blockquote>特别声明: 这个小示例只适合新手学习,大佬就算了,这个小程序对大佬来说很简单。</blockquote>
手写一个有点意思的电梯小程序
https://segmentfault.com/a/1190000042298118
2022-08-10T15:40:47+08:00
2022-08-10T15:40:47+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
10
<h2>通过本示例,你将学到什么?</h2><h3>CSS 相关</h3><ol><li>CSS定位</li><li>CSS弹性盒子布局</li><li>CSS动画</li><li>CSS变量的用法</li></ol><h3>js相关</h3><ol><li>类的封装</li><li>DOM的操作与事件的操作</li><li>样式与类的操作</li><li>延迟函数的使用</li></ol><h2>示例效果</h2><p>废话少说,先看效果如图所示:</p><p><img src="/img/bVc1DPL" alt="" title=""></p><h2>分析小程序的构成</h2><ol><li>电梯井(也就是电梯上升或者下降的地方)</li><li>电梯</li><li>电梯门(分为左右门)</li><li>楼层<br>4.1 楼层数<br>4.2 楼层按钮(包含上升和下降按钮)</li></ol><h2>有哪些功能</h2><ol><li>点击楼层,催动电梯上升或者下降</li><li>电梯到达对应楼层,电梯左右门打开</li><li>门打开之后,里面的美女就出来啦</li><li>提示信息: 本美女就要出来了,请速速来迎接</li><li>按钮会有一个点击选中的效果</li></ol><p>根据以上的分析,我们就可以很好的实现电梯小程序啦,接下来让我们进入编码阶段吧。</p><blockquote>PS: 这里的楼层数是动态生层的,不过建议值不要设置太大,可以在代码里做限制。</blockquote><h2>准备工作</h2><p>创建一个index.html文件,并初始化HTML5文档的基本结构,如下所示:</p><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>elevator</title>
<link rel="stylesheet" href="./styles/style.css">
</head>
<body>
<!--这里写代码-->
</body>
<script src="https://www.eveningwater.com/static/plugin/message.min.js"></script>
<script src="./scripts/index.js"></script>
<script>
//这里写代码
</script>
</html></code></pre><p>创建一个styles目录并创建一个style.css文件,初始化代码如下:</p><pre><code class="css">//色彩变量
:root {
--elevatorBorderColor--: rgba(0,0,0.85);
--elevatorBtnBgColor--: #fff;
--elevatorBtnBgDisabledColor--: #898989;
--elevatorBtnDisabledColor--: #c2c3c4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}</code></pre><h2>如何调用</h2><p>我们是通过将功能封装在一个Elevator类当中,最终调用如下所示:</p><pre><code class="js">//6代表楼层数,不建议设置太大
new Elevator(6);</code></pre><p>初始化工作完成,接下来让我们来看看具体的实现吧。</p><blockquote>特别申明一下: 我们在这里使用了弹性盒子布局,请注意浏览器的兼容性问题。</blockquote><h2>代码实现</h2><h3>构建楼房</h3><p>首先我们需要一个容器元素,代表是当前的包含电梯的楼房或者是建筑,HTML代码如下:</p><pre><code class="html"><div class="ew-elevator-building"></div></code></pre><p>建筑的样式很简单,固定宽,并设置最小高度(因为楼层数是动态生成的,不固定的,所以高度不能够固定),然后设置边框,其它就没有什么了。代码如下:</p><pre><code class="css">.ew-elevator-building {
width: 350px;
max-width: 100%;
min-height: 500px;
border: 6px solid var(--elevatorBorderColor--);
margin: 3vh auto;
overflow: hidden;
display: flex;
}</code></pre><h3>构建电梯井</h3><p>接下来是电梯井,电梯井又包含电梯和电梯左右门,因此HTML文档结构就出来了,如下所示:</p><pre><code class="html"><!--电梯井-->
<div class="ew-elevator-shaft">
<!--电梯-->
<div class="ew-elevator">
<!--电梯左右门-->
<div class="ew-elevator-left-door ew-elevator-door"></div>
<div class="ew-elevator-right-door ew-elevator-door"></div>
</div>
</div></code></pre><p>根据效果图,我们可以很快速的写出样式代码,如下所示:</p><pre><code class="css">//电梯井,主要就是设置相对定位和边框,固定宽度
.ew-elevator-shaft {
border-right: 2px solid var(--elevatorBorderColor--);
width: 200px;
padding: 1px;
position: relative;
}</code></pre><h3>构建电梯</h3><p>我们来思考一下这里为什么要使用相对定位,因为我们的电梯是上升和下降的,我们可以通过绝对定位加bottom或者top的偏移量来模拟电梯的上升和下降,因此电梯我们就需要设置为绝对定位,而电梯是相对于电梯井偏移的,所以需要设置为相对定位。继续编写样式代码:</p><pre><code class="css">.ew-elevator {
height: 98px;
width: calc(100% - 2px);
background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
border: 1px solid var(--elevatorBorderColor--);
padding: 1px;
transition-timing-function: ease-in-out;
position: absolute;
left: 1px;
bottom: 1px;
}</code></pre><h3>构建电梯门</h3><p>可以看到,默认电梯就在第一层,所以bottom偏移量就是1px,并且我们设置一个背景图,代表是里面的人,电梯门开启后就显示该背景图。接下来是电梯门的样式:</p><pre><code class="css">.ew-elevator-door {
position: absolute;
width: 50%;
height: 100%;
background-color: var(--elevatorBorderColor--);
border: 1px solid var(--elevatorBtnBgColor--);
top: 0;
}
.ew-elevator-left-door {
left: 0;
}
.ew-elevator-right-door {
right: 0;
}</code></pre><p>其实也很好理解,就是左右门各占一步,左边的电梯门居左,右边的电梯门居右。接下来是给电梯开门添加动画,添加一个toggle类名就行了:</p><pre><code class="css">.ew-elevator-left-door.toggle {
animation: doorLeft 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.ew-elevator-right-door.toggle {
animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);
}
@keyframes doorLeft {
0% {
left: 0px;
}
25% {
left: -90px;
}
50% {
left: -90px;
}
100% {
left:0;
}
}
@keyframes doorRight {
0% {
right: 0px;
}
25% {
right: -90px;
}
50% {
right: -90px;
}
100% {
right:0;
}
}</code></pre><p>很显然电梯左边的门是往左边偏移,电梯右边的门是往右边偏移。</p><h3>构建楼层</h3><p>接下来是楼层的样式,楼层也包含了两个部分,第一个就是电梯按钮控制,第二个就是楼层数。因此HTML文档结构如下:</p><pre><code class="html"><div class="ew-elevator-storey-zone">
<!--这一块就是楼层,因为是动态生成的,但是我们可以先写第一个,然后写好样式-->
<div class="ew-elevator-storey">
<!--电梯按钮,包含上升按钮和下降按钮-->
<div class="ew-elevator-controller">
<button type="button" class="ew-elevator-to-top ew-elevator-btn">↑</button>
<button type="button" class="ew-elevator-to-bottom ew-elevator-btn">↓</button>
</div>
<!--楼层数-->
<div class="ew-elevator-count">1</div>
</div>
</div></code></pre><p>楼层容器和每个楼层元素的样式很简单没什么可以说的,如下:</p><pre><code class="css">//楼层容器元素
.ew-elevator-storey-zone {
width: auto;
height: 100%;
}
//楼层元素
.ew-elevator-storey {
display: flex;
align-items: center;
height: 98px;
border-bottom: 1px solid var(--elevatorBorderColor--);
}</code></pre><h3>构建电梯按钮</h3><p>接下来是电梯按钮的容器元素,如下所示:</p><pre><code class="css">.ew-elevator-controller {
width: 70px;
height: 98px;
padding: 8px 0;
display: inline-flex;
align-items: center;
justify-content: center;
flex-direction: column;
}</code></pre><p>都是常规的样式,比如弹性盒子的垂直水平居中,与列换行,即便是按钮元素也没有什么好说的。</p><pre><code class="css">//电梯按钮
.ew-elevator-btn {
width: 36px;
height: 36px;
border: 1px solid var(--elevatorBorderColor--);
border-radius: 50%;
outline: none;
cursor: pointer;
background-color: var(--elevatorBtnBgColor--);
}
//需要给按钮添加一个选中样式效果
.ew-elevator-btn.checked {
background-color: var(--elevatorBorderColor--);
color:var(--elevatorBtnBgColor--);
}
//加点上间距
.ew-elevator-btn:last-of-type {
margin-top: 8px;
}
//按钮禁用
.ew-elevator-btn[disabled] {
cursor: not-allowed;
background-color: var(--elevatorBtnBgDisabledColor--);
color: var(--elevatorBtnDisabledColor--);
}
//楼层数样式
.ew-elevator-count {
width: 80px;
height: 98px;
text-align: center;
font: 56px / 98px "微软雅黑","楷体";
}</code></pre><h3>js代码</h3><h4>初始化类</h4><p>html文档和CSS布局就到此为止了,接下来才是重头戏,也就是功能逻辑的实现,首先我们定义一个类叫Elevator,并初始化它的一些属性,代码如下:</p><pre><code class="js">class Elevator {
// 参数代表楼层数
constructor(count){
//总楼层数缓存下来
this.count = count;
//当前楼层索引
this.onFloor = 1;
//电梯按钮组
this.btnGroup = null;
//动态生成楼层数的容器元素
this.zoneContainer = this.$(".ew-elevator-storey-zone");
//电梯元素
this.elevator = this.$(".ew-elevator");
}
}</code></pre><h4>添加获取DOM的工具方法</h4><p>初始化工作完成后,我们需要添加一些工具方法,例如获取DOM元素,我们采用document.querySelector与document.querySelectorAll方法,我们将这两个方法封装一下,于是变成如下的代码:</p><pre><code class="js">class Elevator {
// 参数代表楼层数
constructor(count){
//总楼层数缓存下来
this.count = count;
//当前楼层索引
this.onFloor = 1;
//电梯按钮组
this.btnGroup = null;
//动态生成楼层数的容器元素
this.zoneContainer = this.$(".ew-elevator-storey-zone");
//电梯元素
this.elevator = this.$(".ew-elevator");
}
//这里是封装的代码
$(selector, el = document) {
return el.querySelector(selector);
}
$$(selector, el = document) {
return el.querySelectorAll(selector);
}
}</code></pre><h4>动态生成楼层数</h4><p>接下来,我们要根据传入的参数动态生成楼层数,在构造函数中调用它,因此代码就变成了如下:</p><pre><code class="js">class Elevator {
// 参数代表楼层数
constructor(count){
//总楼层数缓存下来
this.count = count;
//当前楼层索引
this.onFloor = 1;
//电梯按钮组
this.btnGroup = null;
//动态生成楼层数的容器元素
this.zoneContainer = this.$(".ew-elevator-storey-zone");
//电梯元素
this.elevator = this.$(".ew-elevator");
//这里添加了代码
this.generateStorey(this.count || 6);
}
//这里是封装的代码
$(selector, el = document) {
return el.querySelector(selector);
}
$$(selector, el = document) {
return el.querySelectorAll(selector);
}
//这里添加了代码
generateStorey(count){
//这里写逻辑
}
}</code></pre><p>好了,让我们思考一下,我们应该如何生成DOM元素并添加到DOM元素中,很简单,我们可以利用模板字符串的拼接,然后将拼接好的模板添加到容器元素的innerHTML中。如下:</p><p>generateStorey方法内部代码:</p><pre><code class="js"> let template = "";
for (let i = count - 1; i >= 0; i--) {
template += `
<div class="ew-elevator-storey">
<div class="ew-elevator-controller">
<button type="button" class="ew-elevator-to-top ew-elevator-btn" ${
i === count - 1 ? "disabled" : ""
}>↑</button>
<button type="button" class="ew-elevator-to-bottom ew-elevator-btn" ${
i === 0 ? "disabled" : ""
}>↓</button>
</div>
<div class="ew-elevator-count">${i + 1}</div>
</div>
`;
}
this.zoneContainer.innerHTML = template;
this.storeys = this.$$(".ew-elevator-storey", this.zoneContainer);
this.doors = this.$$(".ew-elevator-door", this.elevator);
this.btnGroup = this.$$(".ew-elevator-btn", this.zoneContainer);</code></pre><p>这里我们需要注意一点,在顶楼是没有上升的操作,因此顶楼的上升按钮需要禁用掉,而底层也没有下降的操作,底楼的下降按钮也要禁用掉,这也是以下代码的意义所在:</p><pre><code class="js">i === count - 1 ? "disabled" : "";
i === 0 ? "disabled" : "";</code></pre><h4>为按钮添加点击事件</h4><p>我们通过索引来判断,然后给按钮添加disabled属性禁用按钮点击。动态生成完成之后,我们在初始化楼层元素数组和电梯门的元素数组以及按钮元素数组。然后我们就是需要给按钮添加点击事件,继续在generateStorey方法内部添加如下一行代码:</p><pre><code class="js"> [...this.storeys].forEach((item, index) => {
this.handleClick(
this.$$(".ew-elevator-btn", item),
item.offsetHeight,
index
);
});</code></pre><p>以上代码就是获取每一层楼的电梯按钮,因为偏移量与楼层的高度有关,所以我们获取每个楼层的offsetHeight来确定bottom的偏移量,然后就是每层楼的索引,用来计算偏移量的。</p><p>继续看handleClick方法,如下:</p><pre><code class="js">handleClick(btnGroup, floorHeight, floorIndex) {
Array.from(btnGroup).forEach((btn) => {
btn.addEventListener("click", () => {
if (btn.classList.contains("checked")) {
return;
}
btn.classList.add("checked");
const currentFloor = this.count - floorIndex;
const moveFloor = currentFloor - 1;
this.elevatorMove(currentFloor, floorHeight * moveFloor);
});
});
}</code></pre><h4>电梯的上升与下降</h4><p>该方法内部实际上就是为每个按钮添加点击事件,点击首先判断是否被选中,如果选中则不执行,否则添加选中样式然后通过楼层总数减去当前点击的楼层索引在减去1得到当前需要移动的楼层数,然后调用elevatorMove方法,将当前楼层索引和移动的偏移量当做参数传入该方法。接下来我们来看elevatorMove方法。</p><pre><code class="js">elevatorMove(num, offset) {
const currentFloor = this.onFloor;
const diffFloor = Math.abs(num - currentFloor);
this.addStyles(this.elevator, {
transitionDuration: diffFloor + "s",
bottom: offset + "px",
});
Array.from(this.doors).forEach((door) => {
door.classList.add("toggle");
this.addStyles(door, {
animationDelay: diffFloor + "s",
});
});
$message.success(
`本美女就要出来了,请速速来迎接,再等${
(diffFloor * 1000 + 3000) / 1000
}s就关电梯门了!`
);
setTimeout(() => {
[...this.btnGroup].forEach((btn) => btn.classList.remove("checked"));
}, diffFloor * 1000);
setTimeout(() => {
Array.from(this.doors).forEach((door) => door.classList.remove("toggle"));
}, diffFloor * 1000 + 3000);
this.onFloor = num;
}</code></pre><h4>添加样式的工具方法</h4><p>这个方法我们做了哪些操作呢?首先我们通过楼层索引来计算动画的执行过渡时间,这里就涉及到了一个添加样式的工具方法,代码如下:</p><pre><code class="css">addStyles(el, styles) {
Object.assign(el.style, styles);
}</code></pre><p>十分简单,就是通过Object.assign将style和styles合并在一起。</p><p>添加了电梯的移动样式,我们就需要为电梯门的开启添加延迟执行时间以及<code>toggle</code>类名执行电梯门开启的动画,然后弹出提示消息<code>本美女就要出来了,请速速来迎接,再等${(diffFloor * 1000 + 3000) / 1000}s就关电梯门了!</code>,然后延迟清除按钮的选中效果,最后延迟移除开启电梯左右门的动画效果类名<code>toggle</code>,并将当前楼层设置一下,一个简单的电梯小程序就完成了。</p><h4>完整代码</h4><p>最后我们合并一下代码,完整的js代码就完成了:</p><pre><code class="js">class Elevator {
constructor(count) {
this.count = count;
this.onFloor = 1;
this.btnGroup = null;
this.zoneContainer = this.$(".ew-elevator-storey-zone");
this.elevator = this.$(".ew-elevator");
this.generateStorey(this.count || 6);
}
$(selector, el = document) {
return el.querySelector(selector);
}
$$(selector, el = document) {
return el.querySelectorAll(selector);
}
generateStorey(count) {
let template = "";
for (let i = count - 1; i >= 0; i--) {
template += `
<div class="ew-elevator-storey">
<div class="ew-elevator-controller">
<button type="button" class="ew-elevator-to-top ew-elevator-btn" ${
i === count - 1 ? "disabled" : ""
}>↑</button>
<button type="button" class="ew-elevator-to-bottom ew-elevator-btn" ${
i === 0 ? "disabled" : ""
}>↓</button>
</div>
<div class="ew-elevator-count">${i + 1}</div>
</div>
`;
}
this.zoneContainer.innerHTML = template;
this.storeys = this.$$(".ew-elevator-storey", this.zoneContainer);
this.doors = this.$$(".ew-elevator-door", this.elevator);
this.btnGroup = this.$$(".ew-elevator-btn", this.zoneContainer);
[...this.storeys].forEach((item, index) => {
this.handleClick(
this.$$(".ew-elevator-btn", item),
item.offsetHeight,
index
);
});
}
handleClick(btnGroup, floorHeight, floorIndex) {
Array.from(btnGroup).forEach((btn) => {
btn.addEventListener("click", () => {
if (btn.classList.contains("checked")) {
return;
}
btn.classList.add("checked");
const currentFloor = this.count - floorIndex;
const moveFloor = currentFloor - 1;
this.elevatorMove(currentFloor, floorHeight * moveFloor);
});
});
}
elevatorMove(num, offset) {
const currentFloor = this.onFloor;
const diffFloor = Math.abs(num - currentFloor);
this.addStyles(this.elevator, {
transitionDuration: diffFloor + "s",
bottom: offset + "px",
});
Array.from(this.doors).forEach((door) => {
door.classList.add("toggle");
this.addStyles(door, {
animationDelay: diffFloor + "s",
});
});
$message.success(
`本美女就要出来了,请速速来迎接,再等${
(diffFloor * 1000 + 3000) / 1000
}s就关电梯门了!`
);
setTimeout(() => {
[...this.btnGroup].forEach((btn) => btn.classList.remove("checked"));
}, diffFloor * 1000);
setTimeout(() => {
Array.from(this.doors).forEach((door) => door.classList.remove("toggle"));
}, diffFloor * 1000 + 3000);
this.onFloor = num;
}
addStyles(el, styles) {
Object.assign(el.style, styles);
}
}</code></pre><p>然后我们就可以实例化这个类了,如下所示:</p><pre><code class="js">new Elevator(6);</code></pre><h3>最后</h3><p>当然这里我们还可以扩展,比如楼层数的限制,再比如添加门开启后,里面的美女真的走出来的动画效果,如有兴趣可以参考<a href="https://link.segmentfault.com/?enc=eUlbb7tWXtiZ7ddwfDiNyg%3D%3D.zUIyOLhyk%2FC84LF1PI7AN5YAHh3bf6cDr4E2JBL%2B1zuR51CGIcZWtK%2FVOYIvDWZ5RawAEQy3lnyL5oVN6E6%2FTSBO9X8mF%2BF%2BkntPyaBI%2Btc%3D" rel="nofollow">源码</a>自行扩展。</p><p><a href="https://link.segmentfault.com/?enc=BawtpiPTo2yOE0vIYTnQ9g%3D%3D.hUR6eHCxgq05eLeCUmdcqarLvM6TllhhpUr5MDcYb8CIKY1DnMJflEGTx2uaC9pXJZqJatj6gVxNNNyXkfxZwA%3D%3D" rel="nofollow">在线示例</a></p><p>最后谢谢大家观看,如果觉得本文有帮助到你,望不吝啬点赞和收藏,动动小手点star,嘿嘿。</p><blockquote>特别声明: 这个小示例只适合新手学习,大佬就算了,这个小程序对大佬来说很简单。</blockquote>
使用React手写一个手风琴组件
https://segmentfault.com/a/1190000042181290
2022-07-16T23:35:46+08:00
2022-07-16T23:35:46+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<h2>知识点</h2><ul><li>emotion语法</li><li>react语法</li><li>css语法</li><li>typescript类型语法</li></ul><h2>效果</h2><p>让我们来看一下我们实现的效果图:</p><p><img src="/img/bVc09rw" alt="" title=""></p><h2>结构分析</h2><p>根据上图,我们来分析一下,一个手风琴组件应该包含一个手风琴容器组件和多个手风琴子元素组件。因此,假设我们实现好了所有的逻辑,并写出使用demo,那么代码应该如下:</p><pre><code class="tsx"><Accordion defaultIndex="1" onItemClick={console.log}>
<AccordionItem label="A" index="1">
Lorem ipsum
</AccordionItem>
<AccordionItem label="B" index="2">
Dolor sit amet
</AccordionItem>
</Accordion></code></pre><p>根据以上的结构,我们可以得知,首先容器组件<code>Accordion</code>会暴露一个defaultIndex属性以及一个onItemClick事件。顾名思义,defaultIndex代表默认展开的子元素组件<code>AccordionItem</code>的索引,onItemClick代表点击每一个子元素组件所触发的事件。然后,我们可以看到子元素组件有label属性和index属性,很显然,label代表当前子元素的标题,index代表当前子元素组件的索引值,而我们的Lorem ipsum就是子元素的内容。根据这些分析,我们先来实现一下AccordionItem组件。</p><h2>AccordionItem子组件</h2><p>首先我们定义好子组件的结构,函数组件写法如下:</p><pre><code class="tsx">const AccordionItem = (props) => {
//返回元素
};</code></pre><p>子元素组件分成三个部分,一个容器元素,一个标题元素和一个内容元素,因此我们可以将结构写成如下:</p><pre><code class="tsx"><div className="according-item-container">
<div className="according-item-header"></div>
<div className="according-item-content"></div>
</div></code></pre><p>知道了结构之后,我们就知道props会有哪些属性,首先是索引index属性,它的类型为string 或者number,然后是判断内容是否展开的属性isCollapsed,它的类型是布尔值,其次我们还有渲染标题的属性label,它应该是一个react节点,类型为ReactNode,同理,还有一个内容属性即children,类型也应该是ReactNode,最后就是我们要暴露的事件方法handleClick,它的类型应该是一个方法,因此我们可以定义如下的接口:</p><pre><code class="ts">interface AccordionItemType {
index: string | number;
label: string;
isCollapsed: boolean;
//SyntheticEvent代表react合成事件对象的类型
handleClick(e: SyntheticEvent): void;
children: ReactNode;
}</code></pre><p>接口定义好之后,接下来我们就在接口里面拿值(采用对象解构的方式),这些值都算是可选的,即:</p><pre><code class="tsx">const { label, isCollapsed, handleClick, children } = props;</code></pre><p>此时我们的AccordionItem子组件应该是如下:</p><pre><code class="tsx">const AccordionItem = (props: Partial<AccordionItemType>) => {
const { label, isCollapsed, handleClick, children } = props;
return (
<div className={AccordionItemContainer} onClick={handleClick}>
<div className={AccordionItemHeader}>{label}</div>
<div
aria-expanded={isCollapsed}
className={`${AccordionItemContent}${
isCollapsed ? ' collapsed' : ' expanded'
}`}
>
{children}
</div>
</div>
);
};</code></pre><p>这里我们可以使用emotion/css来写css类名样式,代码如下:</p><pre><code class="css">const baseStyle = css`
line-height: 1.5715;
`;
const AccordionItemContainer = css`
border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
baseStyle,
css`
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
padding: 12px 16px;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
transition: all 0.3s, visibility 0s;
box-sizing: border-box;
`,
);
const AccordionItemContent = css`
color: #000000d9;
background-color: #fff;
border-top: 1px solid #d9d9d9;
transition: all 0.3s ease-in-out;
padding: 16px;
&.collapsed {
display: none;
}
&.expanded {
display: block;
}
`;</code></pre><p>以上的css后面跟模板字符串再跟css样式就是emotion/css语法,<code>cx</code>也就是组合样式写法,样式都是常规的写法,也没什么好说的。这里有一个难点,那就是display:none和display:block没有过渡效果,因此可以采用visibility:hidden和opacity:0的方式来替换,但是这里为了简单,没考虑动画效果,所以也就将问题放着,后面有时间再优化。</p><p>到目前为止,这个子组件就算是完成了,这也就意味着我们的手风琴组件已经完成一半了,接下来我们来看容器组件<code>Accordion</code>的写法。</p><h2>Accordion容器组件</h2><p>首先我们先把结构写好:</p><pre><code>const Accordion = (props) => {
//后续代码
};</code></pre><p>我们再来分析一下需要传给Accordion组件的属性有哪些,很显然有defaultIndex,onItemClick和children,因此我们可以定义如下的接口:</p><pre><code class="ts">interface AccordionType {
defaultIndex: number | string;
onItemClick(key: number | string): void;
children: JSX.Element[];
}</code></pre><p>注意这里的children不应该是ReactNode,而是JSX.Element元素数组,这是为什么呢,我们后面再来解释这个问题。现在我们知道了props的属性之后,我们可以拿到这些属性,代码如下:</p><pre><code>const Accordion = (props:Partial<AccordionType>) => {
const { defaultIndex, onItemClick, children } = props;
//后续代码
};</code></pre><p>现在我们再维护一个状态,用来代表当前显示的子元素组件的索引,使用useState hook函数,初始化默认值就应该是defaultIndex。如下:</p><pre><code>const Accordion = (props:Partial<AccordionType>) => {
const { defaultIndex, onItemClick, children } = props;
//新增的代码
const [bindIndex, setBindIndex] = useState(defaultIndex);
//后续代码
};</code></pre><p>接下来,我们编写好容器元素,并写好样式,如下所示:</p><pre><code>const Accordion = (props: Partial<AccordionType>) => {
const { defaultIndex, onItemClick, children } = props;
const [bindIndex, setBindIndex] = useState(defaultIndex);
return (
<div className={AccordionContainer}></div>
);
};</code></pre><p>容器元素的样式如下:</p><pre><code class="css">const baseStyle = css`
line-height: 1.5715;
`;
const AccordionContainer = cx(
baseStyle,
css`
box-sizing: border-box;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
background-color: #fafafa;
border: 1px solid #d9d9d9;
border-bottom: 0;
border-radius: 2px;
`,
);</code></pre><p>好的,接下来,我们实际上容器元素的子元素应该是多个AccordionItem元素,也正因为如此,这里的children类型就是<code>JSX.Element []</code>,我们应该如何获取这些子元素呢?我们应该知道,每一个子元素对应的就是一个节点,在react中用的是链表来表示这些节点,每个节点对应的就有个type属性,我们只需要拿到容器元素的子组件元素中type属性为AccordionItem的元素数组,如下:</p><pre><code>//name不是AccordionItem,代表子元素不是AccordionItem,不是的我们需要过滤掉
const items = children?.filter(
(item) => item?.type?.name === 'AccordionItem,代表子元素不是AccordionItem,所以我们需要过滤掉',
);</code></pre><p>到了这里,我们就知道了,容器元素的子元素是一个数组,我们就需要遍历,使用map方法,如下:</p><pre><code class="tsx">items?.map(({ props: { index, label, children } }) => (
<AccordionItem
key={index}
label={label}
children={children}
isCollapsed={bindIndex !== index}
handleClick={() => changeItem(index)}
/>
))</code></pre><p>请注意这一段代码:</p><pre><code class="tsx">handleClick={() => changeItem(index)}</code></pre><p>这就是我们之前子组件绑定的事件,也是我们需要暴露出去的事件,在这个事件方法中,我们无非执行的就是更改当前被展开元素的索引。所以代码就很好写了:</p><pre><code class="tsx">const changeItem = (index: number | string) => {
//暴露点击事件方法接口
if (typeof onItemClick === 'function') {
onItemClick(index);
}
//设置索引
if (index !== bindIndex) {
setBindIndex(index);
}
};</code></pre><p>到了这里,我们的一个手风琴组件就完成了,完整代码如下:</p><pre><code class="tsx">import { cx, css } from '@emotion/css';
import React, { useState } from 'react';
import type { ReactNode, SyntheticEvent } from 'react';
const baseStyle = css`
line-height: 1.5715;
`;
const AccordionContainer = cx(
baseStyle,
css`
box-sizing: border-box;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
background-color: #fafafa;
border: 1px solid #d9d9d9;
border-bottom: 0;
border-radius: 2px;
`,
);
const AccordionItemContainer = css`
border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
baseStyle,
css`
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
padding: 12px 16px;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
transition: all 0.3s, visibility 0s;
box-sizing: border-box;
`,
);
const AccordionItemContent = css`
color: #000000d9;
background-color: #fff;
border-top: 1px solid #d9d9d9;
transition: all 0.3s ease-in-out;
padding: 16px;
&.collapsed {
display: none;
}
&.expanded {
display: block;
}
`;
interface AccordionItemType {
index: string | number;
label: string;
isCollapsed: boolean;
handleClick(e: SyntheticEvent): void;
children: ReactNode;
}
interface AccordionType {
defaultIndex: number | string;
onItemClick(key: number | string): void;
children: JSX.Element[];
}
const AccordionItem = (props: Partial<AccordionItemType>) => {
const { label, isCollapsed, handleClick, children } = props;
return (
<div className={AccordionItemContainer} onClick={handleClick}>
<div className={AccordionItemHeader}>{label}</div>
<div
aria-expanded={isCollapsed}
className={`${AccordionItemContent}${
isCollapsed ? ' collapsed' : ' expanded'
}`}
>
{children}
</div>
</div>
);
};
const Accordion = (props: Partial<AccordionType>) => {
const { defaultIndex, onItemClick, children } = props;
const [bindIndex, setBindIndex] = useState(defaultIndex);
const changeItem = (index: number | string) => {
if (typeof onItemClick === 'function') {
onItemClick(index);
}
if (index !== bindIndex) {
setBindIndex(index);
}
};
const items = children?.filter(
(item) => item?.type?.name === 'AccordionItem',
);
return (
<div className={AccordionContainer}>
{items?.map(({ props: { index, label, children } }) => (
<AccordionItem
key={index}
label={label}
children={children}
isCollapsed={bindIndex !== index}
handleClick={() => changeItem(index)}
/>
))}
</div>
);
};</code></pre><p>让我们来看一下效果:</p><p><img src="/img/bVc09rw" alt="" title=""></p><p>到此为止了,更多React组件的实现,可以访问<a href="https://link.segmentfault.com/?enc=8sX75paCqp4gw9u7ae6d%2Fw%3D%3D.zQnSniLXVN30oLW0E65kuPyzAty9%2F%2F0cE81ZHuSW7UF0W9BJE7hR1YWDfKI5KSxr5fVpKzC3XD4rQSHyguiVUg%3D%3D" rel="nofollow">react-code-segment</a>。</p><p>源码地址可以看这里<a href="https://link.segmentfault.com/?enc=cy3P7B3jQMvhR2Bl%2F7aszA%3D%3D.18CilGipmFgNXD%2Bqtj7XeIJhKzsdQEp1%2BUP7R6I%2F5agfNGwPamEEiEMIX%2B8W%2Bvux%2FLFYGwvI69JaJ%2FVWKCedFw%3D%3D" rel="nofollow">源码地址</a>。喜欢觉得不错能够帮助到您,希望能点个赞,您的赞就是我更新文章的最大动力。</p>
2021我的个人总结
https://segmentfault.com/a/1190000041101122
2021-12-11T12:37:40+08:00
2021-12-11T12:37:40+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
8
<blockquote>本文参与了 <a href="https://segmentfault.com/a/1190000041089764">SegmentFault 思否征文「2021 总结」</a>,欢迎正在阅读的你也加入。</blockquote><p>时间过的很快,一转眼一年就已经过去了,在2021年的一年里我的收获还是蛮多的,可惜的是,今年我的目标一样也没有完成,有些遗憾与可惜。不过明年的我将会更加努力,争取实现我的目标。</p><p>下面一起来看看今年的我收获了什么?</p><p>首先感谢思否这个平台,给我一个发展的机会,让我荣幸可以在思否上录制视频课程,不在乎赚了多少收益,至少能让我认知当录制视频的艰难以及录制课程所带来的成长。以下是我的录制成果:</p><ul><li><a href="https://ke.segmentfault.com/course/1650000039393407?utm_source=recommend_web-live-new">玩转TypeScript项目之验证登录</a></li><li><a href="https://ke.segmentfault.com/course/1650000039798692?utm_source=recommend_web-live-new">玩转TypeScript项目之电影选座</a></li><li><a href="https://ke.segmentfault.com/course/1650000040358952">从零开始实现一个mini版本的编译器(compiler)</a></li><li><a href="https://ke.segmentfault.com/course/1650000040761646">从零开始编写颜色选择器</a></li></ul><p>本来今年还打算在录制一门《读书笔记管理系统》,技术栈react + typescript + 全家桶 + php的课程的,但是因为我在逐渐开发的过程中又加入了不少的设计,所以今年是完不成这个目标的,所以我想明年来完成这个目标,敬请期待吧!</p><blockquote>PS:这个课程让我学到了很多,至少react技术栈是熟练了,而且我也自己造了很多UI组件,例如Dialog弹框组件,面包屑breadCrumb组件,Select组件,Upload组件,Pagnation组件等,可以这么说,已经算的上是一个小型的UI组件库了。</blockquote><p>如下图所示:</p><p><img src="/img/bVcWCqA" alt="" title=""></p><p>因为功能逐步增加,相信这门课程一定会让大家学到很多东西,可以提前给大家看看效果。</p><p><img src="/img/bVcWCqS" alt="3.png" title="3.png"><br><img src="/img/bVcWCqT" alt="4.png" title="4.png"><br><img src="/img/bVcWCqR" alt="5.png" title="5.png"></p><p>今年也完成了一些文章,如下所示:</p><ul><li><a href="https://segmentfault.com/a/1190000039129377">2天用vue3.0实现《掘金 - 2020年度人气创作者榜单》网站</a></li><li><a href="https://segmentfault.com/a/1190000039670041">一个灵活高度自定义的JavaScript颜色选择器</a></li><li><a href="https://segmentfault.com/a/1190000039757880">深入JavaScript中的this对象</a></li><li><a href="https://segmentfault.com/a/1190000040236708">2021年,让我们手写一个mini版本的vue2.x和vue3.x框架</a></li><li><a href="https://segmentfault.com/a/1190000040481518">50天用JavaScript完成50个web项目,我学到了什么?</a></li><li><a href="https://segmentfault.com/a/1190000040677455">从零开始使用create-react-app + react + typescript 完成一个网站</a></li><li><a href="https://segmentfault.com/a/1190000040689791">一个工具函数,实现页面tooltip组件的限制使用</a></li><li><a href="https://segmentfault.com/a/1190000040789940">从零开始实现一个颜色选择器(原生JavaScript实现)</a></li><li><a href="https://segmentfault.com/a/1190000040813435">50天用react.js重写50个web项目,我学到了什么?</a></li><li><a href="https://segmentfault.com/a/1190000040854433">从零开始实现一个在线三角形生成器</a></li></ul><p>能有这些成果,我已经很满意了。并且我还写了一个50项目的官网,详情可以看<a href="https://link.segmentfault.com/?enc=VLvSVNPUIjJtzITEbGL%2FWw%3D%3D.aeNFLWOZB0iq7aNnyNIJuhmg2ymVEwB2lSFX%2BbPWCwHg9WVbuFMuYP49AZjdP66N" rel="nofollow">这里</a>,技术栈采用less + vite + vue3来实现。</p><p>当然,我还完成了算法网站,剑指offer的总结,目前我已经把剑指offer第2版的算法题刷完了,正在刷专项突击版,不得不说刷了算法之后,感觉我整个思维逻辑都提升了很多,我知道自己还很菜,还需要努力,愿2022年,我能完成我的所有目标。以下是我的剑指offer算法部分截图。</p><p><img src="/img/bVcWCrk" alt="" title=""></p><p><img src="/img/bVcWCrl" alt="" title=""></p><p>更多详情可以观看官网<a href="https://link.segmentfault.com/?enc=QnfSyvHAguU%2FWNTBYAZf9Q%3D%3D.an2AZZrv%2FRVd5R8cFS9a71%2FYE48mw2yC3AyjKL%2Fd5jIy2S0CA9n0iwJq0QG7pfJu" rel="nofollow">剑指offer</a>。</p><p>还有收集代码片段的网站,如下图所示:</p><p><img src="/img/bVcWCrp" alt="" title=""></p><p>可能来自工作中的总结,也有可能收集网上的代码片段,认真对待每一个开源项目,我不会放弃的,也在努力做到更新。</p><p><a href="https://link.segmentfault.com/?enc=Mo3ufMeqyMwThyJh7QFHgA%3D%3D.1YhvwFZr%2Fag3KHTnNUgCglYroea1WhQCKZA95Q1FIiYu920rWZ5jD%2Bo%2F8aQIibW6" rel="nofollow">code-segment</a></p><p>很希望有朋友一起加入贡献,完善这个代码片段的仓库,不限js,css等技术。</p><p>当然今年还有最大的打击,那就是面试上的失败,虽然失败了5次,但是我并不气馁,我知道自己还需要努力,正如我的专栏所说,每天进步一点点,也就是很大的进步。</p><p>而且还有一些待完成的任务,我也没有很好的完成,这是自己的遗憾,明年的目标就是尽力完成这些目标。</p><ul><li>写一本掘金小册[solid.js实战]。</li><li>录制《读书笔记管理系统》课程。</li><li>完成vue版本的50个JavaScript,mini版本的项目。(其实已经完成了部分了,只是还没有总结出来,具体可以查看我写的官网)。</li><li>争取把剑指offer专项突击版本的算法题给总结完成还有算法总结文章。</li><li>颜色选择器2.0.0的重大更新。</li></ul><p>回望过去的这一年,也许是我从业以来成长最深的一年,与君共勉。</p>
剑指offer刷题之路--1.数组中重复的数字
https://segmentfault.com/a/1190000041049282
2021-12-01T21:34:28+08:00
2021-12-01T21:34:28+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<h3>数组中重复的数字</h3><blockquote>题目:在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。</blockquote><p>示例如下:</p><pre><code class="js">//输入:
[2, 3, 1, 0, 2, 5, 3]
//输出:2 或 3 </code></pre><blockquote>限制:2 <= n <= 100000</blockquote><h3>思路分析</h3><ul><li>方法一:普通解法</li></ul><p>我们可以通过创建一个新数组,然后判断新数组当中是否含有<code>nums</code>中的数组项,如果没有就添加到新数组中,如果有,则表明数组项重复了,则另外定义一个变量接收这个重复项,然后返回。代码如下:</p><pre><code class="js"> var findRepeatNumber = function(nums) {
let res = [],repeat = "",len = nums.length,i = 0;
while(i < len){
if(res.indexOf(nums[i]) === -1){
res.push(nums[i]);
}else{
// 代表已经找到重复数字了,所以跳出循环
repeat = nums[i];
break;
}
}
return repeat;
}</code></pre><p>以上算法的时间复杂度和空间复杂度分析如下:</p><ul><li>时间复杂度O(n)。</li><li>空间复杂度O(n)。</li></ul><p>方法二:原地置换算法</p><p>我们尝试如此思考,因为题目很清楚的说明了数组项中的数的范围是在<code>0~n-1</code>之间(注意,如果没有该条件是无法使用这个算法的,这个算法也只是用时间换空间而已),也就是说比如数组的长度是2,那么数组里的所有数组项的数只能是0或1,因此我们可以猜测当数组下标等于该数组项的数的时候,则一定不会重复,如果不等于的话,那么我们把该数组项的数和等于数组下标的数做一个交换位置,在位置的交换过程中,当两者相等了,这就表明重复了。例如[1,1],两者交换位置始终都会等于1所在的数组下标,也就找到重复数字。</p><pre><code class="js"> var findRepeatNumber = function(nums) {
for(let i = 0,len = nums.length;i < len;i++){
//定义一个中间变量用于交换
let temp;
while(nums[i] !== i){
if(nums[i] === nums[nums[i]]){
// 判断数组项对应的数是否和数组数做下标对应的数一样,如果一样则重复
return nums[i];
}
// 开始做交换
temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
}
return -1;
}</code></pre><p>以上算法的时间复杂度和空间复杂度分析如下:</p><ul><li>时间复杂度 O(n): 遍历数组使用 O(n),每轮遍历的判断和交换操作使用 O(1)。</li><li>空间复杂度 O(1): 使用常数复杂度的额外空间。</li></ul><p>更多题以及解法尽在<a href="https://link.segmentfault.com/?enc=yIolNof9Luvq6zShKhjZRg%3D%3D.aaF2Qr71vacxiKARc5Uedf2dxzuGlL7RiTqDrIKw8glKup7HxPOt6aMix76cAVfQ" rel="nofollow">这里</a>。</p>
从零开始实现一个在线三角形生成器
https://segmentfault.com/a/1190000040854433
2021-10-24T06:52:18+08:00
2021-10-24T06:52:18+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
10
<h2>在线三角形生成器</h2><p>通过本文,你将学到如下知识:</p><ol><li>快速入门vue 3.2的核心API知识</li><li>掌握最新浏览器实现的复制粘贴的clipboard API</li><li>按需引入element plus</li><li>vite 的一些入门配置</li><li>正则表达式以及typescript的类型</li><li>less语法</li><li>element plus 国际化</li></ol><h3>快速创建一个vite项目</h3><p>参考文档官网<a href="https://link.segmentfault.com/?enc=3qIWiAJB7l%2BosjQSu%2BkzXw%3D%3D.aBnUoZr%2Fl3zs36v0D2dw4A3EveaJ4zE9V6u1Aahg2ng%3D" rel="nofollow">vite</a>。我们可以快速创建一个项目:</p><pre><code class="js"># npm 6.x
npm init vite@latest triangle --template vue
# npm 7+, extra double-dash is needed:
npm init vite@latest triangle -- --template vue
# yarn
yarn create vite triangle --template vue</code></pre><p>接下来,我们需要再额外添加一些依赖。</p><pre><code class="js">yarn add unplugin-vue-components element-plus less</code></pre><p>unplugin-vue-components是element plus提供的一个按需引入实现的插件。然后修改vite.config.js的代码如下:</p><pre><code class="js">import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers:[ElementPlusResolver()]
})
],
base:"./"
})</code></pre><p>全是照着<a href="https://link.segmentfault.com/?enc=UZkGT4CHt0ba3KjKRN9qMA%3D%3D.3SCH15354ZPv1y9q4pOsO5icvjyVtHTz4SzT2VGqwB9tgQ6%2Bts3Hi3m6sj9PjCLLVo%2F4UKbMmQr%2BWEIvBbsfdkmpM5vOLN81YHceDYlWanA%3D" rel="nofollow">element plus官方文档</a>来一步一步操作的。</p><p>接下来,在main.js中引入element plus的样式文件:</p><pre><code class="js">import "./style/reset.less"
import 'element-plus/dist/index.css'</code></pre><p>其中reset.less的代码如下:</p><pre><code class="less">body,h1,img,h2,h3,h4,h5,h6,p {
margin: 0;
padding: 0;
}
.app {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg,#e0e0e0 10%,#f7f7f7 90%);
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-y: auto;
overflow-x: hidden;
}
::-webkit-scrollbar {
width: 0;
height: 0;
}</code></pre><p>如此一来,准备工作算是完成了,接下来,我们就来一步一步的实现。</p><h3>实现的工具函数</h3><p>在src目录下创建一个utils目录,然后新建一个utils.ts文件,里面写上如下代码:</p><pre><code class="js">export const getTriangleStyle = (direction:string,w:number,h:number,color:string) => {
const style = {
"top":{
"borderColor":`transparent transparent ${color} transparent`,
"borderWidth":`0 ${w / 2}px ${h}px ${w / 2}px`
},
"bottom":{
"borderColor":`${color} transparent transparent transparent`,
"borderWidth":`${h}px ${w / 2}px 0 ${w / 2}px`
},
"left":{
"borderColor":`transparent ${color} transparent transparent`,
"borderWidth":`${h / 2}px ${w}px ${h / 2}px 0`
},
"right":{
"borderColor":`transparent transparent transparent ${color}`,
"borderWidth":`${h / 2}px 0 ${h / 2}px ${w}px`
}
}
return style[direction];
}</code></pre><p>这个工具函数其实也就是实现三角形的方向切换问题。</p><h3>页面分析</h3><p>接下来,我们来看页面的整体。其实包含了五大部分,如下图所示:</p><p><img src="/img/bVcVAgC" alt="" title=""></p><p>即:</p><ol><li>头部组件(包含标题组件)</li><li>操作样式的表单</li><li>预览模块</li><li>代码编辑器</li><li>底部信息</li></ol><h4>头部组件</h4><p>我们一部分一部分的来看,首先是头部组件的实现,头部组件只是包含一个标题组件,所以我们先来看标题组件的实现。如下所示:</p><p>template部分:</p><pre><code class="html"><component :is="'h' + level">
<slot>{{ content }}</slot>
</component></code></pre><p>js部分:</p><pre><code class="js"><script setup>
import { defineProps } from '@vue/runtime-core';
const props = defineProps({
level:{
type:[String,Number],
default:1
},
content:String
});
</script></code></pre><p>就这么一点代码,我们就需要了解vue3.x的四个知识点。</p><ol><li>vue可以为script标签添加setup,从而使得整个代码块都在setup钩子函数作用域中,setup钩子函数相当于vue2.x的beforeCreated和created的合并,也是vue3.x composition API 的入口函数。</li><li>导入<code>defineProps</code>就可以定义vue的props单向数据流。这里定义了2个字段,即<code>level</code>和<code>content</code>。顾名思义,level就是用于动态组件的,我们实际上就是封装一个动态组件,组件的标签是<code>h1~h6</code>,level的默认值是1。它的类型可以使字符串或者数值。而content就是字符串,被用作插槽的备用内容。</li><li>动态组件component,通过绑定is属性可以知道组件名。</li><li>插槽slot。</li></ol><p>正好,我们的头部就用到了这个标题组件,接下来我们来看头部组件即<code>Header</code>组件的实现。</p><p>template部分:</p><pre><code class="html"><header class="tri-header">
<Title level="2" class="tri-title">
趣谈前端|在线三角形生成器
</Title>
<slot></slot>
</header></code></pre><p>js部分:</p><pre><code class="js"><script setup>
import Title from "./Title.vue";
</script></code></pre><p>style部分:</p><pre><code class="css"><style lang="less">
.tri-header {
width: 100%;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
color: #535455;
}
</style></code></pre><p>可以看到头部组件,我们使用弹性盒子布局,让组件垂直水平居中,字体颜色为<code>#535455</code>。在js部分,我们直接导入了前面我们封装的标题组件。在模板部分,我们直接使用了header元素包裹这个标题组件。并且添加了一个插槽元素。</p><p>这样一来,我们的头部组件部分就完成了,比较简单。</p><h4>表单部分</h4><p>接下来我们来看表单部分。</p><p>template部分:</p><pre><code class="html"><el-form class="tri-form">
<el-form-item label="方向:">
<el-radio-group v-model="state.form.direction">
<el-radio v-for="(item,index) in state.radioList" :key="item.value + index":label="item.value" class="tri-radio">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="宽度:">
<el-slider v-model="state.form.width" :min="0":max="200"></el-slider>
</el-form-item>
<el-form-item label="高度:">
<el-slider v-model="state.form.height" :min="0":max="200"></el-slider>
</el-form-item>
<el-form-item label="旋转角度:">
<el-slider v-model="state.form.rotate" :min="0":max="360"></el-slider>
</el-form-item>
<el-form-item label="背景色:">
<el-config-provider :locale="state.locale">
<el-color-picker v-model="state.form.color"><el-color-picker>
</el-config-provider>
</el-form-item>
</el-form></code></pre><p>js部分:</p><pre><code class="js"><script setup lang="ts">
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
import { ElForm,ElSlider,ElRadio,ElRadioGroup,ElFormItem,ElColorPicker,ElConfigProvider } from 'element-plus';
import { reactive,defineEmits, watch } from 'vue-demi';
const state = reactive({
form:{
direction:"top",
width:60,
height:60,
color:"#2396ef",
rotate:0
},
radioList:[
{ label:"上",value:"top"},
{ label:"下",value:"bottom"},
{ label:"左",value:"left"},
{ label:"右",value:"right"}
],
locale:zhCn
});
const emit = defineEmits(["on-change"]);
watch(() => state.form,(val) => {
emit("on-change",val);
},{ deep:true,immediate:true })
</script></code></pre><p>style部分:</p><pre><code class="css"><style lang="less" scoped>
@media (max-width: 1000px) {
.tri-radio {
margin-right: 10px;
}
}
</style></code></pre><p>在这里,我们分析页面的部分,我们知道,我们需要用到单选框分组组件,单选框组件,颜色选择器组件,表单组件,国际化配置组件(element plus新增的elConfigProvider,个人理解设计借鉴了react的Provider组件),滑块组件。单选框组件用于修改三角形的方向,滑块组件用于配置三角形的宽高以及旋转角度,而颜色选择器组件用于配置三角形的背景颜色。所以我们定义了如下对象:</p><pre><code class="js">form:{
direction:"top",//方向
width:60,//宽度
height:60,//高度
color:"#2396ef",//背景色
rotate:0 //旋转角度
},</code></pre><p>我们使用vue的reactive方法来定义响应式数据。由于颜色选择器默认是英文,所以我导入了element plus的中文包。即:</p><pre><code class="js">import zhCn from 'element-plus/lib/locale/lang/zh-cn'</code></pre><p>然后再颜色选择器中,添加<code>el-config-provider</code>组件包裹颜色选择器。实际上我这里只是单独设置颜色选择器的中文包,这个组件应该是包裹在根元素组件<code>App</code>的。然后我们使用了<code>defineEmits</code>发出一个事件给父组件使用。如下:</p><pre><code class="js">const emit = defineEmits(["on-change"]);
watch(() => state.form,(val) => {
emit("on-change",val);
},{ deep:true,immediate:true })</code></pre><p>我们使用<code>watch</code>方法监听表单数据对象,并且提供了配置选项,也就是说该组件在创建的时候就会立即执行一次该方法,然后发出一个<code>on-change</code>事件,将<code>form</code>表单数据给传给父组件使用。</p><h4>预览组件</h4><p>接下来,我们来看预览组件的实现:</p><p>template部分:</p><pre><code class="html"><div class="tri-code-effect">
<div class="tri-element" :style="state.style"></div>
</div></code></pre><p>js部分:</p><pre><code class="js"><script lang="ts" setup>
import { getTriangleStyle } from "../utils/util";
import { defineProps,reactive,watch } from '@vue/runtime-core';
const props = defineProps({
formData:{
type:Object,
default:() => ({
width:60,
height:60,
direction:"top",
color:"#2396ef",
rotate:0
})
}
});
const state = reactive({
style:{}
});
watch(() => props.formData,(val) => {
const { direction,color,width,height,rotate } = val;
state.style = { ...getTriangleStyle(direction,width,height,color),transform:`rotate(${rotate}deg)`};
},{ deep:true,immediate:true })
</script></code></pre><p>style部分:</p><pre><code class="css"><style lang="less" scoped>
.tri-code-effect{
min-width: 300px;
height: 350px;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(45deg,rgba(0,0,0,.2) 25%,transparent 0,transparent 75%,rgba(0,0,0,.2) 0),
linear-gradient(45deg,rgba(0,0,0,.2) 25%,transparent 0,transparent 75%,rgba(0,0,0,.2) 0);
background-size: 30px 30px;
background-position: 0 0,15px 15px;
.tri-element {
display: inline-block;
border-style: solid;
width: 0;
height: 0;
transition: all .3s;
}
}
</style></code></pre><p>可以看到预览组件就2个元素,其中父元素就是我们最外层的盒子元素,盒子元素通过设置渐变,才会出现那样的效果。然后我们的三角形元素,它的基本样式还是有一些不会变动的,所以我们写在样式当中,变动的样式我们才定义在数据中。可以看到,我们通过接受父组件穿过来的formData表单数据对象,然后需要进行样式对象的规范化处理,这才是我们监听函数的意义所在:</p><pre><code class="js">watch(() => props.formData,(val) => {
const { direction,color,width,height,rotate } = val;
state.style = { ...getTriangleStyle(direction,width,height,color),transform:`rotate(${rotate}deg)`};
},{ deep:true,immediate:true })</code></pre><p>我们也是使用了<code>immediate</code>选项,会让组件在创建的时候就立即调用一次,然后我们通过对象解构拿到父组件出来的props数据,并且修改定义在<code>reactive</code>方法实现的响应式数据中。然后再绑定到元素的<code>style</code>属性上。</p><h4>根组件</h4><p>接下来是第三部分,代码编辑器的部分,我们是直接写在根组件<code>APP.vue</code>中的,可以看到代码编辑器的部分包含三块。</p><ol><li>标题</li><li>复制代码按钮</li><li>显示代码的文本框(禁用)</li></ol><p>接下来,我们就来看一下根组件的代码吧。</p><p>template部分:</p><pre><code class="html"><Header></Header>
<main class="tri-main">
<el-row class="tri-row">
<el-col :span="12" class="tri-left tri-col">
<Form @on-change="changeForm"></Form>
</el-col>
<el-col :span="12" class="tri-right tri-col">
<code-effect :formData="state.formData"></code-effect>
</el-col>
</el-row>
<el-row class="tri-row tri-code-row">
<el-col :span="12" class="tri-left tri-col">
<el-header class="tri-header tri-fcs">
<Title level="2">CSS代码</Title>
<el-button @click="onCopyCodeHandler">复制代码</el-button>
</el-header>
<el-input :autosize="{ minRows: 8, maxRows: 10 }" type="textarea" v-model="state.code" disabled></el-input>
</el-col>
</el-row>
<el-footer class="tri-footer">
inspired by <el-link href="http://49.234.61.19/tool/cssTriangle" target="_blank" type="primary" class="tri-link">在线三角形样式生成器</el-link>
更多示例尽在<el-link href="https://eveningwater.com/my-web-projects/home/" target="_blank" type="primary" class="tri-link">我的个人项目集合</el-link>中。
</el-footer>
</main></code></pre><p>js部分:</p><pre><code class="js"><script setup>
import Header from './components/Header.vue';
import Title from "./components/Title.vue";
import CodeEffect from './components/CodeEffect.vue';
import { ElRow,ElCol,ElInput,ElHeader,ElButton,ElFooter,ElLink,ElMessageBox } from 'element-plus';
import { nextTick, reactive } from 'vue-demi';
const state = reactive({ formData:{},code:"" })
const changeForm = (form) => {
state.formData = {...form};
nextTick(() => {
const codeEffect = document.querySelector(".tri-element");
const style = codeEffect.style.cssText;
const templateStyle = `.tri-element {display: inline-block;border-style: solid;width: 0;height: 0;${style.replace(/\;(.*?)\:/g,w => w.slice(0,1) + w.slice(1).trim())}}`;
state.code = templateStyle.replace(/\;/g,";\n").replace(/\{|\}/,w => w + "\n").replace(/((.*?)\:)/g,w => " "+ w);
})
}
const confirm = () => {
ElMessageBox.alert(`CSS代码已复制,请粘贴查看!`,"温馨提示",{
confirmButtonText:"确定",
callback:() => {}
})
}
const onCopyCodeHandler = () => {
// `navigator.clipboard.writeText` not working in wechat browser.
if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
navigator.clipboard.writeText(state.code).then(() => confirm())
}else{
const input = document.createElement("input");
input.value = state.code;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
confirm();
}
}
</script></code></pre><p>style样式部分:</p><pre><code class="css"><style lang="less" scoped>
.tri-main {
display:flex;
min-height: 600px;
flex-direction: column;
justify-content: center;
width: 100%;
min-width: 750px;
max-width: 980px;
margin: auto;
.tri-left {
background-color: #fff;
box-shadow: 0 4px 12px rgba(255,255,255,.4);
height: 350px;
padding: 10px;
border-radius: 2px;
}
.tri-header.tri-fcs {
justify-content: space-between;
padding: 0;
}
.tri-code-row {
margin-top: 15px;
}
.tri-row {
margin-left: -12.5px;
margin-right: -12.5px;
.tri-col {
padding-left: 12.5px;
padding-right: 12.5px;
}
}
.tri-footer {
align-items: center;
display: flex;
color: #535455;
font-size: 14px;
justify-content: center;
flex-wrap: wrap;
.tri-link {
margin: 0 1%;
}
}
}
@media (max-width: 1000px) {
.tri-left,.tri-right {
width: 100%;
flex:0 0 100%;
max-width: 100%;
overflow-x: hidden;
.tri-code-effect {
margin-top: 15px;
}
}
.tri-main {
padding: 10px 0;
min-width: 300px;
max-width: calc(100% - 20px);
margin: 0 10px;
overflow-x: hidden;
.tri-row {
min-width: 100%;
max-width: 100%;
margin: 0;
&.tri-code-row {
margin-top: 15px;
}
.tri-col:not(.tri-right) {
padding: 10px;
}
.tri-col.tri-right {
padding: 0;
}
}
}
}
</style></code></pre><p>由于兼容了移动端布局,所以样式代码看起来有点多。但整体就是利用媒体查询来调整一下。这里我们用到了element plus的<code>ElRow,ElCol,ElInput,ElHeader,ElButton,ElFooter,ElLink,ElMessageBox</code>组件。template的组件元素应该是很好理解的,包含的五个部分都写进去了。</p><blockquote>tips:这里需要提醒一下,写vue3.x的语法需要安装<code>volar</code>插件。</blockquote><p>接下来我们来看js部分,js部分其实就是导入了需要使用的组件,然后接受了子组件传来的表单数据。并且我们获取到了三角形元素的css代码,然后渲染到文本框中去。这里我们操作css样式获取到了元素的css代码字符串,然后利用正则表达式处理了一下,最后才能得到我们实际显示的那样保持规范化的缩进而显示的代码。</p><p>我们做的一系列正则表达式的匹配,就是为了让代码显示保持合格的缩进。</p><p>比如添加<code>\n</code>换行符,css样式属性名的前面添加一段空白。这些都没什么好说的。这里比较有意思的实现,就是复制代码的实现:</p><pre><code class="js">const onCopyCodeHandler = () => {
// `navigator.clipboard.writeText` not working in wechat browser.
if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
navigator.clipboard.writeText(state.code).then(() => confirm())
}else{
const input = document.createElement("input");
input.value = state.code;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
confirm();
}
}</code></pre><p>在微信浏览器端不知道是不是因为内置view组件的实现原因,并不支持<code>clipboard</code>API。我们通过获取到navigator下的clipboard属性,它就是一个剪贴板对象,然后我们可以调用<code>writeText</code>方法,就可以往剪贴板中写入内容。也就实现了用户鼠标复制需要复制的内容的实现,该方法返回一个promise,在then方法中,我们弹出一个提示框,用于提示用户代码内容已经复制了。</p><p>而在微信浏览器端,我们还是通过创建一个input元素,将复制的内容赋给input元素,然后设置选中,调用<code>document.execCommand("copy")</code>事件。最后弹出提示。如此一来,我们的三角形生成器就这样实现了。</p><h3>最后</h3><p>如果觉得本文能让你学到东西,望不吝啬点个赞。详细源码可以查看<a href="https://link.segmentfault.com/?enc=txyVYsk7xnlebyKagbaZkg%3D%3D.w2UVbeGTA0%2FLj1BULypcQmXex%2BUkyJhq8zFI1Pqf9oBr4Xv2jGwK%2FIQC1KOJFPQXYhWEIAeddZGYc29gFVt2jmJWfhBD4tV1nKlz0KZ7COk%3D" rel="nofollow">这里</a>。最后感谢大家的阅读。</p><blockquote>tips:本示例的实现灵感来自于徐小夕大佬的<a href="https://link.segmentfault.com/?enc=oEXllBZvA%2FTSMlu0zFRzcA%3D%3D.xeDZtD6GX%2F8U65CCG%2F%2BbJtGZxAbPfVVwMdTQBAZA7eAI%2FrqMQBvucIwUrrjQ4nMv" rel="nofollow">在线三角形生成器--文章</a>和<a href="https://link.segmentfault.com/?enc=FWpuvwnzS1e4ojQLSkjvjw%3D%3D.7f0TLENC1Gw8tKzCunACy9HVTDKcdr%2FxnAhEiolBcbJ44nt%2BVMsu%2BMJLAuFtafy4" rel="nofollow">在线三角形生成器--示例</a>,感谢大佬提供的灵感。</blockquote>
50天用react.js重写50个web项目,我学到了什么?
https://segmentfault.com/a/1190000040813435
2021-10-14T19:47:58+08:00
2021-10-14T19:47:58+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
50
<h2>1.Expanding Cards</h2><p>效果如图所示:</p><p><img src="https://segmentfault.com/img/bVcT1ju" alt="1.png" title="1.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=43oHJEEmayF2u5Gwh5j5Kw%3D%3D.4WczVoDfrEfPp5udobjAH6IIBGk3KshgEORG5lnv1ADOSRJoxS8P0BlG1VBXa1cqprgS%2F%2F8%2B9YagJ0vHYEQCkEHXINg%2BoMLA5NRDQbAzLNE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=kWAauHEL5lFp28%2B8%2F4wTHA%3D%3D.2FA5xue%2BL1tnlHyYbzpcOVKVfjIhnnVfhyjazMZBcYDqYTh%2FEXJ7ocTvCEpBssD1cKKFds64RAV0I73QsZCcEQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>React的函数组件中建立数据通信,我们通常使用<code>useState</code>方法。它的使用方式采用数组解构的方式,如下:</li></ol><pre><code class="js">const [state,setState] = React.useState(false);//useState方法的参数可以是任意的JavaScript数据类型</code></pre><p>解构的第一个参数是我们定义并且访问的数据状态,第二个参数则是当我们需要变动数据状态时所调用的方法,其作用类似类组件中的<code>this.setState</code>。</p><p>更详细的使用方式参考文档 <a href="https://link.segmentfault.com/?enc=3LTCSJBOlYUQzxaG5306bQ%3D%3D.08D4M8acOR0qPbMW07Bw2iZl6S4ep%2FBZsuC%2BGnsBygMFFpBJEwu0oiZzJxTJpeo3" rel="nofollow">useState API</a>。</p><p>2.与类组件类似的钩子函数,或者也可以理解为是函数组件的生命周期<code>useEffect</code>。它接收一个副作用函数<code>effect</code>作为参数,如下所示:</p><pre><code class="js">useEffect(() => {});//里面的箭头函数即副作用函数</code></pre><p>以上示例只是做了一个简单的更换文档标题,事实上在这个副作用函数中,我们可以做很多事情,详情参考文档 <a href="https://link.segmentfault.com/?enc=H3PalIi16%2FnQBgVMb%2BdZWQ%3D%3D.r94cdV33iGSMCSfp3hvaotY2oJnA0DKRVSUg3uFE3MDgaTu6Tq73f3d78SgcYTPi" rel="nofollow">useEffect API</a>。</p><p>3.React内部有自己的一套事件机制,我们称之为<a href="https://link.segmentfault.com/?enc=AUDv6Uh8V84QxjkhnaTT%2BA%3D%3D.o4vFk2D9IWgQGgjSWYRLPLcmwhp6rKF1uawj1T8LJHaHz%2B1V0UtIzQM%2FNQLLKGT%2F" rel="nofollow">合成事件</a>。它与正常的<code>JavaScript</code>绑定事件很类似,但是它将事件命名方式采用了驼峰式写法,并且它也对事件对象做了一层包装,其名为<code>SyntheticEvent</code>。注意,这是一种跨浏览器端的包装器,我们如果想要使用原生的事件对象,可以使用<code>nativeEvent</code>属性,这在后面的示例中可能会涉及到。比如我们以上的事件绑定:</p><pre><code class="js">onClick={ onChangeHandler.bind(this,index) }</code></pre><p>注意这里,我们通常需要调用<code>bind</code>方法来将<code>this</code>绑定到该组件实例上,为什么要使用<code>bind</code>方法来绑定<code>this</code>呢,这是因为绑定事件的回调函数(如这里的:<code>onChangeHandler</code>),它是作为一个中间变量的,在调用该回调函数的时候<code>this</code>指向会丢失,所以需要显示的绑定<code>this</code>,这也是受<code>JavaScript</code>自身特性所限制的。详情可参考<a href="https://link.segmentfault.com/?enc=CXiNtkoKpaE8kIdukoZ4Og%3D%3D.DkGSFrgLeSiEm6asDVoHhV%2BfXvJG7M0Oko2B5j3hk1jguyNSEvpaxjRqgyu731XdjVjTHQA0LnBnEHMkVmIHPQ%3D%3D" rel="nofollow">React绑定this的原因</a>中的解释。与之类似的是在类组件中绑定合成事件,我们也一样需要显示的绑定<code>this</code>指向。</p><p>4.<code>map</code>方法的原理。<code>map</code>方法迭代一个数组,然后根据返回值来对数组项做处理,并返回处理后的数组,请注意该方法不会改变原数组。比如:</p><pre><code class="js">[1,2,3].map(i => i <= 1);//返回[1]</code></pre><p><code>jsx</code>中渲染列表也正是利用了<code>map</code>方法的这一特性,并且我们需要注意的是渲染列表的时候必须要指定一个<code>key</code>,这是为了方便<code>DIFF</code>算法更好的比对虚拟DOM。</p><p>5.React中给标签添加类名,我们是写成<code>className</code>的,这是因为<code>class</code>被<code>JavaScript</code>当做关键字。而如果我们需要动态绑定类名,可以看到,我们使用了模板字符串,在这里更像是写JavaScript,比如我们可以利用三元表达式做判断。</p><p>6.React中给标签绑定style样式,我们通常可以绑定一个对象,在React中,我们绑定动态数据就是写一对<code>{}</code>花括号,然后style里面的样式我们通常声明成对象来表示,比如:</p><pre><code class="js">{
background:"#fff"
}</code></pre><p>这代表它是一个样式对象,然后React会在内部去将样式对象转换成样式字符串,然后添加到DOM的style对象中。</p><h2>2.Progress Steps</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813437" alt="2.png" title="2.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=HqWusWkIGtM30JlacFgobQ%3D%3D.eX8Lz7dke2hU%2Bd7JoiFSeNPXeZ%2FrF1%2FVm0nMDpmZYSjfsFtwvCDe27hc2JW4h9%2FDe1xIvtBoQHbfosDZgj9YM20HsfbwT5ga7Miu8QvDj80%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=IBl0K0IuQmEyBkxkCTqo1g%3D%3D.6plhjgykhxYFztgEtQry1wZZFN%2Fo6upPZ2C8BZMCEUxCEtx%2BaSKCcOKk8BqzOP4fgd8%2BxZSVluLipIq3Wbj%2Bow%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>与第一个示例所用到的知识点很类似,相关的不必做说明。接下来我们来看不一样的。</p><ol><li>父组件向子组件传递数据,我们通常都是使用<code>props</code>。可以看到以上的示例,我们暴露了4个props,即:</li></ol><pre><code class="js">width
stepItems
currentActive
onClickItem</code></pre><p>width就是设置步骤条的容器宽度,这个没什么可说的,stepItems就是步骤条的子组件,是一个数组,也可以在数组项中写jsx。而<code>currentActive</code>则是传入的当前是哪一步,是一个索引值,通常应该是数值。至于<code>onClickItem</code>则是子组件暴露给父组件的方法。</p><ol start="2"><li>类组件的生命周期。在这个类组件当中,我们使用到了<code>constructor,componentDidMount,render</code>的生命周期钩子函数。我们可以根据语义来推测,当一个类组件被初始化时所经历的生命周期钩子函数执行顺序一定是<code>constructor => render => componentDidMount</code>。从语义上来将<code>constructor</code>是一个构造函数,用于初始化状态,然后初始化完成之后,我们就会渲染组件,然后才是准备挂载组件。</li></ol><p>额外的,我们扩展一下,根据<a href="https://link.segmentfault.com/?enc=6Wsluy%2FAWAlQSOoSLzWncQ%3D%3D.xqIB1%2F7%2FA1Y9cbe1Lm7dLcUJH7iLVxDdC4%2B6RIxM6hyiNic5JR%2BzSJeupgoGHCkE" rel="nofollow">文档</a>说明,我们可以知道详细的生命周期。React组件生命周期包含3个阶段:</p><p><code>挂载 => 更新 => 卸载</code></p><p>在组件挂载之前执行的有:</p><pre><code>constructor => static getDerivedStateFromProps => render => componentWillMount(即将过时) => componentDidMount</code></pre><p>在组件state变更之后,也就是更新之后,执行的有:</p><pre><code>static getDerivedStateFromProps => shouldComponentUpdate => render => getSnapshotBeforeUpdate => componentWillReceiveProps(即将过时) => componentWillUpdate(即将过时) => componentDidUpdate</code></pre><p>在组件卸载之后,执行的有:</p><pre><code>componentWillUnmount</code></pre><p>另还有错误处理的生命周期,也就是在渲染过程,生命周期,或子组件的构造函数发生错误时,会执行的钩子函数:</p><pre><code>static getDerivedFromStateError => componentDidCatch</code></pre><p>这里面有些生命周期我们并没有用到,有些则是几乎涵盖了我们后续的所有示例,所以我们一定要牢记组件的生命周期的顺序。</p><p>但是就这个示例而言,我们学会的应该是如何封装一个组件。</p><h2>3.Rotating Navigation Animation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813438" alt="3.png" title="3.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=oqsNGX9xEJoW3KK2vBuDAw%3D%3D.iWpMV%2BnRUBPjOGcSHZ1DOLiqhzNIPGF8XdzWC6Y5qXxPpkOgg9EznlBO2eVtW4UD1EprJAfmfLPWZdK%2FgDWPcN7TlNFX0jrL7EQdz9yw%2BM4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=eJim3ZHNaw8FJo9n7B3PaA%3D%3D.89bBOL8VgS1s82NSwUu9dzgvwzFwX5KkCuuAgokS6SjyUox9oCfEy0r4VkoVGwXxB%2FU42q2MxgOgh%2Bga17Y6qw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>一些相同同前面示例相同的知识点自不必说,我们来看不一样的知识点。</p><p>1.模块组合导出</p><pre><code class="js">//components目录下的index.js文件
export { default as Content } from './Content';
export { default as LeftMenu } from './LeftMenu';
export { default as NavList } from "./NavList";</code></pre><p>可以看到,我们可以将组件如上面那样导出,然后我们就可以单独引入一个js文件,再引入相关的组件即可使用。如下:</p><pre><code class="js">import { Content, NavList, LeftMenu } from './components/index';</code></pre><p>2.react组件如何渲染html字符串</p><p>react提供了一个<code>dangerouslySetInnerHTML</code>属性,这个属性的属性值是一个以<code>__html</code>作为属性,值是<code>html</code>字符串的对象,然后,我们就可以将<code>html</code>字符串渲染成真实的<code>DOM</code>元素。如下所示:</p><pre><code class="js"><p dangerouslySetInnerHTML={{ __html:"<span>测试代码</span>" }}></p>
//<p><span>测试代码</span></p></code></pre><h2>4.hidden-search-widget</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813439" alt="4.png" title="4.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=WISGg1h5QsGDnCf6iiFnew%3D%3D.DxAjcO03dJhy6iewsY4jFOILOjXy9KX5GvGhCl5kNSdg5iJrDe%2B6Be9AwdRv4410emUg7ngsqBtZczGmaiveRoqlGCqIX9Go7xQYHzaL%2FK4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=tzn%2F3DukWNGMXI44E0xPRg%3D%3D.%2BLaE%2FiPKu5OT7NJK%2ByZjU0%2FSjemHTF10zHM2S6aqsZmn514Clz8XAfw5%2FYPuSTNqFIh0etj8pu5y9NpT9Za%2FxA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>本示例同样的与前面的示例有一样的知识点,相关的不会做说明,只会做不同的知识点介绍,后续的同理。</p><p>1.判断数据的类型。利用对象原型链上的<code>toString</code>方法,我们可以得到一个字符串值,这个字符串值的格式类似<code>[object Object]</code>这样,也就是说我们可以通过这个字符串值来判断一个数据的类型。例如:</p><pre><code class="js">const getType = v => Object.prototype.toString.call(v).slice(8,-1).toLowerCase();
const isArray = v => getType(v) === "array";
isArray([1,2,3]);//true
isArray("");//false</code></pre><p>这里我们应该知道<code>Object.prototype.toString.call(v)</code>返回的就是类似<code>[object Object]</code>这样的值。所以我们通过截取开始索引为8,结束索引为该字符串长度减1,也就是这里的-1,我们就可以获取到第二个值<code>Object</code>,然后再调用<code>toLowerCase()</code>方法来将全部字母转换成小写,然后,我们就可以知道数据的类型了。比如这里的判断是否是数组,那么只需要判断该值是否等于"array"即可。</p><p>2.React.createRef API</p><p>有时候,我们恰恰需要操作一些原生DOM元素的API,例如这个示例的输入框的关注焦点事件。这时候这个API就有了用武之地,我们相当于使用这个API创建一个与DOM元素通信的桥梁,通过这个访问这个API的实例的current属性,我们就可以访问到相应的DOM元素。</p><p>更详细的可以参考文档<a href="https://link.segmentfault.com/?enc=z%2F7%2BzsnbDjOxkz6s1ZL9Pg%3D%3D.zCWIl5yIAlXrqpQIXZEL0fBFuRZ7DhAmnW5TSdO8omI6arRR3nDuVNVQeCPf%2FIQnjAOrBifyjOU5Vszgj7gMAg%3D%3D" rel="nofollow">createRef API</a>。</p><p>3.如何封装一个input组件。</p><p>这个示例也教会了我们如何封装一个input组件。</p><h2>5.Blurry Loading</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813440" alt="5.png" title="5.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=R85Vjl4mSVPTlK6hPH%2FuLw%3D%3D.vyvXMtGZhm4gFyH0d7PRAhclbN%2FOZm7fAFoPa%2FbfaM%2BRlhEnYnzXU7n5islDmKMbrivAHnM%2F3wgLlXZ60hzV0uVZFkrrIVnfJJCcjHjoOno%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=I5NcwV%2BDrfSQ7CjUyrljzg%3D%3D.TG9u4Qyzzri8aKhqXdJ9APai8Xn0H%2F6Lmv900xDL%2FbGck4DbI767XZQnDT5rNK6BJxWu3tdLrf%2B0woKoDzYyFg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>在<code>componentDidMount</code>生命周期中创建定时器以及在<code>componentWillUnmount</code>中清除定时器。</li><li>类组件中的<code>this.setState</code>更新状态。该方法接收2个参数,第一个参数则是我们的react状态,它可以是一个函数,也可以是一个对象。第二个参数则是一个回调函数。谈到这里,可能就会提到一个问题,那就是setState到底是异步还是同步? 答案如下:</li></ol><p>答:react中的setState在合成事件和钩子函数中是"异步"的,而在原生事件和setTimeout中则是同步的。</p><p>之所以是"异步",并不代表在react内部中是"异步"代码实现的,事实上,它仍然是同步执行的一个过程。</p><p>只是合成事件和钩子函数的调用顺序在更新之前,导致在合成函数和钩子函数中没法立即拿到更新后的值,所以就形成了所谓的"异步"。</p><p>事实上,我们可以通过制定第二个参数即callback(回调函数)来获取到更新后的值。</p><p>react.js对setState的源码实现也不是很复杂,它将传入的参数作为值添加到<code>updater</code>也就是更新器的一个定义好的队列(即:enqueueSetState)中。</p><p>react中的批量更新优化也是建立在合成事件和钩子函数(也就是"异步")之上的,在原生事件和setTimeout中则不会进行批量更新。</p><p>比如在"异步"中对同一个值进行多次setState,依据批量更新则会对其进行策略覆盖,而如果是对不同的多个值setState,则会利用批量更新策略对其进行合并然后批量更新。</p><p>更详细的可以参考文档<a href="https://link.segmentfault.com/?enc=NMV%2FBtXQnbK3UJ%2FeLDsAmg%3D%3D.LbRoznVXPXA5bSKOMSQQeY7vQFZQxwyK66gJD%2FFqytrRmX3qqh6FyRx5CKKDg7MXAMTOLEs5MMvxt7mW467G%2FA%3D%3D" rel="nofollow"> setState </a>。</p><p>除此之外,这里也有一个非常重要的工具函数(这在js的实现中也提及过),如下所示:</p><pre><code class="js">const scale = (n,inMin,inMax,outerMin,outerMax) => (n - inMin) * (outerMax - outerMin) / (inMax - inMin) + outerMin;</code></pre><p>这个工具函数的作用就是将一个范围数字映射到另一个数字范围。比方说,将<code>1 ~ 100</code>的数字范围映射到<code>0 ~ 1</code>之间。<br><a href="https://link.segmentfault.com/?enc=K7%2BZuscezWzn2lX1G0lf6w%3D%3D.65fwkzs9LXondDljPnGpJtBzaqxLuRqi%2BKI6SnLlRFwoZG%2B%2FkZJAUAoe2jS3BWFixwlJQu7%2B3DPTNIpzwGRLynRpFUOjdB5ruYL7WNVbpu7ujsIZTH6Doyj66%2BCvZvI9ytaFoe0IF9X3gwuSSX8mptf7T6IfhdcbQ23wz61%2FkMk%3D" rel="nofollow">详情</a>。</p><h2>6.Scroll Animation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813441" alt="6.png" title="6.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=vGaTzyH4N0xPVwo5%2FcTvWQ%3D%3D.RYyNAhyn7%2B0d6d0iL4zi8793XVO2vLDUed1i7LSf5VgTu8Jkc9DjFnPdeAizK1P2059f1EKSyLyTxb1Cy8g42525lLibSBSzfbyGlTS%2Bdqo%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=j2AXqai9AJqfYQSTWXFgCQ%3D%3D.hUcr%2Bhfv4X41zN1a3Sm3e3ox93s1Scx0NUkErYLqQlHjTH%2FMcgChJQyrxLEblHq%2FDYUOVUsZI1QOVafqwyj3Lg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>1.动态组件</p><p>我们通过props传递一个值用来确定组件名。这里以<code>Title</code>组件为例,如下所示:</p><pre><code class="js">import React from "react";
const TitleComponent = (props) => {
let TagName = `h${ props.level }`;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;</code></pre><p>虽然核心代码十分简单,这里需要注意,React组件需要首字母大写,这算是一个约定的规则,其次我们通过props传递一个level用来确定我们使用的是<code>h1~h6</code>哪个来做标签,事实上这里我们应该对level做一个限制,只允许值为<code>1~6</code>。</p><p>2.Fragment元素</p><p>这个元素类似于一个占位符节点,我们知道,当两个元素并列在一个React组件中,是不被允许的,React组件需要提供一个根节点,但有时候,我们不需要一个实际的元素作为根节点包裹它们,所以我们就可以使用<code>Fragment</code>元素来包裹它们。该元素还有一个简写<code><></></code>,事实上在Vue2.x中也有这个限制,这是受虚拟DOM DIFF算法所限制的。</p><p>更详细的可以见文档<a href="https://link.segmentfault.com/?enc=xvh6yB6zVbTYonBT7J6blg%3D%3D.kk3c%2FaYgJngW09jBSsMkxbKcDzQ4zOHDel%2BviyIvHYDScoNagC03lLOrtW0J2Sc31OuhRbcOiAPCi7XKrC%2FYHA%3D%3D" rel="nofollow">Fragment</a>。</p><p>3.函数防抖</p><pre><code class="js">export const throttle = (fn, time = 1000) => {
let done = false;
return (...args) => {
if (!done) {
fn.call(this, ...args);
done = true;
}
setTimeout(() => {
done = false;
}, time)
}
}</code></pre><p>函数防抖与节流可参考<a href="https://link.segmentfault.com/?enc=VW%2FPxJkBON3Yy7CAZxZdfQ%3D%3D.NGBWvjd4BRhvczysC%2FwppXgLv1CMVCWM4pe9E8JETnEw1Izx7W7DPKuJhkDcQNky" rel="nofollow"> 这篇文章 </a>。</p><p>4.监听滚动事件,事实上这里的实现原理同JavaScript实现版本一致,只不过稍微转换一下思维即可。</p><h2>7. Split Landing Page</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813442" alt="7.png" title="7.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=byVaLaifs4e%2BLnDuGworLg%3D%3D.ED4S0za%2B0tOB6i4lrHY0Aby2eNkqxoHCpwCzrVCUokHHcwgIoD0XzVpEZo7S8C12n4HP%2BoDgJodsUs%2BLHBDQb0%2FvSjPTAzWI7z%2B4Hmc4%2BD8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=F5SwW2Dg6Wy71aYXopb%2BFg%3D%3D.UTCZtihTWu3C93wS2enAgY0Ad54cz2oS3HUDLkx8vqdfn1L5eIHS6Zl4UqnJSonmvE2FYRBdWt3rduj01Kp%2BTA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>8.Form Wave</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813443" alt="8.png" title="8.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=qoodS3YTf5U6ZpDU6b3Mng%3D%3D.tPRzVPzvBkUfC3n8qNMXkM8CLqFK4Skk6rtMA%2FosM%2BKf14hNTymLQcV28jHiJgdwkUhUtC22GZzIwDrgvbyRrbbi97AA8PL8TzFsrusj8pI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Gf0sxAzIiPJZbfbzQ2lFvQ%3D%3D.OYrs7JNALFgcWql4vR8387Tt1lxRIIdU2xFJPiEVcW4yzTr9spSIb%2Fo88Kb95TtB3N3TiEx6Ynov%2BDkCvbLkdw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>setState更新对象,如果state是一个对象,我们有2种方式来更新。<br> 1.1 利用Object.assign方法来更新。<br> 1.2 直接覆盖整个对象。</li></ol><p>以上2种方式伪代码如下:</p><pre><code class="js">// 第1种方式
const loginInfo = Object.assign({},this.state.loginInfo,{
[key]:updateValue
})
this.setState({
loginInfo
});
// 第2种方式
let { loginInfo } = this.state;
loginInfo[key] = updateValue;
this.setState({ loginInfo });</code></pre><h2>9.Sound Board</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813444" alt="9.png" title="9.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=GWVZQDr3S7ERODtHnLKy2A%3D%3D.svbevNhriMUR9OyaG8osSFGY8FzSJwZKukcc2cb8hTAB3agyG598OfRC%2F6UH59Tufatpdi52Tze7V8Y0TTvMZd00UXntJy87XWKhPT93XmM%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=0qgz%2FMQih1GeUWyKb%2B0C3w%3D%3D.6EegLogR1DxM99DfSAO5PHKAGkDSBJ8a1Mc2VtNdk2E5kVFpk797L1HUIW9GTrfG6TwKlR3EDKMK%2FbA1xhlOVA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>10. Dad Jokes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813445" alt="10.png" title="10.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=t14YInzoqzXKVdeCp5ckcQ%3D%3D.UNyeTflMYeejgaijntdBvrdwAJ22PebvHTAVZpmstw3ZhMef6C%2FlnBuZ6A%2FrkVztVvsHov%2F9hO8Jbr9nC3%2B3wtUWpGSHG34A2j%2B3%2BHRYdJ8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=xIiYIX4vwPN5j5UbJiPuIg%3D%3D.9SG89CHXYlUxvbGdbNKA5esabfwf92DFiN%2FRADkShr8ExPGooh47ls3ecaggR93QTA4YQ3ow7By47tPXDHuYmA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>Suspense组件的使用。该组件可以指定一个加载指示器组件,用来实现组件的懒加载。更详细的文档见<a href="https://link.segmentfault.com/?enc=Mr6mY%2BwCa3BdHqhK6Ujpnw%3D%3D.MBfB1z2ychE3agj0nwTWPtZnZyWnD6g%2BbUwGEBUCpnMX9u0TJ%2BJnd18GKqWFFRDzYcNSmJEpQ%2FSDJIBY90JR4g%3D%3D" rel="nofollow"> Suspense </a>。</li><li>接口请求通常都是在componentDidMount钩子函数中完成的。由于不能直接在该钩子函数中更改状态(react.js会给出一个警告)。所以我们需要让接口请求变成异步。</li></ol><h2>11. Event Keycodes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813446" alt="11.png" title="11.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=wgyWYlhBQD4GYgNwOHDEZg%3D%3D.WPtF1K9TXuGDn6UGUjAnOL0pF2L%2BRoPaFPzGSu0FHvUqOEx%2FIxieUWheqAfhJXXUKVCaDIZILJ9MpxItN8WefOtDne4pV%2BIJnmNyK1U5Imo%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=iCfmgVLSlmG94lqphrh35A%3D%3D.4Zx2UyZRVVScECdZRtmaekUPcmU2bQ1EKXCNMqQLIGvUwxoWPnIu%2Fr7srg4mdUEsntltlkeMkKHwhkL4XNUJyQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>12. Faq Collapse</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813447" alt="12.png" title="12.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=p5jhhMhtiEB2rkCWTBD2MQ%3D%3D.hdf8BMfHEaFGXCR8HuyDDZZlEi3aOIIPbWpYpAZ7jq7QL%2BazbuMe2ZJLnZUlzBsn2oj6ARH%2FFGUtPdKIp9eD0l6wipN3fjm5LEgs7He%2Bh4E%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=8W1%2B5Va7eCFehXgPLK5PhA%3D%3D.RxB8BC9zDl0WwoF0hpz%2FcK5yuenYCeTyOE4xvi72ZyqVkliEzphzFe9UdF9Q%2FBL10jJVyhlJptpbwdaXD6U0lg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>13. Random Choice Picker</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813448" alt="13.png" title="13.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=CUNLLfgRdleBCDxhJhYZeA%3D%3D.UbfnD6UtOeT7Y%2BI%2Bm26yj49hNqWKTfX4Zgfz3rz%2FNP2xJCVAzyVIvz8wx4GOWaUv%2FxNgOqg10AgQcVxSPAyL%2BljJqkrR6%2BdOnGlOy%2BgDTzY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Rpr2MXmMMRr8DTBygJC1lw%3D%3D.1jEiFeJjfEzmYcGwMfLQlxy50JKjTVscHg3x006pvLLA6v2RQQ2Z85EuJOEwjqCRJ6%2F1eVJ4891Z9EP8fnNorQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>14. Animated Navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813449" alt="14.png" title="14.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=6f12brvbaT4MatjZbI5WEg%3D%3D.lMMcOF8PWoQAVhtc0mvJlw%2F%2BuWCdD%2FpJZ0OdG5tEqHYur2H%2FrQBuXoH7UcgUbm%2FWwx2bRlQY%2FDtodI7xTIUgQrMM77blVhFD7PXRZqROsF4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=XqMYvPHzsFb7IETk4yCzfA%3D%3D.g%2FOFerVob86bsfiTdl6QuecV5AKOaJ6TSQ2LZWq4p7%2FVCeAxOB%2FYLAdlpSoF2G7FE4TjIE48bDNxGJwsv%2FSFcw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>15. Incrementing Counter</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813450" alt="15.png" title="15.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=rdUoPKFQgB6nWtY4iCI8Uw%3D%3D.l202DIEcfVXl0fAZgvTLihe47F4QyPA9dru2JXnU%2FMZ7%2BvG5ypEvCvmqIXx19g2xAfHtZAy8TGiYNQ1cmx0HbWQJ5yoRf9p8Di2LeODrmaU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=4%2B2ijmQ3xVZgFJTUz%2BdH7Q%3D%3D.MiVjeLvNHnVkuCRWuVqEKnfH39TFpDEVbDts9AARN9LXoQ94yT5WyfsmyJeq%2BuEL7wze8FvUDJMBiR%2FHghQrOg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>react.js如何更新数组的某一项?在这里我是更新整个数组的,或许这不是一种好的方式。也希望有大佬能提供思路。我的代码如下:</li></ol><pre><code class="js">startCounter() {
const counterList = [...this.state.counterList];
// https://stackoverflow.com/questions/29537299/react-how-to-update-state-item1-in-state-using-setstate
// https://stackoverflow.com/questions/26253351/correct-modification-of-state-arrays-in-react-js
counterList.forEach(counter => {
const updateCounter = () => {
const value = counter.value;
const increment = value / 100;
if (counter.initValue < value) {
counter.initValue = Math.ceil(counter.initValue + increment);
// use setTimeout or setInterval ?
counter.timer = setTimeout(updateCounter, 60);
} else {
counter.initValue = value;
clearTimeout(counter.timer);
}
// update the array,maybe it's not a best way,is there any other way?
this.setState({ counterList });
}
updateCounter();
})
}</code></pre><h2>16. Drink Water</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813451" alt="16.png" title="16.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=K9Z7zLK%2FmwkqVm%2Bg%2BH2EHg%3D%3D.Lxlhx%2BycP9kgQKAQhEhN5qL%2FQ4jIaDUjQ6DGTAZ8GRgLicw4wnv8l9FteSNNw1OONPhtRVAcQsX3SjUzetcARXHqq5VFj2YOfmV5R3ZpIqs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ZnuEki1cSai2IJQ1T2taRQ%3D%3D.APESlC56BMYHkCS9lRoRu7%2FkoxZ9vxJZQWDSjNoqfT1wqSnNuxXXJdQdGtIEP7CKgmZdUEdD0NJoanJNCbyJAw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>这里踩了一个坑,如果使用new Array().fill()来初始化状态,会导致意想不到的渲染效果。所以这里直接初始化了所有的数组项。</li></ol><p><a href="https://link.segmentfault.com/?enc=Sfy%2BCfYeRVa5W0DUy3JDFQ%3D%3D.bTU0sPxAxiW5H82h69iw0CVgWHvyRRSvdMBfQ0L3mTqluPoOaQZxoprTqs5Py6Q9rEXYTv2qo9R5%2Fvia5biGhJvXJDIC9PwQMkxjDDlO%2BkM%3D" rel="nofollow"> 详见源码 </a>。</p><h2>17. movie-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813452" alt="17.png" title="17.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=Uep2wwYjMNPgumJiUQEu4g%3D%3D.TMCCqOQf3vM42ZkfQ8kEKmPjdSMVgsz6tdzHUopTv3mUHxqGfJ%2BFSFYy2NSr8V0nwMAzrVw5XurJzJrNk46hC2xflqIJdSYU37vJpq0ZrGc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=OqWN31i4RprWxwO3SSyUWw%3D%3D.qtwoPTrEt3G9a0bwnCJjHmQpLt5K98%2Buatpm%2FSEc6vTh8N%2FwkeSOHqRX1qr%2Buo3MNEkc3GBqUgArxXSn379ibA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><blockquote>PS:这个示例效果由于接口访问受限,需要你懂的访问。</blockquote><h2>18. background-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813453" alt="18.png" title="18.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2Fl1eSxa0KK2qsPPzpJagFQ%3D%3D.I16SyayLKy5A%2Bf0R4rDzPaOQ2pWAJaiHDq%2FMwDwgdu%2FjmbQTGmkMVZYzwGt5EQg8gGCu1iVM9gZk9ceANiSut5%2FKS3sQLoKvodT7hfGsI%2BI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=s4LzzfOuc8rTpl1nW3NZkQ%3D%3D.A6gpxQ%2B5ubBHn1yazZSiFygysuTVn4jEMMvKBY4te4eP05CXIBEi0ck3ldcIfHvNn8C01Ofkylx6SXjjPiryOg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>19. theme-clock</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813454" alt="19.png" title="19.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=3%2Btl%2FPa6I%2FuzBWK2wwyWag%3D%3D.08bufWotP3VmXPmXIjtEtHGK%2BD%2FbYGGyPhacvWgjAHflt8XJoN4RHVFuriR0%2FGf1UJXoYjeBlGMISQAGlC0E8h4oAx8lXx8BtLlXHK865IE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=FrBAM3GDt%2Fde6v9Naq4wuA%3D%3D.9GZg86o%2BkbOHklkilFHXR42EYp2yXfo%2B0TNbEUtHXe4ovuEYk2VD4hQpL6yC%2FzCNXIIk5Pcn8Ykw%2FbQLpQxs5Q%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>中英文切换是通过定义一个对象来完成的。其它的没什么好说的,都是前面提及过的知识点。</li></ol><blockquote>PS:本示例也用到了与示例5一样的工具函数<code>scale</code>函数</blockquote><h2>20. button-ripple-effect</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813455" alt="20.png" title="20.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=qBQPorTnjEUzhFFBFtf4kA%3D%3D.%2BxHLQLwntIULoSO5zb3L9XzahSucoIC5Y2RmadMwuWJQzbNhhZl9akQvsKUGa2jRbXpRyl2BnIqryl44bOpnuIT0DBtQqgPeO%2FS2k4qKl%2FQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=sGVySErKcbAekDW5E27nvQ%3D%3D.IWDwxiDexdJuAO30YFnYwkL10yELSfj7NteMGvN35d9bJZZkRsXkcOvx1mu4z1KQMtu7wA9OmR39R%2FjNfWkP3w%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>可以通过调用函数来渲染一个组件。</li></ol><h2>21. drawing-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813456" alt="21.png" title="21.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=aupoFT2xBlKwTm%2Bpt7gg3Q%3D%3D.A21%2F9kgR6o7bhQ6JAPoqH3LvfEf3divrrKQWudwvkM9JcumuRfBN%2FXkLGuT7GtD%2By%2BGi%2FIue9DP39vmRGO8S3T4hbInjRaRoapLFLmuvWSU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=XHwf9RDvjpGal1TsVJrQhw%3D%3D.PCazDYikOlX7LIQj8F3D%2Bp7qxxTQFQpoX6MAgUb4UWVm7BWjLVqL7RoG2Fzfk5V%2BtL16Pd3SpBSrZvqhUQsvLw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>在react.js中使用<code>ew-color-picker</code>。</li><li>这里踩了一个坑,也就是说必须要设置线条的样式。</li></ol><pre><code class="js">this.ctx.lineCap = "round";</code></pre><p>否则线条的样式不对劲,虽然我也没有搞清楚这里面的原因。毕竟js版本的实现也没有需要显示的设置这个线条的样式。</p><h2>22. drag-n-drop</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813457" alt="22.png" title="22.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=j77UkGtQD6TL2fOkf8kmaw%3D%3D.UMTaEQUCIWrH9ynU5x%2BQZgC%2FH3L9WS3eywbCP4IfBScFmEV0vqeED7AWP2%2B%2FpgrzOfNe0vBPBeSCdCfetiJ508ERPI24lYQjjFZlUkEOnYk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=%2BGt%2BXCSGJtbHNaLXpMmd4A%3D%3D.Ua5hlOfgPrxMB7jUtcVpkWRloLwflEIE5BTYrcF%2FJZHON05IdD9Od70E7I1Os457xGdrtdjCxcusAjtFhocvkg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>这里也踩了一个坑,<a href="https://link.segmentfault.com/?enc=YHOsU3xLsCr%2FdIPwpm4Q%2BQ%3D%3D.RdC8oqWftmDDuUTw07n%2BAcZxwE9SWfWEglO9JpyHXenPGGGd101XDsl%2BMXWzkMO2kff2f6qr3OdrYHfcao3YEdFEg0kcYKg8bO9qLExUzWQ%3D" rel="nofollow"> 详见源码注释 </a>。</li></ol><h2>23. content-placeholder</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813458" alt="23.png" title="23.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=43lDDsrl%2BwJDiT0CjRxjRg%3D%3D.jN4m5C3U2a5MP2LeCxLueXZES66Wxs7nvArMJBfW6LjNwOMSvu2hxc5bbA8gB1baMN31NmoLypYOFJxGPxpnauZCKWXnvCppug0j1BAIZ84%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=gTecDbJ9kU%2FZu51ae0w07g%3D%3D.pSoild9GChCvNpUJqIAM2icDN828%2FMC5SDzGHqBigyf%2BWQU4McJOHstRd043u9R3%2FTVF0FvTSJ3pGAqkRgCENg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>一些判断react组件的工具函数。如下:</li></ol><pre><code class="js">import React from "react";
export function isString(value){
return typeof value === "string";
}
export function isClassComponent(component) {
return typeof component === 'function' && !!component.prototype.isReactComponent;
}
export function isFunctionComponent(component) {
return typeof component === 'function' && String(component).indexOf('return React.createElement') > -1;
}
export function isReactComponent(component) {
return isClassComponent(component) || isFunctionComponent(component);
}
export function isElement(element) {
return React.isValidElement(element);
}
export function isDOMTypeElement(element) {
return isElement(element) && typeof element.type === 'string';
}
export function isCompositeTypeElement(element) {
return isElement(element) && typeof element.type === 'function';
}</code></pre><ol start="2"><li>如何封装一个卡片组件。</li></ol><h2>24. kinetic-loader</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813459" alt="24.png" title="24.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=ta7GaFFyLDdQtMjtk25yOg%3D%3D.55xI20nKy%2B3MQNmSNbo7yVKOnkzGS1MmeKJ%2BHVwmzHQpM0asGGS9wNQOzkU2xp%2BquKKtGzVv6m5ZOiwdZGgZSljWMnt7fDv7PSKr7WuAXwY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ltlIGzb%2Fhtn%2FeCDQnLrPmw%3D%3D.Jwrm%2F6PXf0%2BJbuj7NAlyGRKTEOBNlVycawqOd4SLhd5HcAtqjTxyLFBq7L5BvgVElZXq9H1JkekQ9VffHgpOTg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点前面的示例都提及过,所以这里不必赘述。</p><h2>25. sticky-navbar</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813461" alt="25.png" title="25.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=z%2F8kmdz1mz01WlIHJ%2FTUvQ%3D%3D.4D8mVe394xqZ2IVg%2BIc2g%2BUWZlKrOdL0TmOy8fTNhVZ2Z4PgpTRQTInActPpx3fnJufjQeJDAIx%2FQraAoT8J2VocwAFHwWOVGJ3aHGDbi%2BU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=gziiEKSak%2FX20kmKdSwRag%3D%3D.3Mu4%2B2NHdQr9CwFmv4OZp%2F%2FnIWhiZIP4LmBuSCvILaGv4ASTO1CDxvwmK014OjAHZiGAve6Ms%2BhPs4I1ECJHhw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点除了一个工具函数,其它的知识点前面的示例都提及过,所以这里不必赘述。</p><pre><code class="js">export function marked(template){
return template.replace(/.+?[\s]/g,v => `<p>${v}</p>`);
}</code></pre><p>这个工具函数的作用就是匹配以空格结束的任意字符,然后替换成p标签包裹这些内容。</p><blockquote>PS:这里也做了移动端的布局。</blockquote><h2>26. double-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813462" alt="26.png" title="26.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=FMigZrPiN29YXHcm7EIM%2Bw%3D%3D.S6aAFVDIeMqRLMjC1GmfsUgyOOOIF%2Bk5AGGLggk2OdXHWLCLYKODtJX9VceATMuo3reivZQFHagLRs3k%2BxgapCmIeC6F0m3m%2Ff7%2BI1Kk3fk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=NtmNAh08BWKHTunuWKcs4w%3D%3D.qgVcfvxM9kz8Oi%2FpZqAdGWfq1RNYEVSw2cX0jieahzLv2X5jV6qTnjvoc%2FNrTNRxn0niYtYMQIy3u883z1OCEg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>27. toast-notification</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813463" alt="27.png" title="27.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=LmeeI5lHxZ7XdhDzOW%2Fsrw%3D%3D.aD39zMd1zgJIR%2FCEtdgOgVzmmBdcm6HdaggtQn2f3icVDDmwONGvP1A6YXpJSXpkEXXANWautWXPr0e1a0gq3e1DW74ncuSRTGmh5OdotwA%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=e3sxO2vx0Y%2FcL69%2B9hIFmw%3D%3D.HMcl1tofNeA%2FB9CNsgikyexPjTyg%2B1gp0YDyYqtCyARy65808D7OUROQ8UkBvE1c9Sb8jTnrlk%2Frn76GL6f3hw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>可以利用<code>ReactDOM.render</code>来封装一个函数调用的<code>toast</code>组件。</li><li><code>ReactDOM.unmountComponentAtNode API</code>的用法这个方法会将从DOM中卸载组件,包括事件处理器和state。详见<a href="https://link.segmentfault.com/?enc=ixQza5HmLfTQl3tGmh%2FUbg%3D%3D.DgkINRy%2Fedm16%2Br6gUtpH%2FFF7FJIBcyq6bWINBmkZh1iu4l%2B6HddwgRuK3mKxY9gJd0PhuwbHWuyCG85TYqLCA%3D%3D" rel="nofollow"> 文档 </a>。</li><li>getDerivedStateFromProps 静态函数。详见 <a href="https://link.segmentfault.com/?enc=7jVCe2XOGbVktPk5DtBfbg%3D%3D.C9aZ64sKwt1RVOWJFShllyxrfwZs4h7o4qaOZi32KVnBo8P2VXlAxQ9t3len%2F5kRFCPWSKMiATtxDJ4n1ncl0hLpIAHFulBsAwliWJ%2BpAic%3D" rel="nofollow">文档</a>。</li></ol><h2>28. github-profiles</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813464" alt="28.png" title="28.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=OHZyF3utn%2FPwzcp62yuhHw%3D%3D.elkuVGFZ51fJ%2BV%2FCG84Tsknsvv0m5HMZHpvBIHRXhiuGdSZviL4PbiAkSJu0wM1fVURwL7DeN8zCC0qsjQMYmDtBeSbXoPfRNWp%2BJr4%2Fi7U%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=mq1Xpll5cIwLyjfLEbRUOA%3D%3D.b0FnnO178Fm%2FkAyqWUJRxpoG2Ucfb3RqOzDOtLE2J98%2BhsJfT%2Fe79Nt3aFK70ElDsWYYRt3op5uPxLjPIPcEkw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>29. double-click-heart</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813466" alt="29.png" title="29.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=ijgNehwBKuU%2BBlOUXFQJIg%3D%3D.4xkdLa86%2FacmzI542EDKsk0BVfx2luqAqSX2MuJIm8JmZfEr1uWd5uO9IJEqGa9tGKnAeOzYmE%2B4wDWJiWMyV6B1xNH2xaJeYSePqUiZr%2BQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=1S%2FHQEvSZFJd4HjfBHicEg%3D%3D.BvfTe8jLjMBuDwhkdwMUPImI4xCVERcmorhWD3pIPsJ5%2FeLD60CnP3Wl72PUQMQJj9x%2FiAhX0PboIQ3gs0iCVw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>从合成事件对象中读取原生的事件对象。即<code>nativeEvent</code>属性。</li></ol><h2>30. auto-text-effect</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813469" alt="30.png" title="30.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=d9X5gRIRlF09oJdx0S8HXA%3D%3D.tSicj1dd3b4FPl4LWj2RnS0FppWCJ93qWjzxwx1xiaCafEP6y%2BH%2FFWO%2FwfpEQ4m8vaGcGQ1fnECsc6QvirS6N1jHxUuUNpGEoI9whzgARfg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=qxhSS3mpyIgVn6yf%2FsA7Ww%3D%3D.lBFQhgeQsp1hpazOerquRb5EE22petW3U3Nf13%2BzbFQXR%2FyZ5CWDlSZiDZZ7khRTep3X9dFC6dHPJPwcvTPKxQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>31. password generator</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813470" alt="31.png" title="31.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=7Da0%2FhsvwO%2BSKLkoB%2Bpy%2FA%3D%3D.Ur6Ac4vfmc%2Fn0sk1nQ1BeDwWYSDDBHNFdqY%2BewxLAUAKWHqPiGMFOsG4pofdWmFj4qWI2PN%2FIN1MKGoPp%2BKotermkwW1QgxLjhwKfeaJhwI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=c%2BHtVkX5UeU3uk%2F%2B0HJDwg%3D%3D.lHY%2FQddx5cXC1mk2YeWGS8%2FHreu2cvij46egYuosCcWhxKciV0hBf8mjBO7i9RjEEvMJIofmtQ%2FZ%2Fda4vNtlug%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>32. good-cheap-fast</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813471" alt="32.png" title="32.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=pskIxWoEVaPLpyUOWhhd3g%3D%3D.vro%2Bg6TbqzxLU%2FF%2Fbr9QYA8TGbLF9AWvp0d%2Bcp10emGIZug1cOXWkWFUVE6xMPu%2Bqn8JClZnIP%2BrmYXGdqshEpYs5VNMUv%2F8l9LOItk2TAk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=fC5bgpH9Y4do9upGzljNqA%3D%3D.rYIkKZOymnDCfT0Q0kLRgG1sXf7cEoKR0C%2BEODM%2FGAu98lI2Y07nAgMPplLfywtCcVDNgJiW7DcR7iiks%2F%2FydA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>如何封装一个switch组件,即开关小组件。</li></ol><h2>33. notes-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813472" alt="33.png" title="33.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=hG2Y5rZbSLkRkxrWU82sAQ%3D%3D.9TbSYe%2BFsiQtAZwECVmm8o6g%2FWsB8dGqiJ4kQUdT7VfRzsBTetX3MouIQy6srE%2BCrgB1J2%2Fm6PJw7V2ZYZMnytuBRpUgEV6H8rnSItj2IaQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=k0T4DGsytAou%2FPTD%2BRxIbw%3D%3D.4KRsuqQ2%2F4iPY37VZ9mQmfIkM%2BSXkGV%2Bn22LzXncQodTxYUdpYJazk4St4FYmR9v3ompUIeHT2crpOb5K0MKZw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>34. animated-countdown</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813473" alt="34.png" title="34.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=dQw28uDKb0Smp2LsSTI94A%3D%3D.nMdrOOu%2BTI5zrUd4dqQlld9qSA%2BeMuNqTRlylKv3MLk4Hrz%2BrxFf5Cp3Xf%2BHfdR6UFaTFk1eYNKXu%2F7%2BMA5KoeG0w9FWykEZ%2BCqra2IY3l4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=kfxA3vBfeTv02dWtjfESpQ%3D%3D.3yNJa7xEbOgCa4XRrz0CKEIlunAH%2BZCWa2LKAmuALunn5rpJv6syDLqWmYl34Ib%2Brb8fBBAhM%2FQtMFttucjtTQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>35. image-carousel</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813474" alt="35.png" title="35.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=FSVtvdt2TOilol2nAibJnA%3D%3D.5aTlVK%2F%2Bh7Wlw8J97fTOkguAQRN5ODMya%2FeuMKP4kd%2BVu3IeCGmbas9NOxo0lKeQLv1MwVidxke93Dp%2BNw6JZKxLo7RYhAP4wIVetIz8KMY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=S2jBWbacMw9RyjO7A8s%2FaA%3D%3D.0hJfuGP31%2FUSc0scHthANhZ5B6Ob6n7DTZONz%2F6WHKkFxIIQibK9hA5rGnRo8d7kKGhLQ0EKh3YicIJspzkR8Q%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>36. hover board</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813475" alt="36.png" title="36.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=aFfdCrPaj5WzZ5eTtvBMwg%3D%3D.0hVN09e4mkU2G50gMzSJYcntJmXsa4E44zKies5Mm4suw5a40Z0VK0EINw60zDcZLXzUiZJYkrRFtC50Icwy%2Fz5y%2BLnGWyFlViU9PsVSceg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Y88tmwif72zs7HRQZHVFUQ%3D%3D.0%2BBArIPTPpSs9Idhb2rgya75uPOLxjT4wYbwTYUgicgezTnVBG33jmdUr9TelDMUBnnhjoLZ8IeLGlwCc6nS%2Bw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>react.js合成事件中的setState是同步的,所以这里使用原生的监听事件来实现。<a href="https://link.segmentfault.com/?enc=GVfJzOVhwotWdy4Z6OvjMw%3D%3D.wsLSNmuMUnL4wUzFsFm1FprAGYeDvpXqGMaKHmq%2FS3Sp9c62fgH5dRfg4xq%2Bo08%2FJaphCK9oUrLsGs57VvjmurOxG%2FGvCtJO8Sxp63%2BBK3U%3D" rel="nofollow"> 详见源码 </a>。</li></ol><h2>37. pokedex</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813476" alt="37.png" title="37.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=Sp2VuHO9t2a%2FM15djwK29Q%3D%3D.rQSgTV4Yc5R7fyK%2BN8GUPYiF%2BMfM9ze%2BmULuzzYxDbQKORGyrm%2BNrn5jkbw8QN7zTb4oM77EF05VJSwyhc%2FQooSLKMIXvVUCej4veaKiZPs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=uG%2F6pZHQIxRcZOgParYc3w%3D%3D.fSuHA4or8VIuiGTAyhwspP5p6ZTZP0MfZozInqvUrvq5L4Rf0u2AFO88kpN7O6HzSvqyM5ON%2BdKYc1Sfkxr8Cw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>38. mobile-tab-navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813477" alt="38.png" title="38.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=TtkLCdxv3dDn1ib5Q9J1aw%3D%3D.SjFC59iZ%2F6wz7lqBZqpQQN6rhLZf8%2BBWDaZ5njClhwXcX%2FNgMogJgBtQpr%2B0KYtPq%2BUBF7cQ4w9AH8Y9Jd3zVIiX6IrPECU3uSLCZ5t0d20%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=gMxI2Hhn5jv73ldZJ01Yyw%3D%3D.ZcNBHr%2BGEibESt1FX8YAn77UZ46f2lk3n1X3Gtg8LyrnqqzbVat08vgTUIcDzEtYAeM3BrRuc%2FryA0YOi3EODw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>39. password-strength-background</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813478" alt="39.png" title="39.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=f%2FICKURR7fv6MFn8La8gig%3D%3D.CX6nm13pI5ban5lUKnnit7QEPUxjM%2BERfEghX1rGOj%2B%2B12dlmV3rVGvRwIQycU7lobcaIc5AWsNTm5xjiXs4OBtZ7x9SASKJQ2lpoHk4lHc%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=J4ir%2FynydJqw8x%2FDgW2qGQ%3D%3D.zMNETUNejxZ93pKimqeTPjzh0OYAOZtvd%2BdEPBgA58P2uWfyPkP3o0V4oAa2Oe6oarluFyD0RuEjFcgffp9tkA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>照着<a href="https://link.segmentfault.com/?enc=1R%2FDdFlqIceKiHIKSZQ2qw%3D%3D.LgNV4dPmlCEl98pn3pruL2rWLuLIJru0BfzD3OAT3DHNmJIfacYw9KYU246trQMK6rZPb8ynUHnp%2FfukGf%2FXhg%3D%3D" rel="nofollow"> 文档 </a>一步步将<code>tailwindcss</code>添加到<code>create-react-app</code>脚手架中。</li></ol><h2>40. 3D-background-boxes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813479" alt="40.png" title="40.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=mLK3BfPwmkCatbFaZ%2FOGhw%3D%3D.JmcN63C1O70wfZybopfFumNi1PGJjlR581a2vXv9hjqZ%2BhGw%2B6HpsAu3QPEK5thyQdwO%2BQLhzfrcV2A4w5SMofpCkhbPFRy1yxD6u%2FFXhVs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=5lwTlTSElaniNFAfVtu5xA%3D%3D.0sdfiSwmlPPsbTqVyT4eDm7oHMMb0J7uFKYW%2Fv2a3MYlj6AmqnLCh%2FAGw6qJKF%2BWBzYekf6SjA4xRSvDubTqow%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>41. Verify Your Account</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813480" alt="41.png" title="41.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=ZobjVrcG6Mg2A6UC6JkX9g%3D%3D.v%2BnLoaI6BWh%2F3YiTPJpJ%2BOc5D0%2BFegg8K8XU6IEg%2FC%2FdwQChI3tRfNTLkVe4vRGOI5AGV%2FLdjNtNTm9HDUHpLumX9JshUXRc2pzYugfQxOg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=c85ldAu5oewRrGteHvvExA%3D%3D.YnlTmgHARBr09lb3UjumxWwtCJ6s4WKMjj06ljPozDnkYStXvUZ%2BpeUH1JBk0NRW8JcyHmZShafv5FKe3zR%2F4A%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>42. live-user-filter</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813481" alt="42.png" title="42.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=r8%2FAky3I7uJKZG7ET%2FHCww%3D%3D.Ey85%2BWAdEvhHY%2FcwSmR6R3cy3wcbuAA%2BfcaCEqvlZuzFqPAEcZCJ6LEA2gDIOEcs4W6EkdpbhOHUQOYa2N817h8O17zBNRlc8fLeGV56YOw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=0VnztySgtdgK5zaRJ5HQbg%3D%3D.rySJu4Ktht24%2BJcNe2Yy7Pjz6VmUNeivPAyeBtE8s%2FumDq71FMAg6ZKi3dXNfzYNoa2o4d%2FNzwjDsafhs6iAcQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>43. feedback-ui-design</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813482" alt="43.png" title="43.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=OcfyyK%2FQhNllVR2MHHRZUw%3D%3D.3y7OX0XsbA%2BwgyvtxpEre12LYOahILJY%2FzakUbwyP5ZRBPIJwcxzIzOdKGGOQJWC3t9RNkrgSV29cqRryDlHXWU5qTLwhklG4Aegy40ZeJA%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=aCS7DL8DWWH43%2F7gs1R5UQ%3D%3D.Fj7irIKSI8td7OumpkU9AWtmsrafFqnM3vRJLuQ2pxTZ5dqxfhQxfP60rx9Lzm7ioYqhiGzQlSIDrPHUotOGXA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>44. custom-range-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813483" alt="44.png" title="44.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2FLrM4H0DJxlepBbnILGcXQ%3D%3D.VnQXSfiijieCdcVCFrcmsUa2NnykQpcowxDb1QDyOEMRKSR%2B2BUa1B5d3KT3wkoHAtGqoPpO%2F%2B5U3wSODfBy%2BRUNQGbdO%2FYPGZgg5nTn4UQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=CBKKBhAgvVxLEFosDtPceA%3D%3D.2exYI%2BTUkgDOKrw1Trh6QZQwkhTSN3%2F268ZCDSnJmFcB2XRMALwc6w9yzbVXZ77lFZ8DwBETN%2FKFwg6wzl2Glg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>45. netflix-mobile-navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813484" alt="45.png" title="45.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=9i0JqV6a9VFlKrE7tHqXpw%3D%3D.qp4WucRQaNPHqz7%2FJkTGDkBSvBh2ScDjPA0Zzpk71pX4V45H9F8pYgpLPdYouvZUXniSBBQ0Az1IXtkUkaEz1UoLE57rj4%2FSmQhmTUBTPU0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=YO%2BXtMv%2FLf8EYktks15YPQ%3D%3D.1pOK8%2FYDUMMczGRTuKBel3Zk5bh6j35Ibrf4KpHgf3US5eG0wRIlncQkguWdAUgrrcLq16zVuzXvN517HagJmg%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>为如何实现一个递归组件提供了思路。</li></ol><h2>46. quiz-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813485" alt="46.png" title="46.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2BdINBnpRc5NdAhoKIyqWJg%3D%3D.oUhc91TZ2uGOYq9SXhVko479qUfvMX2V1DI30U%2BRgRhkoiWawQXCxEgQwafiVOrK2jApb2cAElr405uGjV2323pv6QRKnJP%2BgokHCYwanGU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=Gp9RI4CKeoivfOJsGf8zPg%3D%3D.Fqk5ith53CZby9zTFzg0uQebuChGHeECXu1PIaGsotskvJ3zgLWQYxeVEbYwJlSwcjtAlnB0uNZV0wzb1WV4LQ%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>47. testimonial-box-switcher</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813486" alt="47.png" title="47.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=BhYvcDhMG1EGNIV72tC2gw%3D%3D.rZO2jXugzSC6I5rV%2FirXacw6QX4PQkmsY68nOHAQ8IdsWKYI9PoEAtwYymUgbnstHTwJivo6K4dopYV9jK8hKqH4JrQvzhC0kv%2BN61xcc3g%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=046FgR2ONAkZPXUizcTl6w%3D%3D.v8j7fQBJwCNTwhSyQqBBzIrnxULANz1DlGxMGAyO0ZNPIkt9Q8j1ZWePSYud0g8wHP1Ldvsg8B7jkPND%2FiZt6A%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>48. random-image-feed</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813487" alt="48.png" title="48.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=gI3AC%2FbwGy6B%2F1eIJ%2Bu%2FLA%3D%3D.RRWGwYZvYpHkjKutAaAQbrUh%2F43z08hVocF3RH2I%2FymlL6xkNnkC7uPn2qOBM3NEPBC%2FH2FcJuKbpdeb6oVsKJ2GzuWrLrb88MEC1Ga4nlw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=APvvBG1i7gC4MP86g2gYYQ%3D%3D.HJU5jlGv79tiYcMbJOiAUvdJe0NjWOgfFJMVeA1fQMVqE7I4j5C24q5JTaTuD%2BBWNlySr8%2FVauNbTTmgdO0j%2Bw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><ol><li>实现一个简易版本的预览图片。</li></ol><h2>49. todo-list</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813488" alt="49.png" title="49.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2BHf%2FOYWvrjcmNuLLRx7azQ%3D%3D.MVMw4gCuqNh6sa%2FSnnbpi4mCEr34WLumq0wSDZSfgx4YEiGfWDEx5bg6L5YWeoAfKnYybpKFsWqRxZIsX34o%2Bp3%2F1DpkcL%2F%2BMSdm31qKWE0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=5FfuyxEdxp%2BP7O%2Fdb3ZRzg%3D%3D.727bzRPKSJ4r4ew6e%2BHDocyPN3urIcF5WpMtLa28JzEEe8FFRt%2FSKqQG5sglEGRBFd1d7PCsmNkKC3VjB7QJdw%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><h2>50. insect-catch-game</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040813489" alt="50.png" title="50.png"></p><ul><li><a href="https://link.segmentfault.com/?enc=k3toxB2QmdJVLBlQIAowjQ%3D%3D.a36e%2FZuNR%2BV%2BkdHfMIipjdQJ5FMb6FvbOmfbEaX8i2XlgYDxpmeWXUEHHS4K5mwkrE8yKAkeznflLeXIvhStNSNpbWLUkuPt%2B0JVZrzG1xI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=WEwAWtuOCLSvHYbQXTHvtw%3D%3D.bGRHRsgG5M48zu0HEUEG%2BwrrPX7vaWvMHFyrzXGD8nYlEk2AGfmwD2fM1p8siN872WvebAx6Uchcj5v3xA6rqA%3D%3D" rel="nofollow">在线示例</a></li></ul><blockquote>学到了什么?</blockquote><p>这个示例涉及到的知识点,前面的示例都提及过,所以这里不必赘述。</p><blockquote>特别说明:这个示例算是一个比较综合的示例了。</blockquote>
从零开始实现一个颜色选择器(原生JavaScript实现)
https://segmentfault.com/a/1190000040789940
2021-10-10T20:13:28+08:00
2021-10-10T20:13:28+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
10
<h2>准备工作</h2><h3>项目目录与文件创建</h3><p>首先,我们无需搭建项目的环境,我们还是直接用最简单的方式,也就是引入的方式来创建这个项目,这样也就方便了我们一边编写一边测试。创建一个空目录,命名为<code>ColorPicker</code>,创建一个<code>js</code>文件,即<code>color-picker.js</code>,然后创建一个<code>index.html</code>文件以及创建一个样式文件<code>color-picker.css</code>。现在你应该可以看到你的项目目录是如下所示:</p><pre><code>ColorPicker
│ index.html
│ color-picker.js
│ color-picker.css</code></pre><p>在你的<code>index.html</code>中,初始化<code>html</code>文档结构,然后引入这个<code>color-picker.js</code>文件,如下所示:</p><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>color-picker</title>
<link rel="stylesheet" href="./color-picker.css" />
</head>
<body></body>
<script src="./color-picker.js"></script>
</html></code></pre><p>做好这些准备工作之后,让我们继续下一步。</p><h2>结构与布局</h2><h3>模块分析</h3><p>我们通过如下一张图来分析我们要实现的模块,如下图所示:</p><p><img src="/img/bVcVjuh" alt="" title=""></p><p>正如上图所示,我们可以将一个颜色选择器拆分成多个模块,所以我们大致得到了一个结构如下:</p><ul><li>颜色色块</li><li>颜色面板</li><li>色调柱</li><li>透明度柱</li><li>输入框</li><li>清空与确定按钮</li><li>预定义颜色元素列表</li></ul><p>这样一来,我们可以清晰的看到整个颜色选择器都有哪些模块。我们目前只需要考虑开发出基本的模块功能,然后后续就在基础上开始进行扩展和完善。好的,让我们继续下一步,搭建页面的基本结构。</p><h3>色块模块</h3><p>通过分析,我们应该知道,色块分成两种情况,第一种就是有颜色值时,色块应该是一个背景色为该颜色值的左右箭头。就像如下图所示:</p><p><img src="/img/bVcVjuj" alt="" title=""></p><p>而无颜色值,我们的色块应该是如下图所示:</p><p><img src="/img/bVcVjul" alt="" title=""></p><p>如此一来,我们就确定了色块的结构元素,如下:</p><pre><code class="html"><div class="ew-color-picker-box">
<!-- 有颜色值,这里我们并没有使用任何图标,用css来实现一个看起来就像下拉箭头一样 -->
<div class="ew-color-picker-arrow">
<div class="ew-color-picker-arrow-left">
<div class="ew-color-picker-arrow-right"></div>
<!-- 无颜色值 -->
<div class="ew-color-picker-no">&times;</div>
</div>
</div>
</div></code></pre><p>这里我们肯定是通过一个颜色值来确定使用哪一个结构的,这个后续我们再说。我们现在就先确定色块的元素结构应该是如下这样呢。当然这里的类名也可以是自己随便自定义。</p><blockquote>tips:我这里是为了有自己的特色,所以加了<code>ew-</code>前缀名。如果你自己使用自己自定义的类名,那么你后续编写样式和操作 DOM 元素的时候需要注意,要去更改。</blockquote><p>还有注意<code>&times;</code>它是<code>HTML字符实体</code>,我们只需要知道它最终会显示为<code>X</code>就行了,这里不会去细讲,欲了解更多 HTML 字符实体知识,可以前往<a href="https://link.segmentfault.com/?enc=JhLeS7vgVMj1UU%2BW9oLVMg%3D%3D.ccXlm%2BnHg8RxYCbtDgZyQoPnChiTWUXj8xh2hdswMgReLgKebafkUVuNFY98IWro1nPyyhruYbzGXEwjqqJ2BA%3D%3D" rel="nofollow">HTML 字符实体</a><br>查看。</p><p>接下来,让我们完成色块的样式编写。我们先完成最外层的盒子元素。可以看到,最外层的它会有一个自定义的宽高,然后就是一个边框,其它的就没有什么了,这样一来,我们就知道了该编写什么样的<code>CSS</code>代码。这里我们还是采用本身写好的样式。我们做个记录:</p><ul><li>色块盒子的边框颜色为<code>#dcdee2</code></li><li>色块盒子的字体颜色为<code>#535353</code></li><li>色块盒子有<code>4px</code>的圆角</li><li>色块盒子有上下<code>4px</code>的内间距,<code>7px</code>的左右内间距</li><li>色块盒子有<code>14px</code>的字体大小</li><li>色块盒子有<code>1.5</code>的行高,注意没有单位</li></ul><blockquote>tips:1.5 倍行高是一个相对值,它是根据浏览器设置的字体大小来决定的,例如浏览器字体大小为 16px,那么 1.5 倍行高就是 16px * 1.5 = 24px 的行高</blockquote><p>看到以上几点要求,我们应该知道,我们要采用哪个<code>CSS</code>属性来实现,脑海中要有一个清晰的认识。</p><pre><code class="css">.ew-color-picker-box {
/* 边框颜色为#dcdee2 */
border: 1px solid #dcdee2;
/* 边框有4px的圆角 */
border-radius: 4px;
/* 4px的上下内间距,7px的左右内间距 */
padding: 4px 7px;
}</code></pre><p>最外层的盒子元素的样式,我们已经编写完成了,接下来,我们开始编写没有颜色值的时候的一个样式。实际上它和最外层的色块盒子样式差不多,唯一需要注意的就是,我们后续将通过<code>js</code>来设置它的宽高以及行高了。因为它是动态改变的,不过这里我们可以先固定一个值,然后后续再做更改。</p><pre><code class="css">.ew-color-picker-box > .ew-color-box-no {
width: 40px;
height: 40px;
font-size: 20px;
line-height: 40px;
color: #5e535f;
border: 1px solid #e2dfe2;
border-radius: 2px;
}</code></pre><p>接下来就是实现有颜色值的样式了,这个要有一点难度,难点在于我们如何去实现一个类似下拉框箭头一样的下箭头。我们通过分析页面结构元素,不难看出,实际上我们这里的下箭头很明显是通过两个元素来拼凑成的,也就是说一个元素只是一根旋转了 45deg 的横线,同样的道理,另一个元素无非是旋转的方向相反罢了。并且我们可以看到这两根横线是垂直水平居中的,这里,我们肯定很快就想到了弹性盒子布局,只需要两个属性就可以让元素垂直水平居中。即<code>justify-content:center</code>与<code>align-items:center</code>这两个属性。所以,经过这样一分析,我们这里的实现就不难了。</p><blockquote>2D 坐标系</blockquote><p><img src="/img/bVcVjun" alt="" title=""></p><blockquote>3D 坐标系</blockquote><p><img src="/img/bVcVjup" alt="" title=""></p><p>如下所示:</p><pre><code class="css">.ew-color-picker-box-arrow {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
margin: auto;
z-index: 3;
}
.ew-color-picker-box-arrow-left {
width: 12px;
height: 1px;
display: inline-block;
background-color: #fff;
position: relative;
transform: rotate(45deg);
}
.ew-color-picker-box-arrow-right {
width: 12px;
height: 1px;
display: inline-block;
background-color: #fff;
position: relative;
transform: rotate(-45deg);
right: 3px;
}</code></pre><p>如此一来,色块模块的页面结构和样式就这样被我们完成了,让我们继续。</p><h3>颜色面板</h3><p>颜色面板也是整个颜色选择器中最难的部分,现在我们来分析一下结构。首先,我们可以看到,它有一个容器元素,这个容器元素有点阴影效果,背景色是白色。这里需要知道的一个知识点就是盒子模型,也就是<code>box-sizing</code>属性,它有 2 个属性值:<code>content-box,border-box</code>。事实上在实际开发中,我们用到最多的是<code>border-box</code>。我们来看文档<a href="https://link.segmentfault.com/?enc=twsb8AjsgDkasOBj%2FJlUPw%3D%3D.xSvZSOMLd6J5GhGkrTMjSXu%2BQZ5qu6FKbVk5gk%2BM1jAZJhNj%2B1%2BlfqYVqemU4Y6TaDCoBIXgbKMHH%2Bj1zuo3FQ%3D%3D" rel="nofollow">box-sizing</a>。</p><p>通过文档描述,我们知道了这个属性的意思。那么这里这个颜色面板容器元素的盒子模型我们就需要注意了,在这里,它是<code>标准盒子模型</code>,也就是我们只是单独包含内容的宽高就行了。因此,我们总结如下:</p><ul><li>1px 的实线边框#ebeeff</li><li>盒子模型为标准盒子模型</li><li>阴影效果<a href="https://link.segmentfault.com/?enc=cfqJViDDcmE0YaqQjZSI%2Bw%3D%3D.e%2BT0OKv5H%2BprWn97rH8Af8qdmFFK0Hd0Qdd29DM37MunYOBDj5s3uhAkD2n%2FNj8pBjUwxeGZg5T%2FkIpPC0FNDQ%3D%3D" rel="nofollow">文档</a></li><li>7px 的内边距</li><li>5px 的圆角</li></ul><blockquote>tips:这里留一个悬念,为什么要使用标准盒子模型。</blockquote><p>到此为止,我们的容器元素就分析完成了,接下来开始编写结构与样式。</p><pre><code class="html"><div class="ew-color-picker">
<!-- 当然里面的结构后续再分析 -->
</div></code></pre><pre><code class="css">.ew-color-picker {
min-width: 320px;
box-sizing: content-box;
border: 1px solid #ebeeff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
border-radius: 5px;
z-index: 10;
padding: 7px;
text-align: left;
}</code></pre><p>现在我们再来确定容器元素中都有哪些元素,首先是一个颜色面板,颜色面板又包含一个容器元素,我们可以看到,颜色面板很像是三种背景色叠加出来的效果,不用怀疑,大胆的说,是的没错,就是三种背景色叠加出来的,所以我们就需要一个容器元素,然后容器元素里面又包含 2 个面板元素,容器元素的背景色加上 2 个面板元素叠加出来就是这种效果。一个白色的背景加一个黑色的就能叠加看到我们想要的效果。<br>比如我们先来看看一个示例:</p><pre><code class="html"><div class="panel">
<div class="white-panel"></div>
<div class="black-panel"></div>
</div></code></pre><pre><code class="css">.panel {
width: 280px;
height: 180px;
position: relative;
border: 1px solid #fff;
background-color: rgb(255, 166, 0);
}
.panel > div.white-panel,
.panel > div.black-panel {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.white-panel {
background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.black-panel {
background: linear-gradient(0deg, #000, transparent);
}</code></pre><p>这里可能又涉及到一个知识点,那就是渐变颜色,这里就不做细讲,感兴趣的可查看<a href="https://link.segmentfault.com/?enc=FxhRa9z9klioOqE2e%2FxWlQ%3D%3D.u4FumMAS1I8JhzekdsxcpgJFGS9gr4J9AsXtHSknT5M7ZnwmBshMcdgIdQ4CL2n12CFwZp5C%2BGuBB7t%2BCGEEDELOjLf8lMeU%2BtuQiq2p63Q%3D" rel="nofollow">文档</a>。</p><p>所以我们的结构应该是如下:</p><pre><code class="html"><div class="ew-color-picker-content">
<div class="ew-color-picker-panel">
<div class="ew-color-picker-white-panel"></div>
<div class="ew-color-picker-black-panel"></div>
</div>
</div></code></pre><p>根据前面那个示例,我们很快就能写出这个颜色面板了,不过我们还少了一个,也就是在颜色面板区域之内的拖动元素,或者我们可以称之为游标元素。</p><pre><code class="css">.ew-color-picker-panel {
width: 280px;
height: 180px;
position: relative;
border: 1px solid #fff;
background-color: rgb(255, 166, 0);
cursor: pointer;
}
.ew-color-picker-panel > div.ew-color-picker-white-panel,
.ew-color-picker-panel > div.ew-color-picker-black-panel {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.ew-color-picker-white-panel {
background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.ew-color-picker-black-panel {
background: linear-gradient(0deg, #000, transparent);
}</code></pre><p>好了,现在我可以回答之前那个留下的问题了,为什么要使用标准盒子模型而不是 IE 标准盒子模型。这是因为这里我们会通过 js 动态去计算游标元素拖动的距离,如果是 IE 标准盒子模型,则会考虑边框的大小以及间距的大小,这无疑给我们计算拖动距离增加了难度,所以为了简便化,我们使用的是标准盒子模型。</p><p>现在我们再来加上这个游标元素吧,因为它是在颜色面板内动态改变的,通常我们要让一个元素在父元素当中进行移动,那么我们很明显就想到了子元素使用绝对定位,父元素加一个除了静态定位<code>static</code>以外的定位,通常我们用相对定位,这里也不例外。这也就是我们给<code>.ew-color-picker-panel</code>添加一个相对定位<code>position: relative;</code>的原因。</p><pre><code class="html"><div class="ew-color-picker-content">
<div class="ew-color-picker-panel">
<!-- 省略了一些内容,游标元素添加 -->
<div class="ew-color-picker-panel-cursor"></div>
</div>
</div></code></pre><p>这里需要注意了,游标元素设置的宽高会影响我们后续计算,所以在这里设置的宽高是多少,后续计算就要将它的宽高考虑在内,这个到后面会细讲,现在,我们还是编写该元素的样式吧。</p><pre><code class="css">.ew-color-picker-panel-cursor {
width: 4px;
height: 4px;
border-radius: 50%;
position: absolute;
left: 100%;
top: 0;
transform: translate(-4px, -4px);
box-shadow: 0 0 0 3px #fff, inset 0 0 2px 2px rgb(0 0 0 / 40%),
/*等价于rgba(0,0,0,0.4)*/ 0 0 2px 3px rgb(0 0 0 / 50%); /*等价于rgba(0,0,0,0.5)*/
cursor: default;
}</code></pre><p>游标元素,我们看起来就像是一个小圆圈,所以我们给的宽高不是很多,只有 4px,既然是圆,我们都知道可以使用<code>border-radius</code>为<code>50%</code>即可以将一个元素变成圆。接下来就是阴影部分,这样就实现了我们的小圆圈。当然我们不一定非要实现这样的效果,但是为了还原颜色选择器本身,也方便后续的计算,所以我们还是采用原本的样式。</p><h3>色阶柱</h3><p>接下来,我们来看一下色阶柱也就是色调柱的实现。看到这个图,我们应该可以很清晰的分出色阶柱包含了 2 个部分,第一个部分就是柱形部分,称之为 bar,第二个部分就是拖动滑块部分,称之为 thumb。然后我们外加一个容器元素用于包含色阶柱和透明柱,所以我们可以确定色阶柱的结构如下:</p><pre><code class="html"><!-- 容器元素 -->
<div class="ew-color-slider ew-is-vertical">
<div class="ew-color-slider-bar">
<div class="ew-color-slider-thumb"></div>
</div>
</div></code></pre><p>然后我们来确定样式的实现,首先整个色阶柱是垂直布局的,所以我们应该知道它就是有一个固定宽度,然后高度等价于颜色面板的矩形,它的背景色通过一种渐变色来实现,实际上就是红橙黄绿青蓝紫七种颜色的混合,也就类似彩虹。这每一种颜色都有不同的比例。其次我们还要知道滑块部分是需要动态拖动的。在这里我们可以想象得到色阶柱可以是水平或者垂直布局的,目前我们先实现垂直布局(为了区分给容器元素加一个类名 ew-is-vertical)。所以滑块的动态改变部分应该是 top 值。现在我们来看样式:</p><pre><code class="css">.ew-color-slider,
.ew-color-slider-bar {
position: relative;
}
.ew-color-slider.ew-is-vertical {
width: 28px;
height: 100%;
cursor: pointer;
float: right;
}
.ew-color-slider.ew-is-vertical .ew-color-slider-bar {
width: 12px;
height: 100%;
float: left;
margin-left: 3px;
background: linear-gradient(
180deg,
#f00 0,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00
);
}
.ew-color-slider-thumb {
background-color: #fff;
border-radius: 4px;
position: absolute;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
border: 1px solid #dcdee2;
left: 0;
top: 0;
box-sizing: border-box;
position: absolute;
}</code></pre><p>到目前为止,我们色阶柱就算是实现了,接下来来看透明度柱的实现。</p><h3>透明度柱</h3><p>透明度柱的实现原理跟色阶柱很相似,首先我们可以看到透明度柱会有一个透明的背景,这个背景很显然是一个图片,其次它还会有一个背景色条,取决于当且色阶柱处于哪种色调,然后同样还是与色阶柱一样有一个滑块,同样也是有垂直布局和水平布局,改变 top 值。所以我们得到结构如下所示:</p><pre><code class="html"><div class="ew-alpha-slider-bar">
<!-- 背景图 -->
<div class="ew-alpha-slider-wrapper"></div>
<!-- 背景色 -->
<div class="ew-alpha-slider-bg"></div>
<!-- 滑块元素 -->
<div class="ew-alpha-slider-thumb"></div>
</div></code></pre><p>在这里,我们需要注意的一点就是背景色条的背景色是动态改变,这将在后面会讲到。背景色条,我们同样是通过线性渐变来实现的。让我们来看看样式吧:</p><pre><code class="css">.ew-alpha-slider-bar {
width: 12px;
height: 100%;
float: left;
position: relative;
}
.ew-alpha-slider-wrapper,
.ew-alpha-slider-bg {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
}
.ew-alpha-slider-bar.ew-is-vertical .ew-alpha-slider-bg {
/* 这里先暂时写死 */
background: linear-gradient(
to top,
rgba(255, 0, 0, 0) 0%,
rgba(255, 0, 0) 100%
);
}
.ew-alpha-slider-wrapper {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==");
}
.ew-alpha-slider-thumb {
background-color: #fff;
border-radius: 4px;
position: absolute;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
border: 1px solid #dcdee2;
left: 0;
top: 0;
box-sizing: border-box;
position: absolute;
}</code></pre><p>好了,到目前为止,我们的透明度柱也就实现了,接下来我们来看输入框的实现。</p><h3>输入框与按钮</h3><p>输入框比较简单,我想没什么好说的,这个输入框也可以自定义,它的结构无非就是如下:</p><pre><code class="html"><input class="ew-color-input" /></code></pre><p>它和清空与确定按钮元素排在一行,因此我们用一个容器元素来包裹它们,结构应该如下:</p><pre><code class="html"><div class="ew-color-drop-container">
<input class="ew-color-input" />
<div class="ew-color-drop-btn-group">
<button type="button" class="ew-color-drop-btn ew-color-clear">清空</button>
<button type="button" class="ew-color-drop-btn ew-color-sure">确定</button>
</div>
</div></code></pre><p>然后样式也没有什么好分析的,都是一些基础样式,我们继续编写代码。如下:</p><pre><code class="css">.ew-color-drop-container {
margin-top: 6px;
padding-top: 4px;
min-height: 28px;
border-top: 1px solid #cdcdcd;
position: relative;
}
.ew-color-input {
display: inline-block;
padding: 8px 12px;
border: 1px solid #e9ebee;
border-radius: 4px;
outline: none;
width: 160px;
height: 28px;
line-height: 28px;
border: 1px solid #dcdfe6;
padding: 0 5px;
-webkit-transition: border-color 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition: border-color 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border-radius: 5px;
background-color: #fff;
}
.ew-color-drop-btn-group {
position: absolute;
right: 0;
top: 5px;
}
.ew-color-drop-btn {
padding: 5px;
font-size: 12px;
border-radius: 3px;
-webkit-transition: 0.1s;
transition: 0.1s;
font-weight: 500;
margin: 0;
white-space: nowrap;
color: #606266;
border: 1px solid #dcdfe6;
letter-spacing: 1px;
text-align: center;
cursor: pointer;
}
.ew-color-clear {
color: #4096ef;
border-color: transparent;
background-color: transparent;
padding-left: 0;
padding-right: 0;
}
.ew-color-clear:hover {
color: #66b1ff;
}
.ew-color-sure {
margin-left: 10px;
}
.ew-color-sure {
border-color: #4096ef;
color: #4096ef;
}</code></pre><p>输入框和按钮我们就已经完成了,接下来我们再来看预定义颜色元素呢。</p><h3>预定义颜色</h3><p>预定义颜色元素实现起来也比较简单,就是一个容器元素,然后包含多个子元素,可能稍微难一点的就是子元素的样式我们分为四种情况,第一种就是默认的样式,第二种就是禁止点击的样式,除此之外,我们还加了一个颜色透明度之间的区别,然后最后就是选中样式。不多说,我们可以先写 4 个子元素来分别代表四种情况的样式。如下:</p><pre><code class="html"><div class="ew-pre-define-color-container">
<div class="ew-pre-define-color" tabindex="0"></div>
<div class="ew-pre-define-color ew-has-alpha" tabindex="1"></div>
<div
class="ew-pre-define-color ew-pre-define-color-disabled"
tabindex="2"
></div>
<div
class="ew-pre-define-color ew-pre-define-color-active"
tabindex="3"
></div>
</div></code></pre><p>接下来,我们来看样式的实现:</p><pre><code class="css">.ew-pre-define-color-container {
width: 280px;
font-size: 12px;
margin-top: 8px;
}
.ew-pre-define-color-container::after {
content: "";
display: table;
height: 0;
visibility: hidden;
clear: both;
}
.ew-pre-define-color-container .ew-pre-define-color {
margin: 0 0 8px 8px;
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid #9b979b;
cursor: pointer;
float: left;
}
.ew-pre-define-color-container .ew-pre-define-color:hover {
opacity: 0.8;
}
.ew-pre-define-color-active {
box-shadow: 0 0 3px 2px #409eff;
}
.ew-pre-define-color:nth-child(10n + 1) {
margin-left: 0;
}
.ew-pre-define-color.ew-has-alpha {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==");
}
.ew-pre-define-color.ew-pre-define-color-disabled {
cursor: not-allowed;
}</code></pre><p>样式和布局就到此结束了,接下来才是我们的重点,也就是实现颜色选择器的功能。</p><h2>JavaScript</h2><h3>工具方法</h3><p>首先用一个空对象来管理工具方法。如下:</p><pre><code class="js">const util = Object.create(null);</code></pre><p>然后有如下方法:</p><pre><code class="js">const util = Object.create(null);
const _toString = Object.prototype.toString;
let addMethod = (instance, method, func) => {
instance.prototype[method] = func;
return instance;
};
["Number", "String", "Function", "Undefined", "Boolean"].forEach(
(type) => (util["is" + type] = (value) => typeof value === type.toLowerCase())
);
util.addMethod = addMethod;
["Object", "Array", "RegExp"].forEach(
(type) =>
(util["isDeep" + type] = (value) =>
_toString.call(value).slice(8, -1).toLowerCase() === type.toLowerCase())
);
util.isShallowObject = (value) =>
typeof value === "object" && !util.isNull(value);
util["ewObjToArray"] = (value) =>
util.isShallowObject(value) ? Array.prototype.slice.call(value) : value;
util.isNull = (value) => value === null;
util.ewAssign = function (target) {
if (util.isNull(target)) return;
const _ = Object(target);
for (let j = 1, len = arguments.length; j < len; j += 1) {
const source = arguments[j];
if (source) {
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
_[key] = source[key];
}
}
}
}
return _;
};
util.addClass = (el, className) => el.classList.add(className);
util.removeClass = (el, className) => el.classList.remove(className);
util.hasClass = (el, className) => {
let _hasClass = (value) =>
new RegExp(" " + el.className + " ").test(" " + value + " ");
if (util.isDeepArray(className)) {
return className.some((name) => _hasClass(name));
} else {
return _hasClass(className);
}
};
util["setCss"] = (el, prop, value) => el.style.setProperty(prop, value);
util.setSomeCss = (el, propValue = []) => {
if (propValue.length) {
propValue.forEach((p) => util.setCss(el, p.prop, p.value));
}
};
util.isDom = (el) =>
util.isShallowObject(HTMLElement)
? el instanceof HTMLElement
: (el &&
util.isShallowObject(el) &&
el.nodeType === 1 &&
util.isString(el.nodeName)) ||
el instanceof HTMLCollection ||
el instanceof NodeList;
util.ewError = (value) =>
console.error("[ewColorPicker warn]\n" + new Error(value));
util.ewWarn = (value) => console.warn("[ewColorPicker warn]\n" + value);
util.deepCloneObjByJSON = (obj) => JSON.parse(JSON.stringify(obj));
util.deepCloneObjByRecursion = function f(obj) {
if (!util.isShallowObject(obj)) return;
let cloneObj = util.isDeepArray(obj) ? [] : {};
for (let k in obj) {
cloneObj[k] = util.isShallowObject(obj[k]) ? f(obj[k]) : obj[k];
}
return cloneObj;
};
util.getCss = (el, prop) => window.getComputedStyle(el, null)[prop];
util.$ = (ident) => {
if (!ident) return null;
return document[
ident.indexOf("#") > -1 ? "querySelector" : "querySelectorAll"
](ident);
};
util["on"] = (element, type, handler, useCapture = false) => {
if (element && type && handler) {
element.addEventListener(type, handler, useCapture);
}
};
util["off"] = (element, type, handler, useCapture = false) => {
if (element && type && handler) {
element.removeEventListener(type, handler, useCapture);
}
};
util["getRect"] = (el) => el.getBoundingClientRect();
util["baseClickOutSide"] = (element, isUnbind = true, callback) => {
const mouseHandler = (event) => {
const rect = util.getRect(element);
const target = event.target;
if (!target) return;
const targetRect = util.getRect(target);
if (
targetRect.x >= rect.x &&
targetRect.y >= rect.y &&
targetRect.width <= rect.width &&
targetRect.height <= rect.height
)
return;
if (util.isFunction(callback)) callback();
if (isUnbind) {
// 延迟解除绑定
setTimeout(() => {
util.off(document, util.eventType[0], mouseHandler);
}, 0);
}
};
util.on(document, util.eventType[0], mouseHandler);
};
util["clickOutSide"] = (context, config, callback) => {
const mouseHandler = (event) => {
const rect = util.getRect(context.$Dom.picker);
let boxRect = null;
if (config.hasBox) {
boxRect = util.getRect(context.$Dom.box);
}
const target = event.target;
if (!target) return;
const targetRect = util.getRect(target);
// 利用rect来判断用户点击的地方是否在颜色选择器面板区域之内
if (config.hasBox) {
if (
targetRect.x >= rect.x &&
targetRect.y >= rect.y &&
targetRect.width <= rect.width
)
return;
// 如果点击的是盒子元素
if (
targetRect.x >= boxRect.x &&
targetRect.y >= boxRect.y &&
targetRect.width <= boxRect.width &&
targetRect.height <= boxRect.height
)
return;
callback();
} else {
if (
targetRect.x >= rect.x &&
targetRect.y >= rect.y &&
targetRect.width <= rect.width &&
targetRect.height <= rect.height
)
return;
callback();
}
setTimeout(() => {
util.off(document, util.eventType[0], mouseHandler);
}, 0);
};
util.on(document, util.eventType[0], mouseHandler);
};
util["createUUID"] = () =>
(Math.random() * 10000000).toString(16).substr(0, 4) +
"-" +
new Date().getTime() +
"-" +
Math.random().toString().substr(2, 5);
util.removeAllSpace = (value) => value.replace(/\s+/g, "");
util.isJQDom = (dom) =>
typeof window.jQuery !== "undefined" && dom instanceof jQuery;
//the event
util.eventType = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)
? ["touchstart", "touchmove", "touchend"]
: ["mousedown", "mousemove", "mouseup"];</code></pre><h3>动画函数的封装</h3><pre><code class="js">const animation = {};
function TimerManager() {
this.timers = [];
this.args = [];
this.isTimerRun = false;
}
TimerManager.makeTimerManage = function (element) {
const elementTimerManage = element.TimerManage;
if (!elementTimerManage || elementTimerManage.constructor !== TimerManager) {
element.TimerManage = new TimerManager();
}
};
const methods = [
{
method: "add",
func: function (timer, args) {
this.timers.push(timer);
this.args.push(args);
this.timerRun();
},
},
{
method: "timerRun",
func: function () {
if (!this.isTimerRun) {
let timer = this.timers.shift(),
args = this.args.shift();
if (timer && args) {
this.isTimerRun = true;
timer(args[0], args[1]);
}
}
},
},
{
method: "next",
func: function () {
this.isTimerRun = false;
this.timerRun();
},
},
];
methods.forEach((method) =>
util.addMethod(TimerManager, method.method, method.func)
);
function runNext(element) {
const elementTimerManage = element.TimerManage;
if (elementTimerManage && elementTimerManage.constructor === TimerManager) {
elementTimerManage.next();
}
}
function registerMethods(type, element, time) {
let transition = "";
if (type.indexOf("slide") > -1) {
transition = "height" + time + " ms";
util.setCss(element, "overflow", "hidden");
upAndDown();
} else {
transition = "opacity" + time + " ms";
inAndOut();
}
util.setCss(element, "transition", transition);
function upAndDown() {
const isDown = type.toLowerCase().indexOf("down") > -1;
if (isDown) util.setCss(element, "display", "block");
const getPropValue = function (item, prop) {
let v = util.getCss(item, prop);
return util.removeAllSpace(v).length ? parseInt(v) : Number(v);
};
const elementChildHeight = [].reduce.call(
element.children,
(res, item) => {
res +=
item.offsetHeight +
getPropValue(item, "margin-top") +
getPropValue(item, "margin-bottom");
return res;
},
0
);
let totalHeight = Math.max(element.offsetHeight, elementChildHeight + 10);
let currentHeight = isDown ? 0 : totalHeight;
let unit = totalHeight / (time / 10);
if (isDown) util.setCss(element, "height", "0px");
let timer = setInterval(() => {
currentHeight = isDown ? currentHeight + unit : currentHeight - unit;
util.setCss(element, "height", currentHeight + "px");
if (currentHeight >= totalHeight || currentHeight <= 0) {
clearInterval(timer);
util.setCss(element, "height", totalHeight + "px");
runNext(element);
}
if (!isDown && currentHeight <= 0) {
util.setCss(element, "display", "none");
util.setCss(element, "height", "0");
}
}, 10);
}
function inAndOut() {
const isIn = type.toLowerCase().indexOf("in") > -1;
let timer = null;
let unit = (1 * 100) / (time / 10);
let curAlpha = isIn ? 0 : 100;
util.setSomeCss(element, [
{
prop: "display",
value: isIn ? "none" : "block",
},
{
prop: "opacity",
value: isIn ? 0 : 1,
},
]);
let handleFade = function () {
curAlpha = isIn ? curAlpha + unit : curAlpha - unit;
if (element.style.display === "none" && isIn)
util.setCss(element, "display", "block");
util.setCss(element, "opacity", (curAlpha / 100).toFixed(2));
if (curAlpha >= 100 || curAlpha <= 0) {
if (timer) clearTimeout(timer);
runNext(element);
if (curAlpha <= 0) util.setCss(element, "display", "none");
util.setCss(element, "opacity", curAlpha >= 100 ? 1 : 0);
} else {
timer = setTimeout(handleFade, 10);
}
};
handleFade();
}
}
["slideUp", "slideDown", "fadeIn", "fadeOut"].forEach((method) => {
animation[method] = function (element) {
TimerManager.makeTimerManage(element);
element.TimerManage.add(function (element, time) {
return registerMethods(method, element, time);
}, arguments);
};
});</code></pre><h3>一些颜色操作的算法</h3><pre><code class="js">const colorRegExp = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// RGB color
const colorRegRGB =
/[rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0\.\d{1,2}|1|0)?[\)]{1}/g;
// RGBA color
const colorRegRGBA =
/^[rR][gG][Bb][Aa][\(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){3}[\\s]*(1|1.0|0|0?.[0-9]{1,2})[\\s]*[\)]{1}$/;
// hsl color
const colorRegHSL =
/^[hH][Ss][Ll][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*)[\)]$/;
// HSLA color
const colorRegHSLA =
/^[hH][Ss][Ll][Aa][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,){2}([\\s]*(1|1.0|0|0?.[0-9]{1,2})[\\s]*)[\)]$/;
/**
* hex to rgba
* @param {*} hex
* @param {*} alpha
*/
function colorHexToRgba(hex, alpha) {
let a = alpha || 1,
hColor = hex.toLowerCase(),
hLen = hex.length,
rgbaColor = [];
if (hex && colorRegExp.test(hColor)) {
//the hex length may be 4 or 7,contained the symbol of #
if (hLen === 4) {
let hSixColor = "#";
for (let i = 1; i < hLen; i++) {
let sColor = hColor.slice(i, i + 1);
hSixColor += sColor.concat(sColor);
}
hColor = hSixColor;
}
for (let j = 1, len = hColor.length; j < len; j += 2) {
rgbaColor.push(parseInt("0X" + hColor.slice(j, j + 2), 16));
}
return util.removeAllSpace("rgba(" + rgbaColor.join(",") + "," + a + ")");
} else {
return util.removeAllSpace(hColor);
}
}
/**
* rgba to hex
* @param {*} rgba
*/
function colorRgbaToHex(rgba) {
const hexObject = { 10: "A", 11: "B", 12: "C", 13: "D", 14: "E", 15: "F" },
hexColor = function (value) {
value = Math.min(Math.round(value), 255);
const high = Math.floor(value / 16),
low = value % 16;
return "" + (hexObject[high] || high) + (hexObject[low] || low);
};
const value = "#";
if (/rgba?/.test(rgba)) {
let values = rgba
.replace(/rgba?\(/, "")
.replace(/\)/, "")
.replace(/[\s+]/g, "")
.split(","),
color = "";
values.map((value, index) => {
if (index <= 2) {
color += hexColor(value);
}
});
return util.removeAllSpace(value + color);
}
}
/**
* hsva to rgba
* @param {*} hsva
* @param {*} alpha
*/
function colorHsvaToRgba(hsva, alpha) {
let r,
g,
b,
a = hsva.a; //rgba(r,g,b,a)
let h = hsva.h,
s = (hsva.s * 255) / 100,
v = (hsva.v * 255) / 100; //hsv(h,s,v)
if (s === 0) {
r = g = b = v;
} else {
let t = v,
p = ((255 - s) * v) / 255,
q = ((t - p) * (h % 60)) / 60;
if (h === 360) {
r = t;
g = b = 0;
} else if (h < 60) {
r = t;
g = p + q;
b = p;
} else if (h < 120) {
r = t - q;
g = t;
b = p;
} else if (h < 180) {
r = p;
g = t;
b = p + q;
} else if (h < 240) {
r = p;
g = t - q;
b = t;
} else if (h < 300) {
r = p + q;
g = p;
b = t;
} else if (h < 360) {
r = t;
g = p;
b = t - q;
} else {
r = g = b = 0;
}
}
if (alpha >= 0 || alpha <= 1) a = alpha;
return util.removeAllSpace(
"rgba(" +
Math.ceil(r) +
"," +
Math.ceil(g) +
"," +
Math.ceil(b) +
"," +
a +
")"
);
}
/**
* hsla to rgba
* 换算公式:https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSL%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2
* @param {*} hsla
*/
function colorHslaToRgba(hsla) {
let h = hsla.h,
s = hsla.s / 100,
l = hsla.l / 100,
a = hsla.a;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
let compareRGB = (p, q, t) => {
if (t > 1) t = t - 1;
if (t < 0) t = t + 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * 6 * (2 / 3 - t);
return p;
};
let q = l >= 0.5 ? l + s - l * s : l * (1 + s),
p = 2 * l - q,
k = h / 360;
r = compareRGB(p, q, k + 1 / 3);
g = compareRGB(p, q, k);
b = compareRGB(p, q, k - 1 / 3);
}
return util.removeAllSpace(
`rgba(${Math.ceil(r * 255)},${Math.ceil(g * 255)},${Math.ceil(
b * 255
)},${a})`
);
}
/**
* rgba to hsla
* 换算公式:https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSL%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2
* @param {*} rgba
*/
function colorRgbaToHsla(rgba) {
const rgbaArr = rgba
.slice(rgba.indexOf("(") + 1, rgba.lastIndexOf(")"))
.split(",");
let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);
let r = parseInt(rgbaArr[0]) / 255,
g = parseInt(rgbaArr[1]) / 255,
b = parseInt(rgbaArr[2]) / 255;
let max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g >= b ? 0 : 6);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
}
return {
colorStr: util.removeAllSpace(
"hsla(" +
Math.ceil(h * 60) +
"," +
Math.ceil(s * 100) +
"%," +
Math.ceil(l * 100) +
"%," +
a +
")"
),
colorObj: {
h,
s,
l,
a,
},
};
}
/**
* rgba to hsva
* @param {*} rgba
*/
function colorRgbaToHsva(rgba) {
const rgbaArr = rgba
.slice(rgba.indexOf("(") + 1, rgba.lastIndexOf(")"))
.split(",");
let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);
let r = parseInt(rgbaArr[0]) / 255,
g = parseInt(rgbaArr[1]) / 255,
b = parseInt(rgbaArr[2]) / 255;
let h, s, v;
let min = Math.min(r, g, b);
let max = (v = Math.max(r, g, b));
let diff = max - min;
if (max === 0) {
s = 0;
} else {
s = 1 - min / max;
}
if (max === min) {
h = 0;
} else {
switch (max) {
case r:
h = (g - b) / diff + (g < b ? 6 : 0);
break;
case g:
h = 2.0 + (b - r) / diff;
break;
case b:
h = 4.0 + (r - g) / diff;
break;
}
h = h * 60;
}
s = s * 100;
v = v * 100;
return {
h,
s,
v,
a,
};
}
/*
* 任意色值(甚至是CSS颜色关键字)转换为RGBA颜色的方法
* 此方法IE9+浏览器支持,基于DOM特性实现
* @param {*} color
*/
function colorToRgba(color) {
const div = document.createElement("div");
util.setCss(div, "background-color", color);
document.body.appendChild(div);
const c = util.getCss(div, "background-color");
document.body.removeChild(div);
let isAlpha = c.match(/,/g) && c.match(/,/g).length > 2;
let result = isAlpha
? c
: c.slice(0, 2) + "ba" + c.slice(3, c.length - 1) + ", 1)";
return util.removeAllSpace(result);
}
/**
* 判断是否是合格的颜色值
* @param {*} color
*/
function isValidColor(color) {
// https://developer.mozilla.org/zh-CN/docs/Web/CSS/color_value#%E8%89%B2%E5%BD%A9%E5%85%B3%E9%94%AE%E5%AD%97
let isTransparent = color === "transparent";
return (
colorRegExp.test(color) ||
colorRegRGB.test(color) ||
colorRegRGBA.test(color) ||
colorRegHSL.test(color) ||
colorRegHSLA.test(color) ||
(colorToRgba(color) !== "rgba(0,0,0,0)" && !isTransparent) ||
isTransparent
);
}
/**
*
* @param {*} color
* @returns
*/
function isAlphaColor(color) {
return (
colorRegRGB.test(color) ||
colorRegRGBA.test(color) ||
colorRegHSL.test(color) ||
colorRegHSLA.test(color)
);
}</code></pre><p>工具方法这些我们已经完成了,接下来就是正式完成我们的主线功能逻辑了。</p><h3>构造函数的定义</h3><p>首先当然是完成我们的构造函数呢,我们把一个颜色选择器看做是一个构造实例,也因此,我们创建一个构造函数。</p><pre><code class="js">function ewColorPicker(options){
//主要逻辑
}</code></pre><p>好的,接下来,让我们完成第一步,校验用户传入的参数,我们分为2种情况,第一种是如果用户传入的是一个DOM元素字符串或者是一个DOM元素,那么我们就要定义一个默认的配置对象,如果用户传入的是一个自定义的对象,那么我们将不采取默认对象。在校验之前,我们先思考一下可能需要处理的错误情况,也就是说假如用户传入的参数不符合规则,我们是不是需要返回一些错误提示给用户知道,现在让我们来定义一下这些错误规则吧。如下所示:</p><pre><code class="js">const NOT_DOM_ELEMENTS = ['html','head','meta','title','link','style','script','body'];
const ERROR_VARIABLE = {
DOM_OBJECT_ERROR:'can not find the element by el property,make sure to pass a correct value!',
DOM_ERROR:'can not find the element,make sure to pass a correct param!',
CONFIG_SIZE_ERROR:'the value must be a string which is one of the normal,medium,small,mini,or must be an object and need to contain width or height property!',
DOM_NOT_ERROR:'Do not pass these elements: ' + NOT_DOM_ELEMENTS.join(',') + ' as a param,pass the correct element such as div!',
PREDEFINE_COLOR_ERROR:'"predefineColor" is a array that is need to contain color value!',
CONSTRUCTOR_ERROR:'ewColorPicker is a constructor and should be called with the new keyword!',
DEFAULT_COLOR_ERROR:'the "defaultColor" is not an invalid color,make sure to use the correct color!'
};</code></pre><p>这些校验错误都是常量,不允许被修改的,所以我们用大写字母来表示。接下来我们就需要在构造函数里做一个校验了。</p><h3>配置属性的定义与校验</h3><p>1.校验是否是实例化</p><p>判断<code>new.target</code>就可以了,如下所示:</p><pre><code class="js">if(util.isUndefined(new.target))return ewError(ERROR_VARIABLE.CONSTRUCTOR_ERROR);</code></pre><p>2.定义一个函数startInit,在这个函数里对具体的属性做判断。如下所示:</p><pre><code class="js">function startInit(context,options){
let initOptions = initConfig(config);
if(!initOptions)return;
// 缓存配置对象属性
context.config = initOptions.config;
//定义私有属性
context._private = {
boxSize: {
b_width: null,
b_height: null
},
pickerFlag: false,
colorValue: "",
};
// 在初始化之前所作的操作
context.beforeInit(initOptions.element,initOptions.config,initOptions.error);
}</code></pre><p>接下来,我们来看initConfig函数,如下所示:</p><pre><code class="js">export function initConfig(config){
// 默认的配置对象属性
const defaultConfig = { ...colorPickerConfig };
let element,error,mergeConfig = null;
//如果第二个参数传的是字符串,或DOM对象,则初始化默认的配置
if (util.isString(config) || util.isDom(config) || util.isJQDom(config)) {
mergeConfig = defaultConfig;
element = util.isJQDom(config) ? config.get(0) : config;
error = ERROR_VARIABLE.DOM_ERROR;
} //如果是对象,则自定义配置,自定义配置选项如下:
else if (util.isDeepObject(config) && (util.isString(config.el) || util.isDom(config.el) || util.isJQDom(config.el))) {
mergeConfig = util.ewAssign(defaultConfig, config);
element = util.isJQDom(config.el) ? config.el.get(0) : config.el;
error = ERROR_VARIABLE.DOM_OBJECT_ERROR;
} else {
if(util.isDeepObject(config)){
error = ERROR_VARIABLE.DOM_OBJECT_ERROR;
}else{
error = ERROR_VARIABLE.DOM_ERROR;
}
}
return {
element,
config:mergeConfig,
error
}
}</code></pre><p>然后我们来看看默认的配置对象属性:</p><pre><code class="js">export const emptyFun = function () { };
const baseDefaultConfig = {
alpha: false,
size: "normal",
predefineColor: [],
disabled: false,
defaultColor: "",
pickerAnimation: "height",
pickerAnimationTime:200,
sure: emptyFun,
clear: emptyFun,
togglePicker: emptyFun,
changeColor: emptyFun,
isClickOutside: true,
}</code></pre><p>接下来,我们来看beforeInit函数,如下所示:</p><pre><code class="js">function beforeInit(element, config, errorText) {
let ele = util.isDom(element) ? element : util.isString(element) ? util.$(element) : util.isJQDom(element) ? element.get(0) : null;
if (!ele) return util.ewError(errorText);
ele = ele.length ? ele[0] : ele;
if (!ele.tagName) return util.ewError(errorText);
if (!isNotDom(ele)) {
if(!this._color_picker_uid){
this._color_picker_uid = util.createUUID();
}
this.init(ele, config);
}
}</code></pre><p>其中,isNotDom方法,我们先定义好:</p><pre><code class="js">const isNotDom = ele => {
if (NOT_DOM_ELEMENTS.indexOf(ele.tagName.toLowerCase()) > -1) {
util.ewError(ERROR_VARIABLE.DOM_NOT_ERROR);
return true;
}
return false;
}</code></pre><p>最后,我们来看init函数,如下所示:</p><pre><code class="js">function init(element, config) {
let b_width, b_height;
//自定义颜色选择器的类型
if (util.isString(config.size)) {
switch (config.size) {
case 'normal':
b_width = b_height = '40px';
break;
case 'medium':
b_width = b_height = '36px';
break;
case 'small':
b_width = b_height = '32px';
break;
case 'mini':
b_width = b_height = '28px';
break;
default:
b_width = b_height = '40px';
break;
}
} else if (util.isDeepObject(config.size)) {
b_width = config.size.width && (util.isNumber(config.size.width) || util.isString(config.size.width)) ? (parseInt(config.size.width) <= 25 ? 25 : parseInt(config.size.width))+ 'px' : '40px';
b_height = config.size.height && (util.isNumber(config.size.height) || util.isString(config.size.height)) ? (parseInt(config.size.height) <= 25 ? 25 : parseInt(config.size.height)) + 'px' : '40px';
} else {
return util.ewError(ERROR_VARIABLE.CONFIG_SIZE_ERROR);
}
this._private.boxSize.b_width = b_width;
this._private.boxSize.b_height = b_height;
//渲染选择器
this.render(element, config);
}</code></pre><p>如此一来,我们的初始化的工作才算是完成,回顾一下,我们在初始化的时候做了哪些操作。我总结如下:</p><ul><li>定义了一些错误的常量,用于提示。</li><li>验证用户传入的参数,分为2种情况,第一种是字符串或者DOM元素,第二种是自定义对象,其中必须指定el属性为一个DOM元素。</li><li>定义了默认配置对象,定义了一些私有变量。</li><li>对色块盒子的大小做了一次规范化。</li></ul><p>接下来,就是我们实际渲染一个颜色选择器的渲染函数,即render函数。</p><h4>render函数</h4><p>render函数的核心思路非常的简单,实际上就是创建一堆元素,然后添加到元素当中去。只不过我们需要注意几点,例如预定义颜色数组,默认颜色值,以及色块盒子的大小,还有就是alpha柱的显隐。如下所示:</p><pre><code class="js">ewColorPicker.prototype.render = function(element,config){
let predefineColorHTML = '',
alphaBar = '',
hueBar = '',
predefineHTML = '',
boxDisabledClassName = '',
boxBackground = '',
boxHTML = '',
clearHTML = '',
sureHTML = '',
inputHTML = '',
btnGroupHTML = '',
dropHTML = '',
openChangeColorModeHTML = '',
openChangeColorModeLabelHTML = '',
horizontalSliderHTML = '',
verticalSliderHTML = '';
const p_c = config.predefineColor;
if (!util.isDeepArray(p_c)) return util.ewError(ERROR_VARIABLE.PREDEFINE_COLOR_ERROR);
if (p_c.length) {
p_c.map((color,index) => {
let isValidColorString = util.isString(color) && isValidColor(color);
let isValidColorObj = util.isDeepObject(color) && color.hasOwnProperty('color') && isValidColor(color.color);
let renderColor = isValidColorString ? color : isValidColorObj ? color.color : '';
let renderDisabled = isValidColorObj ? setPredefineDisabled(color.disabled) : '';
predefineColorHTML += `
<div class="ew-pre-define-color${hasAlpha(renderColor)}${renderDisabled}" tabindex=${index}>
<div class="ew-pre-define-color-item" style="background-color:${renderColor};"></div>
</div>`;
})
};
//打开颜色选择器的方框
const colorBox = config.defaultColor ? `<div class="ew-color-picker-arrow" style="width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};">
<div class="ew-color-picker-arrow-left"></div>
<div class="ew-color-picker-arrow-right"></div>
</div>` : `<div class="ew-color-picker-no" style="width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};line-height:${this._private.boxSize.b_height};">&times;</div>`;
//透明度
if (config.alpha) {
alphaBar = `<div class="ew-alpha-slider-bar">
<div class="ew-alpha-slider-wrapper"></div>
<div class="ew-alpha-slider-bg"></div>
<div class="ew-alpha-slider-thumb"></div>
</div>`;
}
// hue
if (config.hue) {
hueBar = `<div class="ew-color-slider-bar"><div class="ew-color-slider-thumb"></div></div>`;
}
if (predefineColorHTML) {
predefineHTML = `<div class="ew-pre-define-color-container">${predefineColorHTML}</div>`;
}
if (config.disabled || config.boxDisabled) boxDisabledClassName = 'ew-color-picker-box-disabled';
if (config.defaultColor){
if(!isValidColor(config.defaultColor)){
return util.ewError(ERROR_VARIABLE.DEFAULT_COLOR_ERROR)
}else{
config.defaultColor = colorToRgba(config.defaultColor);
}
};
this._private.color = config.defaultColor;
if (!config.disabled && this._private.color) boxBackground = `background:${this._private.color}`;
// 盒子样式
const boxStyle = `width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};${boxBackground}`;
if (config.hasBox) {
boxHTML = `<div class="ew-color-picker-box ${boxDisabledClassName}" tabIndex="0" style="${boxStyle}">${colorBox}</div>`;
}
if (config.hasClear) {
clearHTML = `<button class="ew-color-clear ew-color-drop-btn">${ config.clearText }</button>`;
}
if (config.hasSure) {
sureHTML = `<button class="ew-color-sure ew-color-drop-btn">${ config.sureText }</button>`;
}
if (config.hasClear || config.hasSure) {
btnGroupHTML = `<div class="ew-color-drop-btn-group">${clearHTML}${sureHTML}</div>`;
}
if (config.hasColorInput) {
inputHTML = '<input type="text" class="ew-color-input">';
}
if (config.openChangeColorMode) {
if (!config.alpha || !config.hue) return util.ewError(ERROR_VARIABLE.COLOR_MODE_ERROR);
openChangeColorModeHTML = `<div class="ew-color-mode-container">
<div class="ew-color-mode-up"></div>
<div class="ew-color-mode-down"></div>
</div>`;
openChangeColorModeLabelHTML = `<label class="ew-color-mode-title">${this.colorMode[1]}</label>`;
}
if (config.hasColorInput || config.hasClear || config.hasSure) {
dropHTML = config.openChangeColorMode ? `<div class="ew-color-drop-container ew-has-mode-container">
${openChangeColorModeLabelHTML}${inputHTML}${openChangeColorModeHTML}
</div><div class="ew-color-drop-container">
${btnGroupHTML}
</div>` : `<div class="ew-color-drop-container">
${inputHTML}${btnGroupHTML}
</div>`;
}
this.isAlphaHorizontal = config.alphaDirection === 'horizontal';
this.isHueHorizontal = config.hueDirection === 'horizontal';
if(this.isAlphaHorizontal && this.isHueHorizontal){
horizontalSliderHTML = hueBar + alphaBar;
}else if(!this.isAlphaHorizontal && !this.isHueHorizontal){
verticalSliderHTML = alphaBar + hueBar;
}else{
if(this.isHueHorizontal){
horizontalSliderHTML = hueBar;
verticalSliderHTML = alphaBar;
} else{
horizontalSliderHTML = alphaBar;
verticalSliderHTML = hueBar;
}
}
if(horizontalSliderHTML){
horizontalSliderHTML = `<div class="ew-color-slider ew-is-horizontal">${ horizontalSliderHTML }</div>`
}
if(verticalSliderHTML){
verticalSliderHTML = `<div class="ew-color-slider ew-is-vertical">${ verticalSliderHTML }</div>`;
}
//颜色选择器
const html = `${boxHTML}
<div class="ew-color-picker">
<div class="ew-color-picker-content">
${ verticalSliderHTML }
<div class="ew-color-panel" style="background:red;">
<div class="ew-color-white-panel"></div>
<div class="ew-color-black-panel"></div>
<div class="ew-color-cursor"></div>
</div>
</div>
${ horizontalSliderHTML }
${dropHTML}
${predefineHTML}
</div>`;
element.setAttribute("color-picker-id",this._color_picker_uid);
element.innerHTML = `<div class="ew-color-picker-container">${ html }</div>`;
this.startMain(element, config);
}</code></pre><h4>startMain函数</h4><p>接下来,我们来看看我们要实现哪些逻辑。首先我们需要确定一个初始值的颜色对象,用hsva来表示,我们创建一个initColor函数,代码如下所示:</p><pre><code class="js">function initColor(context, config) {
if (config.defaultColor) {
context.hsvaColor = colorRegRGBA.test(config.defaultColor) ? colorRgbaToHsva(config.defaultColor) : colorRgbaToHsva(colorToRgba(config.defaultColor));
} else {
context.hsvaColor = {
h: 0,
s: 100,
v: 100,
a: 1
};
}
}</code></pre><p>这是我们要实现的第一个逻辑,也就是初始化颜色值,这个颜色值对象将贯穿整个颜色选择器实例,所有的逻辑更改也会围绕它展开。接下来,我们再内部存储一些DOM元素或者一些私有对象属性以及用户传入的配置对象,这样可以方便我们之后操作。</p><p>现在我们再来分析一下,我们可以大致得到主要的逻辑有:</p><ul><li>初始化一些后续需要操作的DOM元素与颜色值以及面板的left与top偏移</li><li>预定义颜色逻辑</li><li>初始化颜色面板的动画逻辑</li><li>色块盒子的处理逻辑</li><li>输入框逻辑</li><li>禁用逻辑</li><li>点击目标区域之外关闭颜色面板的逻辑</li><li>清空按钮与确定按钮的逻辑</li><li>颜色面板的点击逻辑与颜色面板的元素拖拽逻辑</li></ul><p>我们接下来将围绕这几种逻辑一起展开。如下所示:</p><pre><code class="js"> // 初始化逻辑
let scope = this;
this.$Dom = Object.create(null);
this.$Dom.rootElement = ele;
this.$Dom.picker = getELByClass(ele, 'ew-color-picker');
this.$Dom.pickerPanel = getELByClass(ele, 'ew-color-panel');
this.$Dom.pickerCursor = getELByClass(ele, 'ew-color-cursor');
this.$Dom.verticalSlider = getELByClass(ele, 'ew-is-vertical');
// 清空按钮逻辑
this.$Dom.pickerClear = getELByClass(ele, 'ew-color-clear');
this.$Dom.hueBar = getELByClass(ele, 'ew-color-slider-bar');
this.$Dom.hueThumb = getELByClass(ele, 'ew-color-slider-thumb');
this.$Dom.preDefineItem = getELByClass(ele, 'ew-pre-define-color', true);
this.$Dom.box = getELByClass(ele, 'ew-color-picker-box');
// 输入框逻辑
this.$Dom.pickerInput = getELByClass(ele, 'ew-color-input');
// 确定按钮逻辑
this.$Dom.pickerSure = getELByClass(ele, 'ew-color-sure');
initColor(this, config);
//初始化面板的left偏移和top偏移
const panelWidth = this.panelWidth = parseInt(util.getCss(this.$Dom.pickerPanel, 'width'));
const panelHeight = this.panelHeight = parseInt(util.getCss(this.$Dom.pickerPanel, 'height'));
const rect = util.getRect(ele);
this.panelLeft = rect.left;
this.panelTop = rect.top + rect.height;</code></pre><p>接着我们开始初始化预定义颜色逻辑:</p><pre><code class="js"> // 预定义颜色逻辑
if (this.$Dom.preDefineItem.length) {
initPreDefineHandler(util.ewObjToArray(this.$Dom.preDefineItem), scope);
}
function initPreDefineHandler(items, context) {
// get the siblings
const siblings = el => Array.prototype.filter.call(el.parentElement.children, child => child !== el);
items.map(item => {
const clickHandler = event => {
util.addClass(item, 'ew-pre-define-color-active');
siblings(item).forEach(sibling => util.removeClass(sibling, 'ew-pre-define-color-active'))
const bgColor = util.getCss(event.target, 'background-color');
context.hsvaColor = colorRgbaToHsva(bgColor);
setColorValue(context, context.panelWidth, context.panelHeight, true);
changeElementColor(context);
};
const blurHandler = event => util.removeClass(event.target, 'ew-pre-define-color-active');
[{ type: "click", handler: clickHandler }, { type: "blur", handler: blurHandler }].forEach(t => {
if (!context.config.disabled && util.ewObjToArray(item.classList).indexOf('ew-pre-define-color-disabled') === -1) {
util.on(item, t.type, t.handler);
}
});
})
}</code></pre><p>然后我们开始初始化动画逻辑:</p><pre><code class="js"> initAnimation(scope);
function initAnimation(context) {
//颜色选择器打开的动画初始设置
const expression = getAnimationType(context);
util.setCss(context.$Dom.picker, (expression ? 'display' : 'opacity'), (expression ? 'none' : 0))
let pickerWidth = 0, sliderWidth = 0, sliderHeight = 0;
let isVerticalAlpha = !context.isAlphaHorizontal;
let isVerticalHue = !context.isHueHorizontal;
let isHue = context.config.hue;
let isAlpha = context.config.alpha;
if (isAlpha && isHue && isVerticalAlpha && isVerticalHue) {
pickerWidth = 320;
sliderWidth = 28;
} else if (isVerticalAlpha && isAlpha && (!isVerticalHue || !isHue) || (isVerticalHue && isHue && (!isVerticalAlpha || !isAlpha))) {
pickerWidth = 300;
sliderWidth = sliderHeight = 14;
} else {
pickerWidth = 280;
sliderHeight = isAlpha && isHue && !isVerticalHue && !isVerticalAlpha ? 30 : 14;
}
util.setCss(context.$Dom.picker, 'min-width', pickerWidth + 'px');
if (context.$Dom.horizontalSlider) {
util.setCss(context.$Dom.horizontalSlider, 'height', sliderHeight + 'px');
}
if (context.$Dom.verticalSlider) {
util.setCss(context.$Dom.verticalSlider, 'width', sliderWidth + 'px');
}
}</code></pre><p>接下来,就是我们的一些功能逻辑了,让我们一一来实现吧,首先我们需要的实现的是点击色块打开或者关闭颜色选择器面板。如下所示:</p><pre><code class="js">// 色块
if (!config.disabled){
util.on(this.$Dom.box, 'click', () => handlePicker(ele, scope, (flag) => {
if (flag && scope.config.isClickOutside) {
initColor(this, config);
setColorValue(scope, scope.panelWidth, scope.panelHeight, false);
handleClickOutSide(scope, scope.config);
}
}));
}</code></pre><p>这里的逻辑也不复杂,就是判断是否禁用,然后为盒子元素添加点击事件,在这里核心的功能就是<code>handlePicker</code>方法,我们可以看到传入3个参数,第一个参数为当前根容器元素,第二个参数则是当前执行上下文对象,第三个参数则是一个回调函数,用来做一些细节处理。<code>setColorValue</code>方法暂时先不作说明,而<code>initColor</code>方法我们前面已经讲过,<code>handleClickOutSide</code>方法我们将在讲完<code>handlePicker</code>方法之后再做介绍,现在让我们先来看一下<code>handlePicker</code>这个方法吧。</p><pre><code class="js">export function handlePicker(el, scope,callback) {
scope._private.pickerFlag = !scope._private.pickerFlag;
openAndClose(scope);
initColor(scope, scope.config);
setColorValue(scope, scope.panelWidth, scope.panelHeight, false);
if (util.isFunction(scope.config.togglePicker)){
scope.config.togglePicker(el, scope._private.pickerFlag,scope);
}
if(util.isFunction(callback))callback(scope._private.pickerFlag);
}</code></pre><p>可以看到,这个方法的核心操作是改变颜色选择器的状态,最重要的就是<code>openAndClose</code>方法呢,让我们一起来看一下吧,</p><pre><code class="js">export function openAndClose(scope) {
const time = scope.config.pickerAnimationTime;
scope._private.pickerFlag ? open(getAnimationType(scope), scope.$Dom.picker,time) : close(getAnimationType(scope), scope.$Dom.picker,time);
}
export function getAnimationType(scope) {
return scope.config.pickerAnimation;
}</code></pre><p>这个方法就是获取动画执行时间,然后根据<code>pickerFlag</code>来判断是开启还是关闭颜色选择器,核心的就是<code>open</code>与<code>close</code>方法,两者都接收3个参数,第一个则是动画的类型,第二个则是颜色选择器面板元素,第三个则是动画执行时间。我们分别来看一下:</p><p>1.open方法</p><pre><code class="js">export function open(expression, picker,time = 200) {
time = time > 10000 ? 10000 : time;
let animation = '';
switch(expression){
case 'opacity':
animation = 'fadeIn';
break;
default:
animation = 'slideDown';
}
return ani[animation](picker, time);
}</code></pre><p>2.close方法</p><pre><code class="js">export function close(expression, picker,time = 200) {
time = time > 10000 ? 10000 : time;
let animation = '';
switch(expression){
case 'opacity':
animation = 'fadeOut';
break;
default:
animation = 'slideUp';
}
return ani[animation](picker, time);
}</code></pre><p>可以看到,我们再<code>open</code>与<code>close</code>方法内部对时间做了一次限制处理,然后判断动画类型来决定调用哪种动画来实现颜色选择器的开启和关闭。到这里,我们还少实现了一个方法,那就是<code>handleClickOutSide</code>,让我们来一起看一下这个方法的实现:</p><pre><code class="js">export function handleClickOutSide(context, config) {
util.clickOutSide(context, config, () => {
if (context._private.pickerFlag) {
context._private.pickerFlag = false;
closePicker(getAnimationType(config.pickerAnimation), context.$Dom.picker,config.pickerAnimationTime);
}
});
}</code></pre><p>可以看到,我们主要是对颜色选择器面板如果处于开启状态做的一个操作,也就是点击不包含盒子元素区域以外的空间,我们都要关闭颜色选择器面板。这里设计到如何去实现判断我们的鼠标点击是在元素的区域之外呢?有2种方式来实现,第一种判断我们点击的DOM元素是否是颜色选择器元素以及其子元素节点即可,也就是说我们只需要判断我们点击的元素如果是颜色选择器面板容器元素或者是其子元素,我们都不能关闭颜色选择器,并且当然颜色选择器面板还要处于开启中的状态。另一种就是通过坐标值的计算,判断鼠标点击的坐标区间是否在颜色选择器面板的坐标区域内,这里我们采用第二种实现方式,让我们一起来看一下吧。</p><pre><code class="js">util["clickOutSide"] = (context, config, callback) => {
const mouseHandler = (event) => {
const rect = util.getRect(context.$Dom.picker);
const boxRect = util.getRect(context.$Dom.box);
const target = event.target;
if (!target) return;
const targetRect = util.getRect(target);
// 利用rect来判断用户点击的地方是否在颜色选择器面板区域之内
if (targetRect.x >= rect.x && targetRect.y >= rect.y && targetRect.width <= rect.width) return;
// 如果点击的是盒子元素
if (targetRect.x >= boxRect.x && targetRect.y >= boxRect.y && targetRect.width <= boxRect.width && targetRect.height <= boxRect.height) return;
callback();
setTimeout(() => {
util.off(document, util.eventType[0], mouseHandler);
}, 0);
}
util.on(document, util.eventType[0], mouseHandler);
}</code></pre><p>可以看到,我们是通过比较x与y坐标的大小从而确定是否点击的区域属于颜色选择器面板区域,从而确定颜色选择器的关闭状态。当然这也是我们默认会调用的,当然我们也提供了一个可选项来确定是否可以通过点击元素区域之外的空间关闭颜色选择器面板。如下:</p><pre><code class="js">if (config.isClickOutside) {
handleClickOutSide(this, config);
}</code></pre><p>代码不复杂,很容易就理解了。接下来,我们来看<code>alpha</code>透明度的逻辑的实现。如下:</p><pre><code class="js">if (!config.disabled) {
this.bindEvent(this.$Dom.alphaBarThumb, (scope, el, x, y) => changeAlpha(scope, y));
util.on(this.$Dom.alphaBar, 'click', event => changeAlpha(scope, event.y));
}</code></pre><p>可以看到,我们这里首先需要判断是否禁用,然后我们需要2种方式给透明度柱子添加事件逻辑,第一种就是拖拽透明度柱子的滑块元素所触发的拖拽事件,第二种则是点击透明度柱子的事件,这其中涉及到了一个<code>changeAlpha</code>事件。我们来看一下:</p><pre><code class="js">export function changeAlpha(context, position) {
let value = setAlphaHuePosition(context.$Dom.alphaBar,context.$Dom.alphaBarThumb,position);
let currentValue = value.barPosition - value.barThumbPosition <= 0 ? 0 : value.barPosition - value.barThumbPosition;
let alpha = context.isAlphaHorizontal ? 1 - currentValue / value.barPosition : currentValue / value.barPosition;
context.hsvaColor.a = alpha >= 1 ? 1 : alpha.toFixed(2);
changeElementColor(context, true);
}</code></pre><p>这个方法又涉及到了2个方法<code>setAlphaHuePosition</code>与<code>changeElementColor</code>。我们分别来看一下:</p><pre><code class="js">function setAlphaHuePosition(bar,thumb,position){
const positionProp = 'y';
const barProp = 'top';
const barPosition = bar.offsetHeight,
barRect = util.getRect(bar);
const barThumbPosition = Math.max(0,Math.min(position - barRect[positionProp],barPosition));
util.setCss(thumb,barProp,barThumbPosition +'px');
return {
barPosition,
barThumbPosition
}
}</code></pre><p>可以看到,这里我们主要的逻辑操作就是规范化样式处理,也就是说我们拖动滑块改变的是垂直方向上的top偏移(未来会考虑加入水平方向也就是left偏移),所以单独抽取出来做一个公共的方法,这个<code>top</code>偏移会有一个最大值与最小值的比较。接下来,我们来看<code>changeElementColor</code>方法的实现:</p><pre><code class="js"> export function changeElementColor(scope, isAlpha) {
const color = colorHsvaToRgba(scope.hsvaColor);
let newColor = isAlpha || scope.config.alpha ? color : colorRgbaToHex(color);
scope.$Dom.pickerInput.value = newColor;
scope.prevInputValue = newColor;
changeAlphaBar(scope);
if (util.isFunction(scope.config.changeColor))scope.config.changeColor(newColor);
}</code></pre><p>显然这个方法的核心目的就是处理颜色值的改变,我们有2个参数,第一个参数则是当前上下文,第二个参数用于判断透明度柱是否开启。先利用<code>colorHsvaToRgba</code>方法将当前的颜色值转换成<code>rgba</code>颜色,然后判断如果开启了透明度柱,则不需要进行转换,否则就需要转换成<code>hex</code>颜色模式,然后我们把新的颜色值传给<code>input</code>元素。并且缓存了一下这个颜色值,然后这里需要注意一下,如果改变了颜色值,则有可能透明度会改变,因此,需要再次调用<code>changeAlphaBar</code>方法来改变透明度柱的功能。最后我们暴露了一个<code>changeColor</code>方法接口给用户使用。</p><p>前面还提到了一个<code>bindEvent</code>方法,我们接下来来看一下这个<code>bindEvent</code>方法的实现。如下:</p><pre><code class="js">export function bindEvent(el, callback, bool) {
const context = this;
const callResult = event => {
context.moveX = util.eventType[0].indexOf('touch') > -1 ? event.changedTouches[0].clientX : event.clientX;
context.moveY = util.eventType[0].indexOf('touch') > -1 ? event.changedTouches[0].clientY : event.clientY;
bool ? callback(context, context.moveX, context.moveY) : callback(context, el, context.moveX, context.moveY);
}
const handler = () => {
const moveFn = e => { e.preventDefault(); callResult(e); }
const upFn = () => {
util.off(document, util.eventType[1], moveFn);
util.off(document, util.eventType[2], upFn);
}
util.on(document, util.eventType[1], moveFn);
util.on(document, util.eventType[2], upFn);
}
util.on(el, util.eventType[0], handler);
}</code></pre><p>这个方法的核心就是在PC端监听<code>onmousedown,onmousemove,onmouseup</code>事件,在移动端监听<code>touchstart,touchmove,touchend</code>事件并将当前上下文,<code>x</code>坐标以及<code>y</code>坐标回调出去。</p><p>接下来,让我们继续。我们来实现hue色调柱的逻辑,它的逻辑和透明度柱很相似。</p><pre><code class="js">if (!config.disabled) {
//hue的点击事件
util.on(this.$Dom.hueBar, 'click', event => changeHue(scope, event.y))
//hue 轨道的拖拽事件
this.bindEvent(this.$Dom.hueBarThumb, (scope, el, x, y) => changeHue(scope, y));
}</code></pre><p>可以看到,我们同样是判断是否禁用,然后给色调柱添加点击事件以及给hue滑块添加拖拽事件。这里也就核心实现了一个<code>changeHue</code>方法。让我们来看一下吧。</p><pre><code class="js">export function changeHue(context, position) {
const { $Dom:{ hueBar,hueThumb,pickerPanel },_private:{hsvaColor}} = context;
let value = setAlphaHuePosition(hueBar, hueThumb, position);
const { barThumbPosition,barPosition } = value;
context.hsvaColor.h = cloneColor(hsvaColor).h = parseInt(360 * barThumbPosition / barPosition);
util.setCss(pickerPanel, 'background', colorRgbaToHex(colorHsvaToRgba(cloneColor(context.hsvaColor))));
changeElementColor(context);
}</code></pre><p>这个方法,我们首先同样是获取到一个值,由前面的颜色算法我们应该知道,色彩的角度限制在0~360之间,然后我们通过360 * barThumbPosition / barPosition得到了色彩也就是h的相关值。然后我们需要修改颜色面板的背景样式。然后调用<code>changeElementColor</code>方法(这个在前面已经讲过)。前面我们遗留了一个方法,叫做<code>changeAlphaBar</code>,让我们来看一下这个方法做了什么。</p><pre><code class="js">export function changeAlphaBar(scope) {
if (!scope.$Dom.alphaBarBg) return;
let position = 'to top';
util.setCss(scope.$Dom.alphaBarBg, 'background', 'linear-gradient('+ position +',' + colorHsvaToRgba(scope.hsvaColor,0) + ' 0%,' + colorHsvaToRgba(scope.hsvaColor,1) + ' 100%)');
}</code></pre><p>可以看到,实际上我们就是对透明度柱的背景色做了一个修改。由于我们的透明度柱子不一定存在(因为由用户自定义是否显示),所以这里我们是需要做一个判断的。</p><p>接下来,让我们继续来实现一下<code>颜色面板</code>组件的相关逻辑功能。其实它的逻辑与透明度柱和色彩柱一样,都是分为拖拽和点击。如下所示:</p><pre><code class="js">//颜色面板点击事件
util.on(this.$Dom.pickerPanel, 'click', event => onClickPanel(scope, event));
//颜色面板拖拽元素拖拽事件
this.bindEvent(this.$Dom.pickerCursor, (scope, el, x, y) => {
const left = Math.max(0, Math.min(x - scope._private.panelLeft, panelWidth));
const top = Math.max(0, Math.min(y - scope._private.panelTop, panelHeight));
changeCursorColor(scope, left + 4, top + 4, panelWidth, panelHeight);
});</code></pre><p>我们先来看点击逻辑,同样的是监听面板的点击事件,然后调用<code>onClickPanel</code>方法,我们来看一下这个方法的实现。</p><pre><code class="js">export function onClickPanel(scope, eve) {
if (eve.target !== scope.$Dom.pickerCursor) {
//临界值处理
const moveX = eve.layerX;
const moveY = eve.layerY;
const { _private:{ panelWidth,panelHeight }} = context;
const left = moveX >= panelWidth - 1 ? panelWidth : moveX <= 0 ? 0 : moveX;
const top = moveY >= panelHeight - 2 ? panelHeight : moveY <= 0 ? 0 : moveY;
changeCursorColor(scope, left + 4, top + 4,panelWidth,panelHeight)
}
}</code></pre><p>可以看到,我们所做的操作就是获取一个x坐标和y坐标,然后去设置<code>拖拽游标</code>的left和top偏移,这里会有临界值的处理。稍微宽度减1和高度减2是做一层偏差处理。然后再次调用<code>changeCursorColor</code>方法,我们继续来看这个方法的实现。</p><pre><code class="js">export function changeCursorColor(scope, left, top, panelWidth, panelHeight) {
util.setSomeCss(scope.$Dom.pickerCursor, [{ prop: 'left', value: left + 'px' }, { prop: 'top', value: top + 'px' }])
const s = parseInt(100 * (left - 4) / panelWidth);
const v = parseInt(100 * (panelHeight - (top - 4)) / panelHeight);
//需要减去本身的宽高来做判断
scope.hsvaColor.s = s > 100 ? 100 : s < 0 ? 0 : s;
scope.hsvaColor.v = v > 100 ? 100 : v < 0 ? 0 : v;
changeElementColor(scope);
}</code></pre><p>可以看到这个方法我们所做的操作就是设置游标元素的偏移量,以及它的偏移量所代表的的就是hsva颜色模式中的s和v,然后我们再次调用<code>changeElementColor</code>方法就可以改变颜色值了。</p><p>让我们继续看清空按钮的事件逻辑,如下所示:</p><pre><code class="js">util.on(this.$Dom.pickerClear, 'click', () => onClearColor(scope));</code></pre><p>也就是添加点击事件的监听,然后再事件的回调函数中调用<code>onClearColor</code>方法,接下来,我们看<code>onClearColor</code>方法。如下所示:</p><pre><code class="js">export function onClearColor(scope) {
scope._private.pickerFlag = false;
closePicker(getAnimationType(scope.config.pickerAnimation),scope.$Dom.picker,scope.config.pickerAnimationTime);
scope.config.defaultColor = scope._private.color = "";
scope.config.clear(scope.config.defaultColor, scope);
}</code></pre><p>可以看到我们所做的操作比较简单,就是重置颜色选择器开启状态,然后调用关闭颜色选择器方法关闭颜色选择器,然后重置我们的颜色,再回调一个<code>clear</code>方法接口给用户使用。同样的道理,我们的确定按钮的逻辑也就是如此了。如下所示:</p><pre><code class="js">util.on(this.$Dom.pickerSure, 'click', () => onSureColor(scope));</code></pre><p>也就是添加点击事件的监听,然后再事件的回调函数中调用<code>onSureColor</code>方法,接下来,我们看<code>onSureColor</code>方法。如下所示:</p><pre><code class="js">export function onSureColor(scope) {
const result = scope.config.alpha ? colorHsvaToRgba(scope._private.hsvaColor) : colorRgbaToHex(colorHsvaToRgba(scope._private.hsvaColor));
scope._private.pickerFlag = false;
closePicker(getAnimationType(scope.config.pickerAnimation),scope.$Dom.picker,scope.config.pickerAnimationTime);
scope.config.defaultColor = scope._private.color = result;
changeElementColor(scope);
scope.config.sure(result, scope);
}</code></pre><p>可以看到这个操作的逻辑也比较简单,类似于清空按钮的逻辑,我们不外乎需要设置颜色值,然后回调一个<code>sure</code>方法给用户,这个方法回调两个参数,第一个参数为当前选中的颜色值,第二个参数则是当前上下文对象。另外,我们还需要调用<code>changeElementColor</code>方法来改变颜色值。</p><p>接下来,让我们继续来实现一下<code>input</code>框的相关逻辑功能,这也是我们的最后一个逻辑。首先我们需要确定的就是,当<code>input</code>框移开焦点的时候,就意味着更改颜色值。所以我们监听它的移开焦点事件,然后额外封装了一个方法。当然在这之前,我们先需要监听禁用逻辑,如下所示:</p><pre><code class="js">// 禁用逻辑
if (config.disabled) {
if (!util.hasClass(this.$Dom.pickerInput, 'ew-input-disabled')) {
util.addClass(this.$Dom.pickerInput,'ew-input-disabled');
}
if (!util.hasClass(this.$Dom.picker, 'ew-color-picker-disabled')) {
util.addClass(this.$Dom.picker,'ew-color-picker-disabled');
}
this.$Dom.pickerInput.disabled = true;
return false;
}</code></pre><p>可以看到,以上的逻辑,我们就是判断用户是否传入了<code>disabled</code>属性,然后判断<code>input</code>元素是否还有我们自定义的禁用类名<code>ew-input-disabled</code>,如果没有则添加该类名,同样的,我们为<code>picker</code>也做相同的逻辑,最后我们将<code>input</code>元素的<code>disabled</code>属性设置为<code>true</code>。接下来我们来看<code>blur</code>事件的实现:</p><pre><code class="js">util.on(this.$Dom.pickerInput, 'blur', event => onInputColor(scope, event.target.value));</code></pre><p>这段代码很简单,就是添加监听事件,接下来,我们来看<code>onInputColor</code>方法的实现。如下:</p><pre><code class="js"> export function onInputColor(scope, value) {
if (!isValidColor(value)) return;
// 两者相等,说明用户没有更改颜色
if (util.removeAllSpace(scope.prevInputValue) === util.removeAllSpace(value))return;
let color = scope.config.alpha ? colorRgbaToHsva(value) : colorRgbaToHsva(colorHexToRgba(value));
scope.hsvaColor = color;
setColorValue(scope, scope.panelWidth, scope.panelHeight,true);
}</code></pre><p>这段代码的逻辑也不复杂,首先判断输入框的值是否是合格的颜色值或者判断当前值和我们缓存的值是否相同,如果不是合格的颜色值或者与缓存的值相同则不作任何操作。然后我们再根据是否开启了透明度柱来判断是否需要调用<code>colorHexToRgba</code>方法来将颜色值转换成<code>rgba</code>颜色,然后再使用<code>colorRgbaToHsva</code>方法来将颜色值转换成<code>hsva</code>的颜色。然后再赋值。最后再调用<code>setColorValue</code>方法来赋值。接下来,我们就来看<code>setColorValue</code>方法的实现。如下:</p><pre><code class="js">export function setColorValue(context, panelWidth, panelHeight,boxChange) {
changeElementColor(context);
context._private.prevInputValue = context.$Dom.pickerInput.value;
let sliderBarHeight = 0;
let l = parseInt(context.hsvaColor.s * panelWidth / 100),
t = parseInt(panelHeight - context.hsvaColor.v * panelHeight / 100);
[
{
el: context.$Dom.pickerCursor,
prop: 'left',
value: l + 4 + 'px'
},
{
el: context.$Dom.pickerCursor,
prop: 'top',
value: t + 4 + 'px'
},
{
el: context.$Dom.pickerPanel,
prop: 'background',
value: colorRgbaToHex(colorHsvaToRgba(cloneColor(context.hsvaColor)))
}
].forEach(item => util.setCss(item.el, item.prop, item.value));
getSliderBarPosition(context.$Dom.hueBar,(position,prop) => {
util.setCss(context.$Dom.hueThumb, prop, parseInt(context.hsvaColor.h * position / 360) + 'px');
});
if (context.config.alpha) {
getSliderBarPosition(context.$Dom.alphaBar,(position,prop) => {
util.setCss(context.$Dom.alphaBarThumb, prop, position - context.hsvaColor.a * position + 'px');
});
}
}
export function getSliderBarPosition(bar,callback){
let sliderPosition = bar.offsetHeight;
let sliderProp = 'top';
callback(sliderPosition,sliderProp);
}</code></pre><p>这个方法的实现稍微有点复杂,实际上这个方法在前面我们已经用到过,只是没有讲解。接下来,让我们来一一分析这个方法到底做了什么。首先,调用了<code>changeElementColor</code>方法赋值,其次缓存当前的输入框的颜色值,然后计算颜色面板游标元素的left和top偏移量,然后分别设置它们,再然后设置颜色面板的背景色。以及设置色彩柱的偏移量。如果透明度柱子存在,则也要设置透明度柱子的偏移量。</p><p>到目前为止,我们所要实现的颜色选择器的基本功能就已经完成,接下来,我们来对我们的文档做一个总结。我们从分析每一个颜色选择器的模块开始,对应的结构及样式我们都是一一分析了,然后再细化到每一个功能。每一个颜色选择器的模块如下:</p><ul><li>颜色色块</li><li>颜色面板</li><li>色调柱</li><li>透明度柱</li><li>输入框</li><li>清空与确定按钮</li><li>预定义颜色元素列表</li></ul><p>再然后,我们对照每一个模块去一一实现它们的功能。在这些功能中,我们学到了哪些东西呢?</p><ol><li>闭包。(也就是说我们在某一个作用域中访问其它作用域中的变量。例如:bindEvent方法的实现)</li><li>定时器。 (如动画函数的实现)</li><li>颜色转换算法。</li><li>正则表达式。</li><li>面向对象的编程。</li><li>如何实现点击目标区域之外的逻辑功能</li></ol><p>当然还有很多,细细品味下来,我们应该知道远远不止如此,但是我们的文档确实到此为止了,后续应该还会有扩展。让我们后面再见,感谢大家的观看,祝大家能够学习愉快。</p><p>如果觉得本文不够详细,可以查看<a href="https://ke.segmentfault.com/course/1650000040761646">视频课程</a>,感谢支持。当然你也可以查看<a href="https://link.segmentfault.com/?enc=sxUzDmDK0K41Aij049sUAw%3D%3D.3EeOh%2BdcGlfYXYUg1MAMmqZMc1ZkODiJZbaLVI9Uyxf0WdWuPyBkLXr8eIrCsgaL" rel="nofollow">源码</a>。</p>
一个工具函数,实现页面tooltip组件的限制使用
https://segmentfault.com/a/1190000040689791
2021-09-15T16:35:32+08:00
2021-09-15T16:35:32+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
2
<h2>需求描述</h2><p>有这样一个需求:页面已经写了太多的详情表单元素,并且每一个表单元素都使用了<code>Tooltip</code>组件来包裹,这样是不符合需求的,因为用户需要限定当表单元素的文本太多时,也就是说出现了省略号才会出现<code>Tooltip</code>组件的包裹。比如项目采用的element UI组件库。在一个页面有太多的这样的表单元素:</p><pre><code class="html"><el-form>
<el-form-item label="姓名" prop="name">
<el-tooltip :content="form.name">
<el-input v-model="form.name" disabled="true"></el-input>
</el-tooltip>
</el-form-item>
.....后续出现多个这样的元素
</el-form></code></pre><p>如果我每一个都加<code>disabled</code>属性,那么页面模板元素有将近一百个,很显然我这样加是很耗费时间的,很显然对于追求高效的我是不喜欢一个一个加,然后一个一个判断的。</p><p>在这之前,我们需要确定一点,那就是我们控制文本的截断是通过CSS代码来实现的。也就是如下这段代码:</p><pre><code class="css">.el-input {
text-overflow:ellipsis;
white-space:nowrap;
overflow:hidden;
}</code></pre><p>因此,完成以上的需求的第一步就是需要先判断哪些元素满足被截断的条件。那么如何判断文本是否被截断呢?关于这个实现,我想<code>element ui</code>的表格组件符合这种场景,所以我只需要去参考一下<code>element ui</code>的表格组件的截断判断实现就知道了。没错,我在<a href="https://link.segmentfault.com/?enc=x5UeTuewUZnBsY1PZ8UyTw%3D%3D.D5oWqy85ZZIQwbgxiU%2BKkpeLB4xKhviveP7nkVcxbNem4fZbier8tJ0krk2dmpJfHkEOLJFvKT7j4y0LuahMzu%2F0io1vk8iQq7rPKL4%2ByYg%3D" rel="nofollow">源码</a>中找到了实现方式。</p><h3>创建range元素</h3><p>核心思路就是创建一个range元素,通过获取range元素width然后与元素的offsetWidth进行判断就行了。所以,我按照element ui的实现思路完成了这个工具函数。如下所示:</p><pre><code class="js">function isTextOverflow(element) {
// use range width instead of scrollWidth to determine whether the text is overflowing
// to address a potential FireFox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1074543#c3
if (!element || !element.childNodes || !element.childNodes.length) {
return false;
}
const range = document.createRange();
range.setStart(element, 0);
range.setEnd(element, element.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
// if the element has padding style,should add the padding value.
const padding = (parseInt(getStyle(element, 'paddingLeft'), 10) || 0) + (parseInt(getStyle(element, 'paddingRight'), 10) || 0);
return rangeWidth + padding > element.offsetWidth || element.scrollWidth > element.offsetWidth;
}
function hasClass(el, cls) {
if (!el || !cls) {
return false;
}
if (cls.indexOf(" ") > -1) {
return console.error(`className should not contain space!`);
}
if (el.classList) {
return el.classList.contains(cls);
} else {
return (" " + el.className + " ").indexOf(" " + cls + " ") > -1;
}
}
function camelCase(name) {
return name.replace(/([\:\_\-]+(.))/g, (_, separator, letter, offset) => offset ? letter.toUpperCase() : letter).replace(/^moz([A-Z])/, "Moz$1")
}
// IE version more than 9
function getStyle(el, styleName) {
if (!element || !styleName) return null;
styleName = camelCase(styleName);
if (styleName === 'float') {
styleName = 'cssFloat';
}
try {
var computed = document.defaultView.getComputedStyle(element, '');
return element.style[styleName] || computed ? computed[styleName] : null;
} catch (e) {
return element.style[styleName];
}
}</code></pre><h3>收集所有的tooltip组件实例</h3><p>这只是完成了第一步,接下来是第二步,就是我需要将页面上所有包含tooltip组件的vue组件实例都要收集起来。首先我们可以确定页面所有的toolTip组件实例应该都是当前这个表单组件实例的所有子组件,因此,我想到了递归去收集所有包含tooltip组件的组件实例。代码如下:</p><pre><code class="js">export function findToolTip(children,tooltips = []){
//这里写代码
}</code></pre><p>所有的tooltip组件实例都是vue组件实例的子组件,所以我们可以知道我们去循环组件实例的子组件,即<code>vm.$children</code>属性。然后tooltip组件有什么标志呢?或者说我们如何判断该子组件实例就是一个tooltip组件呢?我又参考了<code>element ui tooltip</code>组件的实现,发现<code>element ui tooltip</code>组件都会有一个<code>doDestory</code>方法。所以我们就可以根据这个来做判断。所以递归方法,我们就可以写好了。如下:</p><pre><code class="js">//参数1:子组件实例数组
//参数2:收集组件实例的数组
export function findToolTip(children,tooltips = []){
for(let i = 0,len = children.length;i < len;i++){
//递归条件
if(Array.isArray(children[i]) && children[i].length){
findToolTip(children[i],tooltips);
}else{
//判断如果doDestroy属性是一个方法,则代表是一个tooltip组件,添加到数组中
if(typeof children[i].doDestroy === "function"){
tooltips.push(children[i]);
}
}
}
//把收集到的组件实例返回
return tooltips;
}
//调用方式,传入当前组件实例this对象的所有子组件实例
//如:findToolTip(this.$children)</code></pre><p>我们找到所有tooltip组件实例了,接下来,我们要遍历它,然后判断是否符合截断条件,不符合我们就需要将组件给销毁,事实上,我们直接设置tooltip组件实例的disabled属性为true就可以了。接下来,就是这个工具函数的实现了。如下:</p><pre><code class="js">// 参数1:当前组件实例this对象
// 参数2:用于获取被tooltip包裹的子元素的类名
export function isShowToolTip(vm,className="el-input"){
//这里写代码
}</code></pre><h3>如何修改tooltip组件?</h3><p>接下来,我们就要实现这个工具函数。首先,我们需要确定的是,因为disabled是props数据,props是单向数据流,vue.js是不建议我们直接修改的,那么我们应该如何做到设置disabled呢?我想了很久,想的办法就是重新渲染整个组件,利用<code>Vue.compie API</code>也就是编译API直接重新编译整个tooltip组件,然后替换原本渲染的tooltip组件。如下:</p><pre><code class="js">const res = Vue.compile(`<el-tooltip disabled><el-input value="${ child && child.innerText }" disabled></el-input></el-tooltip>`);
const reRender = new Vue({
render:res.render,
staticRenderFns:res.staticRenderFns
}).$mount(parent);//parent为获取的tooltip组件实例根元素</code></pre><p>所以,接下来在工具函数里,我们可以这样实现:</p><pre><code class="js">// 参数1:当前组件实例this对象
// 参数2:用于获取被tooltip包裹的子元素的类名
export function isShowToolTip(vm,className="el-input"){
//为了确保能够获取到DOM元素,需要调用一次nextTick方法
vm.$nextTick(() => {
const allTooltips = findToolTip(vm.$children);
allTooltips.forEach(item => {
//获取子元素
const child = item.$el.querySelector("." + className);
//判断是否被截断
if(!isTextOverflow(child)){
//获取渲染元素
const parent = item.$el;
const res = Vue.compile(`<el-tooltip disabled><el-input value="${ child && child.innerText }" disabled></el-input></el-tooltip>`);
const reRender = new Vue({
render:res.render,
staticRenderFns:res.staticRenderFns
}).$mount(parent);
}
})
}};
}</code></pre><h3>如何使用这个工具函数?</h3><p>如此一来,就做到了一个方法完成了以上的需求。如何使用这个方法呢,很简单,我们可以在详情组件里面监听表单详情数据的变化。如:</p><pre><code class="js">watch:{
form(val){
if(typeof val === "object" && val && Object.keys(val).length){
//将this当做参数传入即可
isShowToolTip(this);
}
}
}</code></pre><p>关于这个需求更好的实现,如有更好的方法,望不吝赐教。</p>
从零开始使用create-react-app + react + typescript 完成一个网站
https://segmentfault.com/a/1190000040677455
2021-09-13T18:54:59+08:00
2021-09-13T18:54:59+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
14
<h2>在线示例</h2><p>以下是一个已经完成的成品,如图所示:</p><p><img src="/img/bVcUQd8" alt="" title=""></p><p>你也可以点击<a href="https://link.segmentfault.com/?enc=nijKkEJuRlzNcHqdmv%2FnNQ%3D%3D.0vioz9r%2Fnr65XG38TurKwOJgF5%2Fb%2BbLfsjq0GZlvcJjGuVARvdGiKHNUC8qy%2FxLCOMqDKxUgwWXA9fqafbjydg%3D%3D" rel="nofollow">此处</a>查看在线示例。</p><p>也许有人咋一看,看到这个网站有些熟悉,没错,这个网站来源于<a href="https://link.segmentfault.com/?enc=P6Tn3P7yZJJqFgdV6Ee9Og%3D%3D.63B4wXK39DJp5%2BUJ%2FiAX61ngW8S7LXsVpRcpcJNmkF4%3D" rel="nofollow">https://jsisweird.com/</a>。我花了三天时间,用<code>create-react-app + react + typescript</code>重构这个网站,与网站效果不同的是,我没有加入任何的动画,并且我添加了中英文切换以及回到顶部的效果。</p><h2>设计分析</h2><p>观看整个网站,其实整体的架构也不复杂,就是一个首页,20道问题页面以及一个解析页面构成。这些涉及到的问题也好,标题也罢,其实都是一堆定义好的数据,下面我们来一一查看这些数据的定义:</p><h3>问题数据的定义</h3><p>很显然,问题数据是一个对象数组,我们来看结构如下:</p><pre><code class="js"> export const questions = [];
//因为问题本身不需要实现中英文切换,所以我们这里也不需要区分,数组项的结构如:{question:"true + false",answer:["\"truefalse\"","1","NaN","SyntaxError"],correct:"1"},</code></pre><p>数据的表示一眼就可以看出来,<code>question</code>代表问题,<code>answer</code>代表回答选项,<code>correct</code>代表正确答案。让我们继续。</p><h3>解析数据的定义</h3><p>解析数据,需要进行中英文切换,所以我们用一个对象表示,如下:</p><pre><code class="js">export const parseObject = {
"en":{
output:"",//输出文本
answer:"",//用户回答文本:[],
successMsg:"",//用户回答正确文本
errorMsg:"",//用户回答错误文本
detail:[],//问题答案解析文本
tabs:[],//中英文切换选项数组
title:"",//首页标题文本
startContent:"",//首页段落文本
endContent:"",//解析页段落文本
startBtn:"",//首页开始按钮文本
endBtn:"",//解析页重新开始文本
},
"zh":{
//选项同en属性值一致
}
}</code></pre><p>更多详情,请查看<a href="https://link.segmentfault.com/?enc=3ZL7nPnBGIxDcHiR3S1eNA%3D%3D.hVYJKyB2qy1ymZzpZayNUvh5D%2FfTtAm5cGaL8ZZwDXMZtozd5Xt8OJP09DEStp7sE9GCg0ICfqfC6MmeMSFqBL3onaayYEqPGTfGonHGc0%2FVk%2BmvJUAaKiko8E1cY9BA" rel="nofollow">此处源码</a>。</p><p>这其中,由于<code>detail</code>里的数据只是普通文本,我们需要将其转换成<code>HTML字符串</code>,虽然有<code>marked.js</code>这样的库可以帮助我们,但是这里我们的转换规则也比较简单,无需使用<code>marked.js</code>这样的库,因此,我在这里封装了一个简易版本的<code>marked</code>工具函数,如下所示:</p><pre><code class="js">export function marked(template) {
let result = "";
result = template.replace(/\[.+?\]\(.+?\)/g,word => {
const link = word.slice(word.indexOf('(') + 1, word.indexOf(')'));
const linkText = word.slice(word.indexOf('[') + 1, word.indexOf(']'));
return `<a href="${link}" target="blank">${linkText}</a>`;
}).replace(/\*\*\*([\s\S]*?)\*\*\*[\s]?/g,text => '<code>' + text.slice(3,text.length - 4) + '</code>');
return result;
}</code></pre><p>转换规则也比较简单,就是匹配<code>a</code>标签以及<code>code</code>标签,这里我们写的是类似<code>markdown</code>的语法。比如<code>a</code>标签的写法应该是如下所示:</p><pre><code class="md">[xxx](xxx)</code></pre><p>所以以上的转换函数,我们匹配的就是这种结构的字符串,其正则表达式结构如:</p><pre><code class="js">/\[.+?\]\(.+?\)/g;</code></pre><p>这其中<code>.+?</code>表示匹配任意的字符,这个正则表达式就不言而喻了。除此之外,我们匹配代码高亮的<code>markdown</code>的语法定义如下:</p><pre><code class="js">***//code***</code></pre><p>为什么我要如此设计?这是因为如果我也使用<code>markdown</code>的<code>三个模板字符串符号</code>来定义代码高亮,会和js的<code>模板字符串起冲突</code>,所以为了不必要的麻烦,我改用了三个<code>*</code>来表示,所以以上的正则表达式才会匹配<code>*</code>。如下:</p><pre><code class="js">/\*\*\*([\s\S]*?)\*\*\*[\s]?/g</code></pre><p>那么以上的正则表达式应该如何理解呢?首先,我们需要确定的是<code>\s</code>以及<code>\S</code>代表什么意思,<code>*</code>在正则表达式中需要转义,所以加了<code>\</code>,这个正则表达式的意思就是匹配<code>***//code***</code>这样的结构。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=V1Ek7Xhq%2BkEqTEjCYL7mdw%3D%3D.8epQuFafZ5XnOR3gRdjjU2m1ZWXFABSahBiKWYO8Ic5lSdYniYQc3f0eEXf8JClFL9sfH44yvTCXgm87sUN4iGcVq5lmdKceE9NaqstoTPKzijcrK2hlauJTFUXRKE66" rel="nofollow">此处</a>。</p><h3>其它文本的定义</h3><p>还有2处的文本的定义,也就是问题选项的统计以及用户回答问题的统计,所以我们分别定义了2个函数来表示,如下:</p><pre><code class="js">export function getCurrentQuestion(lang="en",order= 1,total = questions.length){
return lang === 'en' ? `Question ${ order } of ${ total }` : `第${ order }题,共${ total }题`;
}
export function getCurrentAnswers(lang = "en",correctNum = 0,total= questions.length){
return lang === 'en' ? `You got ${ correctNum } out of ${ total } correct!` : `共 ${ total }道题,您答对了 ${ correctNum } 道题!`;
}</code></pre><p>这2个工具函数接受3个参数,第一个参数代表语言类型,默认值是"en"也就是英文模式,第二个代表当前第几题/正确题数,第三个参数代表题的总数。然后根据这几个参数返回一段文本,这个也没什么好说的。</p><h2>实现思路分析</h2><h3>初始化项目</h3><p>此处略过。可以参考<a href="https://link.segmentfault.com/?enc=PFu8ra%2BjUCqQ08QiyU6wdA%3D%3D.njT5OANKCtH66oBbALBIrgrKdywU8AjgAodnWZkf6Y3NO83zXn%2BCe3OUVOWlAFyKbe0xIA%2FHAsiuz%2BPaFpMV5w%3D%3D" rel="nofollow">文档</a>。</p><h3>基础组件的实现</h3><p>接下来,我们实际上可以将页面分成三大部分,第一部分即首页,第二部分即问题选项页,第三部分则是问题解析页面,在解析页面由于解析内容过多,所以我们需要一个回到顶部的效果。在提及这三个部分的实现之前,我们首先需要封装一些公共的组件,让我们来一起看一下吧!</p><h3>中英文选项卡切换组件</h3><p>不管是首页也好,问题页也罢,我们都会看到右上角有一个中英文切换的选项卡组件,效果自不比多说,让我们来思考一下应该如何实现。首先思考一下DOM结构。我们可以很快就想到结构如下:</p><pre><code class="html"><div class="tab-container">
<div class="tab-item">en</div>
<div class="tab-item">zh</div>
</div></code></pre><p>在这里,我们应该知道类名应该会是动态操作的,因为需要添加一个选中效果,暂定类名为<code>active</code>,我在这里使用的是事件代理,将事件代理到父元素<code>tab-container</code>上。并且它的文本也是动态的,因为需要区分中英文。于是我们可以很快写出如下的代码:</p><pre><code class="js">import React from "react";
import { parseObject } from '../data/data';
import "../style/lang.css";
export default class LangComponent extends React.Component {
constructor(props){
super(props);
this.state = {
activeIndex:0
};
}
onTabHandler(e){
const { nativeEvent } = e;
const { classList } = nativeEvent.target;
if(classList.contains('tab-item') && !classList.contains('tab-active')){
const { activeIndex } = this.state;
let newActiveIndex = activeIndex === 0 ? 1 : 0;
this.setState({
activeIndex:newActiveIndex
});
this.props.changeLang(newActiveIndex);
}
}
render(){
const { lang } = this.props;
const { activeIndex } = this.state;
return (
<div className="tab-container" onClick = { this.onTabHandler.bind(this) }>
{
parseObject[lang]["tabs"].map(
(tab,index) =>
(
<div className={`tab-item ${ activeIndex === index ? 'tab-active' : ''}`} key={tab}>{ tab }</div>
)
)
}
</div>
)
}
}</code></pre><p>css样式代码如下:</p><pre><code class="css">.tab-container {
display: flex;
align-items: center;
justify-content: center;
border:1px solid #f2f3f4;
border-radius: 5px;
position: fixed;
top: 15px;
right: 15px;
}
.tab-container > .tab-item {
padding: 8px 15px;
color: #e7eaec;
cursor: pointer;
background: linear-gradient(to right,#515152,#f3f3f7);
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.tab-container > .tab-item:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius:5px;
}
.tab-container > .tab-item:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius:5px;
}
.tab-container > .tab-item.tab-active,.tab-container > .tab-item:hover {
color: #fff;
background: linear-gradient(to right,#53b6e7,#0c6bc9);
}</code></pre><p><code>js</code>逻辑,我们可以看到我们通过父组件传递一个<code>lang</code>参数用来确定中英文模式,然后开始访问定义数据上的<code>tabs</code>,即数组,<code>react.js</code>渲染列表通常都是使用<code>map</code>方法。事件代理,我们可以看到我们是通过获取原生事件对象<code>nativeEvent</code>拿到类名,判断元素是否含有<code>tab-item</code>类名,从而确定点击的是子元素,然后调用<code>this.setState</code>更改当前的索引项,用来确定当前是哪项被选中。由于只有两项,所以我们可以确定当前索引项不是<code>0</code>就是<code>1</code>,并且我们也暴露了一个事件<code>changeLang</code>给父元素以便父元素可以实时的知道语言模式的值。</p><p>至于样式,都是比较基础的样式,没有什么好说的,需要注意的就是我们是使用固定定位将选项卡组件固定在右上角的。以上的源码可以查看<a href="https://link.segmentfault.com/?enc=aaAoiU9dt3c17u5oR6lxqw%3D%3D.c2%2FhhAU9wlwTR736qbUxmSqSUnCrw1Od8M7elfyMf7mW5otOBtz4NEuLVkfyYA%2FwMjGkYfOR7geNcHfk18L9h455elbXKAkwBfecnUMudpJtzM4YdMZCNmeUbOuixVXyGPWda%2FqtvxnQDJp9oVcvzQ%3D%3D" rel="nofollow">此处</a>。</p><p>接下来,我们来看第二个组件的实现。</p><h3>底部内容组件</h3><p>底部内容组件比较简单,就是一个标签包裹内容。代码如下:</p><pre><code class="js">import React from "react";
import "../style/bottom.css";
const BottomComponent = (props) => {
return (
<div className="bottom" id="bottom">{ props.children }</div>
)
}
export default BottomComponent;</code></pre><p>CSS代码如下:</p><pre><code class="css">.bottom {
position: fixed;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
}</code></pre><p>也就是函数组件的写法,采用固定定位定位在底部。以上的源码可以查看<a href="https://link.segmentfault.com/?enc=lrFKUTUZCBEBtqOcOGLPFA%3D%3D.L0W%2FA2PlGkHeqqHoApvXuODrK%2FfVJzxArL9hQkG5nW853lQUUXPCuXYulUvpxb6KWkoYslWBUftoIgssycLvBne7ud41htvFpoE0j94zZoxwJChR1r9AFDfLz9F7MCYi5VJS4zyPYE3WZyIy%2BOE7Ew%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>内容组件的实现</h3><p>该组件的实现也比较简单,就是用<code>p</code>标签包装了一下。如下:</p><pre><code class="js">import React from "react";
import "../style/content.css";
const ContentComponent = (props) => {
return (
<p className="content">{ props.children }</p>
)
}
export default ContentComponent;</code></pre><p>CSS样式代码如下:</p><pre><code class="css">.content {
max-width: 35rem;
width: 100%;
line-height: 1.8;
text-align: center;
font-size: 18px;
color: #fff;
}</code></pre><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=pQJrxoG2UbZa7xDC7L6zTw%3D%3D.1S2haBIsz%2BaRUD9FL79%2BgmlBHMjg2g4OW%2BSILML3XGCvmcZqCXb0hsnrJnfsxapcp9ZQYENtMpT13Cf1f1h62jobBNAMprew5y%2FV0QOapPbY3xD%2BbF8l640O5cWvNsFC7L95P8DPqdfAtQDvPu%2BgQQ%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>渲染HTML字符串的组件</h3><p>这个组件其实也就是利用了<code>react.js</code>的<code>dangerouslySetInnerHTML</code>属性来渲染<code>html</code>字符串的。代码如下:</p><pre><code class="js">import "../style/render.css";
export function createMarkup(template) {
return { __html: template };
}
const RenderHTMLComponent = (props) => {
const { template } = props;
let renderTemplate = typeof template === 'string' ? template : "";
return <div dangerouslySetInnerHTML={createMarkup( renderTemplate )} className="render-content"></div>;
}
export default RenderHTMLComponent;</code></pre><p>CSS样式代码如下:</p><pre><code class="css">.render-content a,.render-content{
color: #fff;
}
.render-content a {
border-bottom:1px solid #fff;
text-decoration: none;
}
.render-content code {
color: #245cd4;
background-color: #e5e2e2;
border-radius: 5px;
font-size: 16px;
display: block;
white-space: pre;
padding: 15px;
margin: 15px 0;
word-break: break-all;
overflow: auto;
}
.render-content a:hover {
color:#efa823;
border-color: #efa823;
}</code></pre><p>如代码所示,我们可以看到其实我们就是<code>dangerouslySetInnerHTML</code>属性绑定一个函数,将模板字符串当做参数传入这个函数组件,在函数组件当中,我们返回一个对象,结构即:<code>{ __html:template }</code>。其它也就没有什么好说的。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=z34Wtnt3uEDJMcsc1XXLfA%3D%3D.hCfiIZJSTJoTJ0k2lljYkM7QzSUEFMjiDSAKt%2FNF8lfAY8eC6xRhG2F2MewtoCUIKwuSozWEOOM7GWrK3eZ2fiN4ruaVjMh21QmQeSWBnJn9FlB%2BXOs7DJUjP1W1aoIAOIdHfJIcacw2OoIpg7Hazg%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>标题组件的实现</h3><p>标题组件也就是对<code>h1~h6</code>标签的一个封装,代码如下:</p><pre><code class="js">import React from "react";
const TitleComponent = (props) => {
let TagName = `h${ props.level || 1 }`;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;</code></pre><p>整体逻辑也不复杂,就是根据父元素传入的一个<code>level</code>属性从而确定是<code>h1 ~ h6</code>的哪个标签,也就是动态组件的写法。在这里,我们使用了<code>Fragment</code>来包裹了一下组件,关于<code>Fragment</code>组件的用法可以参考<a href="https://link.segmentfault.com/?enc=BNexf%2FRkt8lBnt4KTIQ07Q%3D%3D.kXU0f42E%2F353mMqKCA%2BjkFVLUsU58ws91gfbCwUweZEvenriTPI6Qbykile%2BTIVe" rel="nofollow">文档</a>。我的理解,它就是一个占位标签,由于<code>react.js</code>虚拟DOM的限制需要提供一个根节点,所以这个占位标签的出现就是为了解决这个问题。当然,如果是<code>typescript</code>,我们还需要显示的定义一个类型,如下:</p><pre><code class="js">import React, { FunctionComponent,ReactNode }from "react";
interface propType {
level:number,
children?:ReactNode
}
//这一行代码是需要的
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const TitleComponent:FunctionComponent<propType> = (props:propType) => {
//这里断言一下只能是h1~h6的标签名
let TagName = `h${ props.level }` as HeadingTag;
return (
<React.Fragment>
<TagName>{ props.children }</TagName>
</React.Fragment>
)
}
export default TitleComponent;</code></pre><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=phJB%2BZci%2FbUlYEssS1TG8w%3D%3D.HKEWy3znK7K6mEcJAVbUUVTT8LgrJQIDbS%2FrPLB%2BIsf5RmQNb1xjlHvkOjOxGZQSwyEM8F6kVk3pQ5kUFtue3Uw4uFtN1v5BvdJ%2FMjsCe%2B0%2FnU2E7NSrZPa5wlIjNJQjkEn6cx2bNioxjHHqM%2FfF9g%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>按钮组件的实现</h3><p>按钮组件是一个最基本的组件,它的默认样式肯定是不符合我们的需求的,所以我们需要将它简单的封装一下。如下所示:</p><pre><code class="js">import React from "react";
import "../style/button.css";
export default class ButtonComponent extends React.Component {
constructor(props){
super(props);
this.state = {
typeArr:["primary","default","danger","success","info"],
sizeArr:["mini",'default',"medium","normal","small"]
}
}
onClickHandler(){
this.props.onClick && this.props.onClick();
}
render(){
const { nativeType,type,long,size,className,forwardedRef } = this.props;
const { typeArr,sizeArr } = this.state;
const buttonType = type && typeArr.indexOf(type) > -1 ? type : 'default';
const buttonSize = size && sizeArr.indexOf(size) > -1 ? size : 'default';
let longClassName = '';
let parentClassName = '';
if(className){
parentClassName = className;
}
if(long){
longClassName = "long-btn";
}
return (
<button
ref={forwardedRef}
type={nativeType}
className={ `btn btn-${ buttonType } ${ longClassName } btn-size-${buttonSize} ${parentClassName}`}
onClick={ this.onClickHandler.bind(this)}
>{ this.props.children }</button>
)
}
}</code></pre><p>CSS样式代码如下:</p><pre><code class="css">.btn {
padding: 14px 18px;
outline: none;
display: inline-block;
border: 1px solid var(--btn-default-border-color);
color: var(--btn-default-font-color);
border-radius: 8px;
background-color: var(--btn-default-color);
font-size: 14px;
letter-spacing: 2px;
cursor: pointer;
}
.btn.btn-size-default {
padding: 14px 18px;
}
.btn.btn-size-mini {
padding: 6px 8px;
}
.btn:not(.btn-no-hover):hover,.btn:not(.btn-no-active):active,.btn.btn-active {
border-color: var(--btn-default-hover-border-color);
background-color: var(--btn-default-hover-color);
color:var(--btn-default-hover-font-color);
}
.btn.long-btn {
width: 100%;
}</code></pre><p>这里对按钮的封装,主要是将按钮分类,通过叠加类名的方式,给按钮加各种类名,从而达到不同类型的按钮的实现。然后暴露一个<code>onClick</code>事件。关于样式代码,这里是通过CSS变量的方式。代码如下:</p><pre><code class="css">:root {
--btn-default-color:transparent;
--btn-default-border-color:#d8dbdd;
--btn-default-font-color:#ffffff;
--btn-default-hover-color:#fff;
--btn-default-hover-border-color:#a19f9f;
--btn-default-hover-font-color:#535455;
/* 1 */
--bg-first-radial-first-color:rgba(50, 4, 157, 0.271);
--bg-first-radial-second-color:rgba(7,58,255,0);
--bg-first-radial-third-color:rgba(17, 195, 201,1);
--bg-first-radial-fourth-color:rgba(220,78,78,0);
--bg-first-radial-fifth-color:#09a5ed;
--bg-first-radial-sixth-color:rgba(255,0,0,0);
--bg-first-radial-seventh-color:#3d06a3;
--bg-first-radial-eighth-color:#7eb4e6;
--bg-first-radial-ninth-color:#4407ed;
/* 2 */
--bg-second-radial-first-color:rgba(50, 4, 157, 0.41);
--bg-second-radial-second-color:rgba(7,58,255,0.1);
--bg-second-radial-third-color:rgba(17, 51, 201,1);
--bg-second-radial-fourth-color:rgba(220,78,78,0.2);
--bg-second-radial-fifth-color:#090ded;
--bg-second-radial-sixth-color:rgba(255,0,0,0.1);
--bg-second-radial-seventh-color:#0691a3;
--bg-second-radial-eighth-color:#807ee6;
--bg-second-radial-ninth-color:#07ede1;
/* 3 */
--bg-third-radial-first-color:rgba(50, 4, 157, 0.111);
--bg-third-radial-second-color:rgba(7,58,255,0.21);
--bg-third-radial-third-color:rgba(118, 17, 201, 1);
--bg-third-radial-fourth-color:rgba(220,78,78,0.2);
--bg-third-radial-fifth-color:#2009ed;
--bg-third-radial-sixth-color:rgba(255,0,0,0.3);
--bg-third-radial-seventh-color:#0610a3;
--bg-third-radial-eighth-color:#c07ee6;
--bg-third-radial-ninth-color:#9107ed;
/* 4 */
--bg-fourth-radial-first-color:rgba(50, 4, 157, 0.171);
--bg-fourth-radial-second-color:rgba(7,58,255,0.2);
--bg-fourth-radial-third-color:rgba(164, 17, 201, 1);
--bg-fourth-radial-fourth-color:rgba(220,78,78,0.1);
--bg-fourth-radial-fifth-color:#09deed;
--bg-fourth-radial-sixth-color:rgba(255,0,0,0);
--bg-fourth-radial-seventh-color:#7106a3;
--bg-fourth-radial-eighth-color:#7eb4e6;
--bg-fourth-radial-ninth-color:#ac07ed;
}</code></pre><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=%2BGK%2F6p9KzJveJGv0kZagRg%3D%3D.rv5ITa0%2BRZdqAv6xSGd8wST23dgjou7BdZWsY%2B505ZbDgK%2B%2F7VuPRJG7dV5VxLcWtf%2BEJtjdKTT0tTduyPtER1N4kYAz6mCLsZGYC7xxDAVG0muFzm9VjwLwhirlTzZ4DOGCM%2BfyUCD9rmMXGSbJgA%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><blockquote>注意:这里的按钮组件样式事实上还没有写完,其它类型的样式因为我们要实现的网站没有用到所以没有去实现。</blockquote><h3>问题选项组件</h3><p>实际上就是问题部分页面的实现,我们先来看实际的代码:</p><pre><code class="js">import React from "react";
import { QuestionArray } from "../data/data";
import ButtonComponent from './buttonComponent';
import TitleComponent from './titleComponent';
import "../style/quiz-wrapper.css";
export default class QuizWrapperComponent extends React.Component {
constructor(props:PropType){
super(props);
this.state = {
}
}
onSelectHandler(select){
this.props.onSelect && this.props.onSelect(select);
}
render(){
const { question } = this.props;
return (
<div className="quiz-wrapper flex-center flex-direction-column">
<TitleComponent level={1}>{ question.question }</TitleComponent>
<div className="button-wrapper flex-center flex-direction-column">
{
question.answer.map((select,index) => (
<ButtonComponent
nativeType="button"
onClick={ this.onSelectHandler.bind(this,select)}
className="mt-10 btn-no-hover btn-no-active"
key={select}
long
>{ select }</ButtonComponent>
))
}
</div>
</div>
)
}
}</code></pre><p>css样式代码如下:</p><pre><code class="css">.quiz-wrapper {
width: 100%;
height: 100vh;
padding: 1rem;
max-width: 600px;
}
.App {
height: 100vh;
overflow:hidden;
}
.App h1 {
color: #fff;
font-size: 32px;
letter-spacing: 2px;
margin-bottom: 15px;
text-align: center;
}
.App .button-wrapper {
max-width: 25rem;
width: 100%;
display: flex;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height:100vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-first-radial-first-color) 0,var(--bg-first-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%,var(--bg-first-radial-third-color) 1%,var(--bg-first-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%,var(--bg-first-radial-fifth-color) 1%,var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%,var(--bg-first-radial-seventh-color) 1%,var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%,var(--bg-first-radial-eighth-color) 0,var(--bg-first-radial-ninth-color) 100%);
animation:background 50s linear infinite;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.mt-10 {
margin-top: 10px;
}
.ml-5 {
margin-left: 5px;
}
.text-align {
text-align: center;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-direction-column {
flex-direction: column;
}
.w-100p {
width: 100%;
}
::-webkit-scrollbar {
width: 5px;
height: 10px;
background: linear-gradient(45deg,#e9bf89,#c9a120,#c0710a);
}
::-webkit-scrollbar-thumb {
width: 5px;
height: 5px;
background: linear-gradient(180deg,#d33606,#da5d4d,#f0c8b8);
}
@keyframes background {
0% {
background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-first-radial-first-color) 0,var(--bg-first-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%,var(--bg-first-radial-third-color) 1%,var(--bg-first-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%,var(--bg-first-radial-fifth-color) 1%,var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%,var(--bg-first-radial-seventh-color) 1%,var(--bg-first-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%,var(--bg-first-radial-eighth-color) 0,var(--bg-first-radial-ninth-color) 100%);
}
25%,50% {
background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-second-radial-first-color) 0,var(--bg-second-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%,var(--bg-second-radial-third-color) 1%,var(--bg-second-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%,var(--bg-second-radial-fifth-color) 1%,var(--bg-second-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%,var(--bg-second-radial-seventh-color) 1%,var(--bg-second-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%,var(--bg-second-radial-eighth-color) 0,var(--bg-second-radial-ninth-color) 100%);
}
50%,75% {
background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-third-radial-first-color) 0,var(--bg-third-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%,var(--bg-third-radial-third-color) 1%,var(--bg-third-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%,var(--bg-third-radial-fifth-color) 1%,var(--bg-third-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%,var(--bg-third-radial-seventh-color) 1%,var(--bg-third-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%,var(--bg-third-radial-eighth-color) 0,var(--bg-third-radial-ninth-color) 100%);
}
100% {
background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-fourth-radial-first-color) 0,var(--bg-fourth-radial-second-color) 100%),
radial-gradient(113% 91% at 17% -2%,var(--bg-fourth-radial-third-color) 1%,var(--bg-fourth-radial-fourth-color) 99%),
radial-gradient(142% 91% at 83% 7%,var(--bg-fourth-radial-fifth-color) 1%,var(--bg-fourth-radial-sixth-color) 99%),
radial-gradient(142% 91% at -6% 74%,var(--bg-fourth-radial-seventh-color) 1%,var(--bg-fourth-radial-sixth-color) 99%),
radial-gradient(142% 91% at 111% 84%,var(--bg-fourth-radial-eighth-color) 0,var(--bg-fourth-radial-ninth-color) 100%);
}
}</code></pre><p>可以看到,我们使用<code>h1</code>标签来显示问题,四个选项都使用的按钮标签,我们将按钮标签选中的是哪一项,通过暴露一个事件<code>onSelect</code>给传递出去。通过使用该组件的时候传递<code>question</code>数据就可以确定一组问题以及选项答案。所以实现效果如下图所示:</p><p><img src="/img/bVcUQep" alt="" title=""></p><p>这个组件里面可能比较复杂一点的是<code>CSS</code>布局,有采用弹性盒子布局以及背景色渐变动画等等,其它的也没什么好说的。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=UyonWYSf88q44FQtUsgeVA%3D%3D.34mhz4c1w7Fdvrhi%2Fn6GJCCUYQqgzrWCWph9EOL%2FmFXs05wyIgngr%2BiC8Nfw15YR7BmuHiBzfIWcfkAzgpN9awHQMAXbwPGqxN9XG1WaZ1UDMzyYezEVIE1g4TpUc3rnBQQ%2FIEvl0yLcRB0a5LxAoQ%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>解析组件</h3><p>解析组件实际上就是解析页面部分的一个封装。我们先来看一下实现效果:</p><p><img src="/img/bVcUQeq" alt="" title=""></p><p>根据上图,我们可以得知解析组件分为六大部分。第一部分首先是对用户回答所作的一个正确统计,实际上就是一个标题组件,第二部分则同样也是一个标题组件,也就是题目信息。第三部分则是正确答案,第四部分则是用户的回答,第五部分则是确定用户回答是正确还是错误,第六部分就是实际的解析。</p><p>我们来看一下实现代码:</p><pre><code class="js">import React from "react";
import { parseObject,questions } from "../data/data";
import { marked } from "../utils/marked";
import RenderHTMLComponent from './renderHTML';
import "../style/parse.css";
export default class ParseComponent extends React.Component {
constructor(props){
super(props);
this.state = {};
}
render(){
const { lang,userAnswers } = this.props;
const setTypeClassName = (index) =>
`answered-${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;
return (
<ul className="result-list">
{
parseObject[lang].detail.map((content,index) => (
<li
className={`result-item ${ setTypeClassName(index) }`} key={content}>
<span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span>
<div className="result-item-wrapper">
<span className="result-correct-answer">
{ parseObject[lang].output }:<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span>
<span className="result-user-answer">
{parseObject[lang].answer }:<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span>
<span
className={`inline-answer ${ setTypeClassName(index) }`}>
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span>
<RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent>
</div>
</li>
))
}
</ul>
)
}
}</code></pre><p>CSS样式代码如下:</p><pre><code class="css">.result-wrapper {
width: 100%;
height: 100%;
padding: 60px 15px 40px;
overflow-x: hidden;
overflow-y: auto;
}
.result-wrapper .result-list {
list-style: none;
padding-left: 0;
width: 100%;
max-width: 600px;
}
.result-wrapper .result-list .result-item {
background-color: #020304;
border-radius: 4px;
margin-bottom: 2rem;
color: #fff;
}
.result-content .render-content {
max-width: 600px;
line-height: 1.5;
font-size: 18px;
}
.result-wrapper .result-question {
padding:25px;
background-color: #1b132b;
font-size: 22px;
letter-spacing: 2px;
border-radius: 4px 4px 0 0;
}
.result-wrapper .result-question .order {
margin-right: 8px;
}
.result-wrapper .result-item-wrapper,.result-wrapper .result-list .result-item {
display: flex;
flex-direction: column;
}
.result-wrapper .result-item-wrapper {
padding: 25px;
}
.result-wrapper .result-item-wrapper .result-user-answer {
letter-spacing: 1px;
}
.result-wrapper .result-item-wrapper .result-correct-answer .result-correct-answer-value,
.result-wrapper .result-item-wrapper .result-user-answer .result-user-answer-value {
font-weight: bold;
font-size: 20px;
}
.result-wrapper .result-item-wrapper .inline-answer {
padding:15px 25px;
max-width: 250px;
margin:1rem 0;
border-radius: 5px;
}
.result-wrapper .result-item-wrapper .inline-answer.answered-incorrectly {
background-color: #d82323;
}
.result-wrapper .result-item-wrapper .inline-answer.answered-correctly {
background-color: #4ee24e;
}</code></pre><p>可以看到根据我们前面分析的六大部分,我们已经可以确定我们需要哪些组件,首先肯定是渲染一个列表,因为有20道题的解析,并且我们也知道根据传递的<code>lang</code>确定中英文模式。另外一个<code>userAnswers</code>则是用户的回答,根据用户的回答和正确答案做匹配,我们就可以知道用户回答是正确还是错误。这也就是如下这行代码的意义:</p><pre><code class="js">const setTypeClassName = (index) => `answered-${ questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;</code></pre><p>就是通过索引,确定返回的是正确的类名还是错误的类名,通过类名来添加样式,从而确定用户回答是否正确。我们将以上代码拆分一下,就很好理解了。如下:</p><h4>1.题目信息</h4><pre><code class="js"><span className="result-question">
<span className="order">{(index + 1)}.</span>
{ questions[index].question }
</span></code></pre><h4>2.正确答案</h4><pre><code class="js"> <span className="result-correct-answer">
{ parseObject[lang].output }:
<span className="ml-5 result-correct-answer-value">{ questions[index].correct }</span>
</span></code></pre><h4>3.用户回答</h4><pre><code class="js"><span className="result-user-answer">
{parseObject[lang].answer }:
<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span></code></pre><h4>4.提示信息</h4><pre><code class="js"><span className={`inline-answer ${ setTypeClassName(index) }`}>
{
questions[index].correct === userAnswers[index]
? parseObject[lang].successMsg
: parseObject[lang].errorMsg
}
</span></code></pre><h4>5.答案解析</h4><p>答案解析实际上就是渲染<code>HTML</code>字符串,所以我们就可以通过使用之前封装好的组件。</p><pre><code class="js"><RenderHTMLComponent template={ marked(content) }></RenderHTMLComponent></code></pre><p>这个组件完成之后,实际上,我们的整个项目的大部分就已经完成了,接下来就是一些细节的处理。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=dnw0imHfWR%2F8SnrrRWtgag%3D%3D.xegDhJY7z8u%2B25WQD%2BjYCpbCQR9%2BzBNDqB6mlSZOgkPu%2FX4xGqGC9Q2rEMmAsJDvHzTdykNOTeViSU3NmrxKEWf4%2BJr15eLM8ao8fqRt%2BFIZ31B0VqiEpzu128HUxSmXDOzRE2VaPYEvBdWNlh90oA%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><p>让我们继续,下一个组件的实现也是最难的,也就是回到顶部效果的实现。</p><h3>回到顶部按钮组件</h3><p>回到顶部组件的实现思路其实很简单,就是通过监听滚动事件确定回到顶部按钮的显隐状态,当点击回到顶部按钮的时候,我们需要通过定时器以一定增量来进行计算<code>scrollTop</code>,从而达到平滑回到顶部的效果。请看代码如下:</p><pre><code class="js">import React, { useEffect } from "react";
import ButtonComponent from "./buttonComponent";
import "../style/top.css";
const TopButtonComponent = React.forwardRef((props, ref) => {
const svgRef = React.createRef();
const setPathElementFill = (paths, color) => {
if (paths) {
Array.from(paths).forEach((path) => path.setAttribute("fill", color));
}
};
const onMouseEnterHandler = () => {
const svgPaths = svgRef?.current?.children;
if (svgPaths) {
setPathElementFill(svgPaths, "#2396ef");
}
};
const onMouseLeaveHandler = () => {
const svgPaths = svgRef?.current?.children;
if (svgPaths) {
setPathElementFill(svgPaths, "#ffffff");
}
};
const onTopHandler = () => {
props.onClick && props.onClick();
};
return (
<ButtonComponent
onClick={onTopHandler.bind(this)}
className="to-Top-btn btn-no-hover btn-no-active"
size="mini"
forwardedRef={ref}
>
{props.children ? ( props.children) : (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4158"
onMouseEnter={onMouseEnterHandler.bind(this)}
onMouseLeave={onMouseLeaveHandler.bind(this)}
ref={svgRef}
>
<path
d="M508.214279 842.84615l34.71157 0c0 0 134.952598-188.651614 134.952598-390.030088 0-201.376427-102.047164-339.759147-118.283963-357.387643-12.227486-13.254885-51.380204-33.038464-51.380204-33.038464s-37.809117 14.878872-51.379181 33.038464C443.247638 113.586988 338.550111 251.439636 338.550111 452.816063c0 201.378473 134.952598 390.030088 134.952598 390.030088L508.214279 842.84615zM457.26591 164.188456l50.948369 0 50.949392 0c9.344832 0 16.916275 7.522324 16.916275 16.966417 0 9.377578-7.688099 16.966417-16.916275 16.966417l-50.949392 0-50.948369 0c-9.344832 0-16.917298-7.556093-16.917298-16.966417C440.347588 171.776272 448.036711 164.188456 457.26591 164.188456zM440.347588 333.852624c0-37.47859 30.387078-67.865667 67.865667-67.865667s67.865667 30.387078 67.865667 67.865667-30.387078 67.865667-67.865667 67.865667S440.347588 371.331213 440.347588 333.852624z"
p-id="4159"
fill={props.color}
></path>
<path
d="M460.214055 859.812567c-1.87265 5.300726-2.90005 11.000542-2.90005 16.966417 0 12.623505 4.606925 24.189935 12.244882 33.103956l21.903869 37.510312c1.325182 8.052396 8.317433 14.216793 16.750499 14.216793 8.135284 0 14.929014-5.732561 16.585747-13.386892l0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-18.425651 9.29469-29.325909 0-5.965875-1.027399-11.665691-2.90005-16.966417L460.214055 859.81359z"
p-id="4160"
fill={props.color}
></path>
<path
d="M312.354496 646.604674c-18.358113 3.809769-28.697599 21.439288-23.246447 39.399335l54.610782 179.871647c3.114944 10.304693 10.918677 19.086707 20.529569 24.454972l8.036024-99.843986c1.193175-14.745842 11.432377-29.226648 24.737404-36.517705-16.502859-31.912827-34.381042-71.079872-49.375547-114.721835L312.354496 646.604674z"
p-id="4161"
fill={props.color}
></path>
<path
d="M711.644481 646.604674l-35.290761-7.356548c-14.994506 43.641963-32.889061 82.810031-49.374524 114.721835 13.304004 7.291057 23.544229 21.770839 24.737404 36.517705l8.036024 99.843986c9.609869-5.368264 17.397229-14.150278 20.529569-24.454972L734.890928 686.004009C740.34208 668.043962 730.003618 650.414443 711.644481 646.604674z"
p-id="4162"
fill={props.color}
></path>
</svg>
)}
</ButtonComponent>
);
}
);
const TopComponent = (props) => {
const btnRef = React.createRef();
let scrollElement= null;
let top_value = 0,timer = null;
const updateTop = () => {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1);
}
};
const topHandler = () => {
scrollElement = props.scrollElement?.current || document.body;
top_value = scrollElement.scrollTop;
updateTop();
props.onClick && props.onClick();
};
useEffect(() => {
const scrollElement = props.scrollElement?.current || document.body;
// listening the scroll event
scrollElement && scrollElement.addEventListener("scroll", (e: Event) => {
const { scrollTop } = e.target;
if (btnRef.current) {
btnRef.current.style.display = scrollTop > 50 ? "block" : "none";
}
});
});
return (<TopButtonComponent ref={btnRef} {...props} onClick={topHandler.bind(this)}></TopButtonComponent>);
};
export default TopComponent;
</code></pre><p>CSS样式代码如下:</p><pre><code class="css">.to-Top-btn {
position: fixed;
bottom: 15px;
right: 15px;
display: none;
transition: all .4s ease-in-out;
}
.to-Top-btn .icon {
width: 35px;
height: 35px;
}</code></pre><p>整个回到顶部按钮组件分为了两个部分,第一个部分我们是使用<code>svg</code>的图标作为回到顶部的点击按钮。首先是第一个组件<code>TopButtonComponent</code>,我们主要做了2个工作,第一个工作就是使用<code>React.forwardRef API</code>来将<code>ref</code>属性进行转发,或者说是将<code>ref</code>属性用于通信。关于这个<code>API</code>的详情可查看文档 <a href="https://link.segmentfault.com/?enc=dpdn6yV8Zi9IL%2BKli3Gmag%3D%3D.BVC%2BJnb3of%2F4TF5UFBUgPR9wDgJeAR4WITSLWlHQ5tpdlhhmdKSqdxwoVuRgassd%2BLI0FRK%2FIvp%2FlBWZcB5Y5Q%3D%3D" rel="nofollow">forwardRef API</a>。然后就是通过<code>ref</code>属性拿到svg标签下面的所有子元素,通过<code>setAttribute</code>方法来为<code>svg</code>标签添加悬浮改变字体色的功能。这就是以下这个函数的作用:</p><pre><code class="js">const setPathElementFill = (paths, color) => {
//将颜色值和path标签数组作为参数传入,然后设置fill属性值
if (paths) {
Array.from(paths).forEach((path) => path.setAttribute("fill", color));
}
};</code></pre><p>第二部分就是在钩子函数<code>useEffect</code>中去监听元素的滚动事件,从而确定回到顶部按钮的显隐状态。并且封装了一个更新<code>scrollTop</code>值的函数。</p><pre><code class="js">const updateTop = () => {
top_value -= 20;
scrollElement && (scrollElement.scrollTop = top_value);
if (top_value < 0) {
if (timer) clearTimeout(timer);
scrollElement && (scrollElement.scrollTop = 0);
btnRef.current && (btnRef.current.style.display = "none");
} else {
timer = setTimeout(updateTop, 1);
}
};</code></pre><p>采用定时器来递归实现动态更改<code>scrollTop</code>。其它也就没有什么好说的呢。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=iSjia%2FX4AN7OBNmlH4mmYA%3D%3D.RDf6n8cWcKThuHcQDp1BUlkckoPgNg137QrszKErweArZNgIpKPfuGE8%2FZ9BXCFsvXC7RJUvU7ivxH8as%2BnBaYdZAnvX59SeMujETHT91y7whb98otq11AUuByDjsBc5MtGkn%2Fk0xKHxKf8j37KB5w%3D%3D" rel="nofollow">此处</a>。让我们看下一个组件的实现。</p><h3>app组件的实现</h3><p>实际上该组件就是将所有封装的公共组件的一个拼凑。我们来看详情代码:</p><pre><code class="js">import React, { useReducer, useState } from "react";
import "../style/App.css";
import LangComponent from "../components/langComponent";
import TitleComponent from "../components/titleComponent";
import ContentComponent from "../components/contentComponent";
import ButtonComponent from "../components/buttonComponent";
import BottomComponent from "../components/bottomComponent";
import QuizWrapperComponent from "../components/quizWrapper";
import ParseComponent from "../components/parseComponent";
import RenderHTMLComponent from '../components/renderHTML';
import TopComponent from '../components/topComponent';
import { getCurrentQuestion, parseObject,questions,getCurrentAnswers,QuestionArray } from "../data/data";
import { LangContext, lang } from "../store/lang";
import { OrderReducer, initOrder } from "../store/count";
import { marked } from "../utils/marked";
import { computeSameAnswer } from "../utils/same";
let collectionUsersAnswers [] = [];
let collectionCorrectAnswers [] = questions.reduce((v,r) => {
v.push(r.correct);
return v;
},[]);
let correctNum = 0;
function App() {
const [langValue, setLangValue] = useState(lang);
const [usersAnswers,setUsersAnswers] = useState(collectionUsersAnswers);
const [correctTotal,setCorrectTotal] = useState(0);
const [orderState,orderDispatch] = useReducer(OrderReducer,0,initOrder);
const changeLangHandler = (index: number) => {
const value = index === 0 ? "en" : "zh";
setLangValue(value);
};
const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1 });
const endQuestionHandler = () => {
orderDispatch({ type:"reset",payload:0 });
correctNum = 0;
};
const onSelectHandler = (select:string) => {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
orderDispatch({ type:"reset",payload:25 });
}
if(select){
collectionUsersAnswers.push(select);
}
correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
setCorrectTotal(correctNum);
setUsersAnswers(collectionUsersAnswers);
}
const { count:order } = orderState;
const wrapperRef = React.createRef();
return (
<div className="App flex-center">
<LangContext.Provider value={langValue}>
<LangComponent lang={langValue} changeLang={changeLangHandler}></LangComponent>
{
order > 0 ? order <= 25 ?
(
<div className="flex-center flex-direction-column w-100p">
<QuizWrapperComponent
question={ questions[(order - 1 < 0 ? 0 : order - 1)] }
onSelect={ onSelectHandler }
>
</QuizWrapperComponent>
<BottomComponent lang={langValue}>{getCurrentQuestion(langValue, order)}</BottomComponent>
</div>
)
:
(
<div className="w-100p result-wrapper" ref={wrapperRef}>
<div className="flex-center flex-direction-column result-content">
<TitleComponent level={1}>{ getCurrentAnswers(langValue,correctTotal)}</TitleComponent>
<ParseComponent lang={langValue} userAnswers={ usersAnswers }></ParseComponent>
<RenderHTMLComponent template={marked(parseObject[langValue].endContent)}></RenderHTMLComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={endQuestionHandler}>
{parseObject[langValue].endBtn}
</ButtonComponent>
</div>
</div>
<TopComponent scrollElement={wrapperRef} color="#ffffff"></TopComponent>
</div>
)
:
(
<div className="flex-center flex-direction-column">
<TitleComponent level={1}>{parseObject[langValue].title}</TitleComponent>
<ContentComponent>{parseObject[langValue].startContent}</ContentComponent>
<div className="button-wrapper mt-10">
<ButtonComponent nativeType="button" long onClick={startQuestionHandler}>
{parseObject[langValue].startBtn}
</ButtonComponent>
</div>
</div>
)
}
</LangContext.Provider>
</div>
);
}
export default App;</code></pre><p>以上代码涉及到了一个工具函数,如下所示:</p><pre><code class="js">export function computeSameAnswer(correct = 0,userAnswer,correctAnswers,index) {
if(userAnswer === correctAnswers[index - 1] && correct <= 25){
correct++;
}
return correct;
}</code></pre><p>可以看到,这个函数的作用就是计算用户回答的正确数的。</p><p>另外,我们通过使用<code>context.provider</code>来将<code>lang</code>这个值传递给每一个组件,所以我们首先是需要创建一个<code>context</code>如下所示:</p><pre><code class="js">import { createContext } from "react";
export let lang = "en";
export const LangContext = createContext(lang);</code></pre><p>代码也非常简单,就是调用<code>React.createContext API</code>来创建一个上下文,更多关于这个<code>API</code>的描述可以查看<a href="https://link.segmentfault.com/?enc=qaeB90TLTiFTOBT4AahKpg%3D%3D.pU2%2BBoGfwR3XvsDtMPcoHIdqNN8mxhYpB7BoPkq%2BLSgc9BzB4JwtPIJH8ECPWoh5" rel="nofollow">文档</a>。</p><p>除此之外,我们还封装了一个<code>reducer</code>函数,如下所示:</p><pre><code class="js">export function initOrder(initialCount) {
return { count: initialCount };
}
export function OrderReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initOrder(action.payload ? action.payload : 0);
default:
throw new Error();
}
}</code></pre><p>这也是<code>react.js</code>的一种数据通信模式,状态与行为(或者说叫载荷),是的我们可以通过调用一个方法来修改数据。比如这一段代码就是这么使用的:</p><pre><code class="js">const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1 });
const endQuestionHandler = () => {
orderDispatch({ type:"reset",payload:0 });
correctNum = 0;
};
const onSelectHandler = (select:string) => {
// console.log(select)
orderDispatch({ type:"increment"});
if(orderState.count > 25){
orderDispatch({ type:"reset",payload:25 });
}
if(select){
collectionUsersAnswers.push(select);
}
correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
setCorrectTotal(correctNum);
setUsersAnswers(collectionUsersAnswers);
}</code></pre><p>然后就是我们通过一个状态值或者说是数据值<code>order</code>值从而决定页面是渲染哪一部分的页面。<code>order <= 0</code>的时候则是渲染首页,<code>order > 0 && order <= 25</code>的时候则是渲染问题选项页面,<code>order > 25</code>则是渲染解析页面。</p><p>以上的源码可以查看<a href="https://link.segmentfault.com/?enc=6MuX8OQTUaDHyLrbpMBZhw%3D%3D.Orw4%2B0UrwXnwVO7SkrncEb2GppHCqLqHk7z0zR1rBmKJsGxLvygXIWmWGVlYfIlZHxcQ92fDHdNfzbQZhv3PAlKk2vGFgUi4PueJl4yg6lVSOkAa%2FuHH9WLOpwDtp9mh" rel="nofollow">此处</a>。</p><p>关于这个网站,我用<code>vue3.X</code>也实现了一遍,感兴趣可以参考<a href="https://link.segmentfault.com/?enc=LCcmAKmBENaxFdj16weCBg%3D%3D.6jnhSOL%2BZGOrINyfKX269HJtfiaLh27qfvld55DPTaD0Us4APr4B%2BeRdE717FEkdRPBwPgQTv%2BPYN6hpqqbVhIHcLxViCnB64VUw9MROizk%3D" rel="nofollow">源码</a>。</p>
50天用JavaScript完成50个web项目,我学到了什么?
https://segmentfault.com/a/1190000040481518
2021-08-09T16:51:23+08:00
2021-08-09T16:51:23+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
166
<h2>1.Expanding Cards</h2><p>效果如图所示:</p><p><img src="/img/bVcT1ju" alt="" title=""></p><ul><li><a href="https://link.segmentfault.com/?enc=cWEl9YE6GLFe2jLbKlmNnA%3D%3D.HVf%2B6Nn%2F7V38FPpHVJ3VuVFNap4YpB4Eqcg0eWy5bM5rcM8SS%2F1hh7sn3VjdqgJS33D1hEtNEbdWZuhMZSZ3%2Bk8lvYo%2Bc4zGQaU%2B%2FXyhous%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=a1YUlw28WqeDXLiAlyPRUA%3D%3D.ruktDX%2B%2B2UFMVBivecLYy5qr%2FLD3Zp%2BeYgfpA0%2BvbClVPcWxBG0usk0WICGj27vzo4CS5SfSKo4NsaWfFb8sqg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:弹性盒子布局中的<code>flex</code>属性:让所有弹性盒模型对象的子元素都有相同的长度,且忽略它们内部的内容。</li><li>JavaScript:利用<code>[].filter.call()</code>方法可快速实现简单的选项卡切换。如上述示例源码:</li></ol><pre><code class="js">const panelItems = document.querySelectorAll(".container > .panel");
panelItems.forEach(item => {
item.addEventListener('click',() => {
[].filter.call(item.parentElement.children,el => el !== item).forEach(el => el.classList.remove('active'));
item.classList.add('active')
});
});
</code></pre><h2>2.Progress Steps</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481521" alt="2.gif" title="2.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=hAsm%2FUlXuheS6J9%2BTzb8yA%3D%3D.cJDhp63fSO3sw6IsxrBA18v8Ep7%2B7VOIw8kYILN6HMuJpg4gQW5XdOiEKvNXYQOiG8p2uoKUZrcRY6DHrtEsegHDqQNC91Gf0Boe7a62f54%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=sAMM0MmikCxF%2B%2B924Y%2FaRg%3D%3D.mP81fLDeH5wqVKEuMJ1Vp5pofduiI8qcjRZxT0fq3JSMdSjWLc8DWKW0gqjaNKNkvwcT0A294IK%2BZOu7GWcaaQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>变量的用法以及弹性盒子垂直水平居中外加伪元素的用法。</li><li>JavaScript:计算进度条的宽度,类名的操作。如上述示例部分源码如下:</li></ol><pre><code class="js">function handleClass(el){
let methods = {
addClass,
removeClass
};
function addClass(c){
el.classList.add(c);
return methods;
};
function removeClass(c){
el.classList.remove(c);
return methods;
}
return methods
}</code></pre><h2>3.Rotating Navigation Animation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481522" alt="3.gif" title="3.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=vU26WZUyqtWw%2Fb0Y5k0wGg%3D%3D.vu2Vf4IabcVTrUdjPUq3tziwg1bTCM3PleoXUjux%2Bhtw9Qd2ziI0jtyOZrD9GX3LMWips4R1mTcMs09A6eczxtDnLssXA2flTs8yeQMbzfk%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=K1OBSiG%2BlRfSmB8KJ%2FFw8w%3D%3D.erEbAk4Fw%2B%2FuTuZc1heKws%2FRzlDsFjKtZaHiDMq7LuczMSZH5kG0uBQcyZ2kRY89zV9DZJitPo9wJH5SpRcSuQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:CSS弹性盒子布局加<code>rotate</code>动画。</li><li>JavaScript:添加和移除类名的操作。如上述示例部分源码如下:</li></ol><pre><code class="js">const addClass = (el,className) => el.classList.add(className);
const removeClass = (el,className) => el.classList.remove(className);</code></pre><h2>4.hidden-search-widget</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481523" alt="4.gif" title="4.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=qfWYQawM%2FbMhmeAhlMbgNA%3D%3D.F96XGDoxKTOgB0b%2BkKLnD8swKbgGjlN%2BcgrdSO%2FOjvEfSLf%2BEw9gQ%2FEdPhgHgEmH3dZstWXSwf1yofFjy23s5clA0ZBr4XpRyaaumMsFQaU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=CsusUEOU%2FI5v%2Fdgi%2BZBNHg%3D%3D.JMemUWKex6oIJeGYEp3%2FCpZ8YIuP0bofNeN6JNP6OVNyahjWdm7oIAYqwems%2F%2B5tmHNu5GXQ%2B2l3A7ELM32urw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:CSS过渡动画 + 宽度的更改 + <code>input</code>的<code>placeholder</code>样式。</li><li>JavaScript:添加和移除类名的操作。如上述示例部分源码如下:</li></ol><pre><code class="css">.search.active > .input {
width: 240px;
}
.search.active > .search-btn {
transform: translateX(238px);
}
.search > .search-btn,.search > .input {
border: none;
width: 45px;
height: 45px;
outline: none;
transition: all .3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border-radius: 8px;
}</code></pre><pre><code class="js">searchBtn.addEventListener('click',() => {
searchContainer.classList.toggle('active');
searchInput.focus();
})</code></pre><h2>5.Blurry Loading</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481524" alt="5.gif" title="5.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=g%2BzpyiTmRavbNAiXrSTR%2Bw%3D%3D.sSMqtM4sv5vUzOU%2FM6Q3HS5dmMpthLomxFlDKl%2FRGDujvV6SpyHUxoa1gKpr5U5HNFlF0QmqzaeaRNqyf369%2BXWG%2FAGAEOOjRjULB%2BgiXuY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ANpSHhza%2BUSSmDiaXwktQg%3D%3D.5KoYf85boMVR56fYYyJsHZ5gRlkWi9IDvq882v0%2Fhzz0XW2C%2F6sRvQ366WfADt1ObmH6%2By52GeVEhvOToynPxQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS filter</code>属性的用法;。</li><li>JavaScript:定时器加动态设置样式。如上述示例部分源码如下:</li></ol><pre><code class="js">let load = 0;
let timer = null;
let blurryLoadingHandler = function(){
load++;
if(load > 99){
clearTimeout(timer)
}else{
timer = setTimeout(blurryLoadingHandler,20);
}
text.textContent = `页面加载${ load }%`;
text.style.opacity = scale(load,0,100,1,0);
bg.style.filter = `blur(${scale(load,0,100,20,0)}px)`;
}
blurryLoadingHandler();</code></pre><p>这里有一个非常重要的工具函数(后续有好几个示例都用到了这个工具函数),如下所示:</p><pre><code class="js">const scale = (n,inMin,inMax,outerMin,outerMax) => (n - inMin) * (outerMax - outerMin) / (inMax - inMin) + outerMin;</code></pre><p>这个工具函数的作用就是将一个范围数字映射到另一个数字范围。比方说,将<code>1 ~ 100</code>的数字范围映射到<code>0 ~ 1</code>之间。<br><a href="https://link.segmentfault.com/?enc=lk9NmEPVnlFmgrJ%2BwUYr5A%3D%3D.9juXCFIdHrhnoGh0QOhSwt4EabHPK9Cd5kqGoCLq%2BLaNhoIFCZqLEVxrkR3EoyoW2u5%2BAGiOGOvmxtTX03brh%2FZ6q%2B6dGpDw9hhNVDPdb1Cr7RjhzukAxqA4iEOKT%2FedWUNewFxQNnytOVGHRemciFaTjS65Wl2%2FtM4SSxyQaHU%3D" rel="nofollow">详情</a>。</p><h2>6.Scroll Animation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481525" alt="6.gif" title="6.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=8gK1qGEfZVjs14UsEot2XQ%3D%3D.9vxr1eF6Zf3T64QGRsPP9FTk%2F0PInu5F%2B8iEFhEsPnhq6znjfzfpL7tF%2FZIHsXrRfG%2BhrXTsh4RfEHPMjxUQyaTDOPou5ivqBa%2BB9emHH28%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=HjMMxL06U9ZfIHYMR6PH6Q%3D%3D.WZjLabhjgFU5fFjHIbZHNIkBH%2Ft5Flbt7fzBDcIS4IOqkncdWncpQn1GtV0tVeRavF%2BT0W%2FTwVh%2Bn3XRnwzvvg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>位移动画。</li><li>JavaScript:动态创建元素+元素滚动事件监听+视图可见区域的判断 + 防抖函数。如上述示例部分源码如下:</li></ol><pre><code class="js">function debounce(fn,time = 100){
let timer = null;
return function(){
if(timer)clearTimeout(timer);
timer = setTimeout(fn,time);
}
}
let triggerBottom = window.innerHeight / 5 * 4;
boxElements.forEach((box,index) => {
const top = box.getBoundingClientRect().top;
if(top <= triggerBottom){
box.classList.add('show');
}else{
box.classList.remove('show');
}
})</code></pre><h2>7. Split Landing Page</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481526" alt="7.gif" title="7.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=tu7wR3AYSmFNm7sOmZIC6g%3D%3D.A6m72KGLoI0vABZ1G68iBguQcOLh2329yQCC1i9RF8Wg%2F9uO97eUgKYkyls0GT0B2FdRERb9NQOgbPb80pTIi7EDqX65ZZzZY7PjnsxC33A%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=CmGltUfMjq%2BAQugfeImweg%3D%3D.Q5dBw%2BH%2BHgp8pkBrPcLOiGz2pAAVAV9O3FZMDoO21f1RVnkRcXanY6nGjlYbF3d%2BJlRmAYOl8geaM6BhLNBeRg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>过渡特效 + 弹性盒子垂直水平居中 + <code>CSS</code>定位 + 宽度的更改。</li><li>JavaScript:鼠标悬浮事件 + 类名的操作。如上述示例部分源码如下:</li></ol><pre><code class="js">HTMLElement.prototype.addClass = function(className) {
this.classList.add(className);
};
HTMLElement.prototype.removeClass = function(className){
this.classList.remove(className);
}
const on = (el,type,handler,useCapture = false) => {
if(el && type && handler) {
el.addEventListener(type,handler,useCapture);
}
}
on(leftSplit,'mouseenter',() => container.addClass('hover-left'));
on(leftSplit,'mouseleave',() => container.removeClass('hover-left'));
on(rightSplit,'mouseenter',() => container.addClass('hover-right'));
on(rightSplit,'mouseleave',() => container.removeClass('hover-right'));</code></pre><p>从这个示例,我也知道了<code>mouseenter、mouseleave</code>和<code>mouseover、mouseout</code>的区别,总结如下:</p><blockquote><ol><li>enter只触发1次,只有等到鼠标离开了目标元素之后再进入才会继续触发,同理leave也是如此理解。而over与out就是不断的触发。</li><li>enter进入子元素,通过e.target也无法区分是移入/移出子元素还是父元素,而over与out则可以区分。</li></ol></blockquote><h2>8.Form Wave</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481527" alt="8.gif" title="8.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=mL8raUu%2FOGjrVz29fcJSvw%3D%3D.gGhIBQ8klP32pR5mCUmoNmSRqvYxlJIPka9KzACf%2Fo6%2B9%2FwvLeZNb19JiZuYuRsg5AaFuV%2F3wYB%2FrGBOsBgJNr9Lh5qTeQwjbwo9IbOqk0M%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ATX%2BQhs%2Bcg3Or2WJf%2Fxbkg%3D%3D.BsLOJutiFit6p0g9z0%2BOOBZHYB6GqPbFJbJjeRLL6Od0JfWrRxGigWlI1x1M63KBH34gGTqqOsA%2BRzqh2vjDHg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>渐变 + 弹性盒子垂直水平居中 + <code>CSS</code>过渡动画 + <code>CSS</code>位移变换 + 关注焦点伪类选择器与同级元素选择器的用法。</li><li>JavaScript:字符串替换成标签 + 动态创建元素。如上述示例部分源码如下:</li></ol><pre><code class="js">const createLetter = v => v.split("").map((letter,idx) => `<span style="transition-delay:${ idx * 50 }ms">${ letter }</span>`).join("");</code></pre><h2>9.Sound Board</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481528" alt="9.gif" title="9.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=0n0%2Fm%2BqP3s%2FoAROg%2F4jQ0g%3D%3D.41sTr6%2BhOTND%2FxILdiAx1RfDV1609Ul3ScFBILTGk28I1vsv7Z7797RRQrNf3rvconhUhcxWp%2B7SkKEoK8lxA64J5qNe47D%2BibNdjehztCo%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=grGjQtDKlqDD6PF8FFYl9A%3D%3D.eJcnDOapakOZJTg%2F1hp%2FldFHnLLGbm%2FROSHxxesvkwZY33yOWAgE7rm8tqXQ6XE3%2FYzWztDxIbhlv1ZKwztbTQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>渐变 + 弹性盒子垂直水平居中 + 基本样式。</li><li>JavaScript:动态创建元素 + 播放音频(<code>audio</code>标签)。如上述示例部分源码如下:</li></ol><pre><code class="js">sounds.forEach(sound => {
const btn = create('button');
btn.textContent = sound;
btn.type = "button";
const audio = create('audio');
audio.src = "./audio/" + sound + '.mp3';
audio.id = sound;
btn.addEventListener('click',() => {
stopSounds();
$('#' + sound).play();
});
buttonContainer.appendChild(btn);
buttonContainer.insertAdjacentElement('beforebegin',audio);
});</code></pre><h2>10. Dad Jokes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481529" alt="10.gif" title="10.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=NHVQqj5ICijBzWBKg242eA%3D%3D.zq3aazzf8EVNYu9XxWcO30WefpqAVb42zb7AbLzpSGdFQdlT2wu1UOXrIx6un7x%2FZGU99YvuLvnlpRgeyaV8feRakkfPpHMOeyZChlrc9dw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=eDnkoiP7uhILMnFHQbrDUg%3D%3D.6vl%2Bv2ktyCk37OGQAwhxIA6NdKIMlf71Bmb9qMCa6o1%2FAD419RopvdskjHZBr7FrKIpygPuz84ntENPPcP5%2F%2Bw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>渐变 + 弹性盒子垂直水平居中 + 基本样式。</li><li>JavaScript:事件监听 + <code>fetch API</code>请求接口。如上述示例部分源码如下:</li></ol><pre><code class="js">const api = 'https://icanhazdadjoke.com';
const on = (el,type,handler,useCapture = false) => {
if(el && type && handler){
el.addEventListener(type,handler,useCapture);
}
}
on(getJokeBtn,'click',request);
request();
async function request(){
const res = await fetch(api,headerConfig);
const data = await res.json();
content.innerHTML = data.joke;
}</code></pre><h2>11. Event Keycodes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481530" alt="11.gif" title="11.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=84oakq%2FVSZJ0SOnvQYQRbQ%3D%3D.oqfyigkh0H1gI%2Fkkkn1NBV6W0G4tKn2j2oPK6sTEaSeaqwyrDVYcLsm6QM2xAKbdQrprcKtopEW23lfbltMTpthKhXLKjr28IJ%2FjECfSBlY%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=VnkjqD6tYMcEscxj0ojrxg%3D%3D.x4gFD%2BAW89TEhC1VokIpp8%2BVxM4AuVsTl%2BnjyM093%2Fe9mLSkkNCZ20HluSinuY3vu4kjqL1blEbW9iH5bpVF1A%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>渐变 + 弹性盒子垂直水平居中 + 基本样式。</li><li>JavaScript:键盘事件监听 + 事件对象的属性。如上述示例部分源码如下:</li></ol><pre><code class="js">const container = document.querySelector('#container');
window.addEventListener("keydown",event => {
createKeyCodeTemplate(event);
});
function createKeyCodeTemplate(e){
const { key,keyCode,code } = e;
let template = "";
[
{
title:"event.key",
content:key === " " ? "Space" : key
},
{
title:"event.keyCode",
content:keyCode
},
{
title:"event.code",
content:code
}
].forEach(value => {
template += `<div class="key"><small>${ value.title }</small>${ value.content }</div>`;
});
container.innerHTML = template;
}</code></pre><h2>12. Faq Collapse</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481531" alt="12.gif" title="12.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=eHwFU85twP6uZDuhnYjrFg%3D%3D.8De9xex4b2BfcxllIOynIcfW%2Bs7pMrlEJF%2BkHpGrj%2By3TD2Z4WuOYoTktR%2BsHn39AxCCYG%2FWHdfbjbZPmjrwOqTnTtvBRdNg9SSukQS82dg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=JvlsCvQoviIFJL5Bva2iVw%3D%3D.O0H3fNevqjS%2B%2BGMxtNtUudrMjPYtpX%2FomNPLlA9dZxEQeltIWNc%2FEK9bEqRUG46Vg2MRBjdnK%2BO49JzKt%2FXcgw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>font-awesome</code>字体库的使用 + 伪元素选择器 + 定位 + <code>CSS</code>渐变 + 基本样式。</li><li>JavaScript:动态创建元素 + 类名的切换 + 事件代理。如上述示例部分源码如下:</li></ol><pre><code class="js">const faqs = document.querySelectorAll('.faq-container > .faq');
container.addEventListener('click',e => {
if(e.target.className.indexOf('faq-icon') > -1){
faqs[[].indexOf.call(faqs,e.target.parentElement)].classList.toggle('active');
}
});</code></pre><h2>13. Random Choice Picker</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481532" alt="13.gif" title="13.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=KLs4GKJlOjkGBjFW8WrPJw%3D%3D.ZdQyxBfpCHXRuBN8IS5%2Bp0mq9miPuM6kQQk4Jcb10E%2BzMduVpmyE%2FWJ7dZ6NNrFvb0iYz1q09XHUfrs1sW60VVEWdFa3pZd0AKquObgtTtE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=1q7WZK2w6mQK15p1%2FnJnDw%3D%3D.YoQebmcCmkwKGcfQ8%2Fq%2BB5HIomoVUN0rsxI0gfm%2FDy1X3fjIM%2FP%2B4zGsAZ%2Bqi2bB4mx9IyokCFsUQjp5Wx9Wqg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式。</li><li>JavaScript:动态创建元素 + 类名的切换 + 延迟定时器的用法 + 随机数 + 键盘事件的监听。如上述示例部分源码如下:</li></ol><pre><code class="js">function createTags(value,splitSymbol){
if(!value || !value.length)return;
const words = value.split(splitSymbol).map(v => v.trim()).filter(v => v);
tags.innerHTML = '';
words.forEach(word => {
const tag = document.createElement('span');
tag.classList.add('tag');
tag.innerText = word;
tags.appendChild(tag);
})
}
function randomTag(){
const time = 50;
let timer = null;
let randomHighLight = () => {
const randomTagElement = pickerRandomTag();
highLightTag(randomTagElement);
timer = setTimeout(randomHighLight,100);
setTimeout(() => {
unHighLightTag(randomTagElement);
},100);
}
randomHighLight();
setTimeout(() => {
clearTimeout(timer);
setTimeout(() => {
const randomTagElement = pickerRandomTag();
highLightTag(randomTagElement);
},100);
},time * 100);
}
function pickerRandomTag(){
const tagElements = document.querySelectorAll('#tags .tag');
return tagElements[Math.floor(Math.random() * tagElements.length)];
}</code></pre><h2>14. Animated Navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481533" alt="14.gif" title="14.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=541lGBACHMpw7e%2BNvdjzjA%3D%3D.%2FGf33Kx517nMc47mqWjowXI3aBcrbSxjHbr1SoNUFhELGmRrLJKt2IngMaQpwgRT22uPN0XwhiYvZzmY%2BbO8g79H27y7yuz96SjoU6sButs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=4qFL9kglVcbAIpBElU1xZA%3D%3D.Yp3ATI6Q6lztiD%2BDRjiGD4MGsBZGlcCf9duCmlFJ0WsLrM%2FTbGf3k%2BePa8XeE1RZffOs%2F%2BDixhNW1m91wmHYdg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + 定位 + 位移变换以及角度旋转。</li><li>JavaScript:类名的切换。如上述示例部分源码如下:</li></ol><pre><code class="js">toggle.addEventListener('click',() => nav.classList.toggle('active'));</code></pre><h2>15. Incrementing Counter</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481534" alt="15.gif" title="15.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=BY4O8tejCMS2CGWgqZEbuQ%3D%3D.rVhSlEB9qLlTn%2B4O3Fqr8o2baSmJxQkVF08YMVpimGcGysEay9sf%2BHkQLf9TuWSdWBHWAjCkzulrwErwPtP75GePqL78NTRp4rWnlC3AlQ0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=u0oRUdoQJcQ3xS%2BP6qIqzA%3D%3D.sOxcLRX%2FKp4YBy0VILFUj7ra1HKSXpDTOO31le%2FQ%2B1i6sARlCRZH2MyTKQRJQ4fikXcbzZZ5XSn9SWbVTySsHg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>font-awesome</code>字体库的使用。</li><li>JavaScript:动态创建元素 + 定时器实现增量相加。如上述示例部分源码如下:</li></ol><pre><code class="js">function updateCounterHandler() {
const counters_elements = document.querySelectorAll('.counter');
counters_elements.forEach(element => {
element.textContent = '0';
const updateCounter = () => {
const value = +element.getAttribute('data-count');
const textContent = +element.textContent;
const increment = value / 100;
if (textContent < value) {
element.textContent = `${Math.ceil(increment + textContent)}`;
setTimeout(updateCounter, 10);
} else {
element.textContent = value;
}
}
updateCounter();
});
}</code></pre><h2>16. Drink Water</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481535" alt="16.gif" title="16.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=t1MVzh8vOf3qJd74hY1biQ%3D%3D.kxlFPsWhUUKhl3UcU4TqcNmxNlfjXOof8Y2WK0X9CF5coWL9QSbyPV8kzBzyitCQB1arHn%2BL0O4vF2H9Mg65luAtGVJzafUbw1xAR%2BigXOg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=RlqHL0K7p%2FXfB9Fw3UktCQ%3D%3D.ss09Uj9Qxy%2BrTJfbcskxZaOXMqQLaFSgmYCbMTb8pjxMa4opWc0Y9%2B82YZO%2Fyoxfgc4GCnO4It1aROgNil3wWg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>CSS</code>过渡特效。</li><li>JavaScript:正则表达式 + 循环 + 样式与内容的设置。如上述示例部分源码如下:</li></ol><pre><code class="js">if (actives.length === l) {
setHeightVisible('0', 'hidden', '350px', 'visible');
setTextContent("100%", "0L");
} else if (actives.length === 0) {
setHeightVisible('350px', 'visible', '0', 'hidden');
setTextContent("12.5%", "2L");
} else {
const h1 = unitHei * (l - actives.length) + 'px';
const h2 = unitHei * actives.length + 'px';
setHeightVisible(h1, 'visible', h2, 'visible');
const t1 = (unitHei * actives.length / 350) * 100 + "%";
const t2 = (cups[i].textContent.replace(/ml/, "").replace(/\s+/, "") - 0) * (l - actives.length) / 1000 + 'L';
setTextContent(t1, t2);
}</code></pre><h2>17. movie-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481536" alt="17.gif" title="17.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=ONtIVYpLcIpuSI1XkGKKww%3D%3D.cLAXZokF4bPH6MSzCdtAM6c7Ciw%2F%2B7b2mnmE%2FtyAhljWFuB4tMoXbthrtrXwtneOL%2FQ5k9mH9GdRZfQpbJPcji8gL%2Bthy5mUthV04zxtzOE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=AwdnNKnSpkx6dWgkxM%2FpqQ%3D%3D.zMXWoA7ZLTD56E5F0BIiCmupL730hYF6FybuaA8Oxo2iHzsM2WdKTv2XGadiNg9jAOwoBjssAEhlcQdIHNGmuA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>CSS</code>过渡特效 + 清除浮动。</li><li>JavaScript:接口请求 + 键盘事件的监听。如上述示例部分源码如下:</li></ol><pre><code class="js">search.addEventListener('keydown',e => {
if(e.keyCode === 13){
let value = e.target.value.replace(/\s+/,"");
if(value){
getMovies(SEARCH_API + value);
setTimeout(() => {
e.target.value = "";
},1000);
}else{
window.location.reload(true);
}
}
})</code></pre><blockquote>PS:这个示例效果由于接口访问受限,需要xxx(你懂的)访问。</blockquote><h2>18. background-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481537" alt="18.gif" title="18.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=hsJ6ww%2BwIfeWoGNvfWSfjw%3D%3D.xJKYPH6BgI7XIUG2m0kkwr%2F2covj7BQ%2B7EtKvNudi%2BPeEFVoIjB9whtIPiRJJiXVDS8lfRDhpMl9ysCGHrrAufXZTVJcHEXV5MajeA6cCRQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=k3YCiG7wzYSV3CMILklKRw%3D%3D.bbJvMbmF1VWBnAEvqwkgVcrdi0w55kbw1pPN%2BXh66TwA7oZDfQa1U4V2UD22%2B%2BMgwPgueMtwjvjQJ1ppAr0g9g%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + 阴影效果 + 定位 + 伪元素选择器。</li><li>JavaScript:背景设置与类名的操作。如上述示例部分源码如下:</li></ol><pre><code class="js">let currentActive = 0;
function setBackgroundImage(){
const url = slideItems[currentActive].style.backgroundImage;
background.style.backgroundImage = url;
}
function setSlideItem(){
const currentSlide = slideItems[currentActive];
const siblings = [].filter.call(slideItems,slide => slide !== currentSlide);
currentSlide.classList.add('active');
siblings.forEach(slide => slide.classList.remove('active'));
}</code></pre><h2>19. theme-clock</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481539" alt="19.gif" title="19.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=5FKiKcceUOP3H2YD1v%2BKBQ%3D%3D.5DaiuUwuTs%2Bcr6gi2Wxa7KhbeLNACju1DqUuBka0pEIm3IXCElJQGttQCjpGpnwKtYpYEda3WXHY5tPxTQSbHPWbfFBtwmfhBnvqtOcnFNI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=HBEhlif6AIPU%2BoYUT4nsAg%3D%3D.h146ZVUbP5kQfMQ162yv8GQoX0Q00Yt148s22Urc79hDZNep2Wu0yF5wN0yZjd62KYVH8o%2Bn79Dgzth0JNeYbw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>变量 + 阴影效果 + 定位。</li><li>JavaScript:中英文的切换以及主题模式的切换,还有日期对象的处理。如上述示例部分源码如下:</li></ol><pre><code class="js">function setCurrentDate(){
const date = new Date();
const month = date.getMonth();
const day = date.getDay();
const time = date.getDate();
const hour = date.getHours();
const hourForClock = hour % 12;
const minute = date.getMinutes();
const second = date.getSeconds();
const amPm = hour >= 12 ? langText[currentLang]['time-after-text'] : langText[currentLang]['time-before-text'];
const w = currentLang === 'zh' ? dayZHs : days;
const m = currentLang === 'zh' ? monthZHs : months;
const values = [
`translate(-50%,-100%) rotate(${ scale(hourForClock,0,11,0,360) }deg)`,
`translate(-50%,-100%) rotate(${ scale(minute,0,59,0,360) }deg)`,
`translate(-50%,-100%) rotate(${ scale(second,0,59,0,360) }deg)`
];
[hourEl,minuteEl,secondEl].forEach((item,index) => setTransForm(item,values[index]));
timeEl.innerHTML = `${ hour }:${ minute >= 10 ? minute : '0' + minute }&nbsp;${ amPm }`;
dateEl.innerHTML = `${w[day]},${ m[month]}<span class="circle">${ time }</span>${ langText[currentLang]['date-day-text'] }`;
timer = setTimeout(setCurrentDate,1000);
}</code></pre><blockquote>PS:本示例也用到了与示例5一样的工具函数<code>scale</code>函数</blockquote><h2>20. button-ripple-effect</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481540" alt="20.gif" title="20.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=cm%2F3IPVXFmJ2qMFh49%2FmXQ%3D%3D.%2FdZ7NumNWfVmxZgtuAjAaZe6uWNTg0MmSH6g2nXkCeKcpix%2BgTybkM5eTI999fTGwLncOaBp0psxsuvImJZwlHAqjDvbjBbhec6lkDsYv74%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=WF4INZqpeiDcXgL10Rldcw%3D%3D.IdfoG%2FPg0zYTNaFEcLIiz8bn%2BRV7hjCJDcilLQSUIR7C3EBwun6f%2F1ey4PHyOwvYTY5W45sbMhksYWv4JC1%2FCg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>CSS</code>动画。</li><li>JavaScript:坐标的计算 + 偏移 + 定时器。如上述示例部分源码如下:</li></ol><pre><code class="js">const x = e.clientX;
const y = e.clientY;
const left = this.offsetLeft;
const top = this.offsetTop;
const circleX = x - left;
const circleY = y - top;
const span = document.createElement('span');
span.classList.add('circle');
span.style.left = circleX + 'px';
span.style.top = circleY + 'px';
this.appendChild(span);
setTimeout(() => span.remove(),1000);</code></pre><h2>21. drawing-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481541" alt="21.gif" title="21.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=PGQrp7ISQiwL%2F0L1MOfidA%3D%3D.aRiS3oj9rtzyTtGoFw%2FsDH08udn6dInTkCILS3ucDF%2Flop%2FDKuXwnlJYEavCgiDqmyjnSaUvnNrN89kWuN9k9i%2Fvg8upsTfZ6Se6sbPw8Uw%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=7FPwVQMj6cq%2FubKyLOg7Gg%3D%3D.UUxsd7sJc%2FkQ5yhV95HWL4906sAr%2B89oUCAiw%2Bm3j6qY1%2FJMwud3A4SY3LSq1vtjURczvRlYC8Yb6meMcgmqrA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式。</li><li>JavaScript:<code>canvas API</code> + <code>mouse</code>事件以及计算<code>x</code>与<code>y</code>坐标 + <code>ewColorPicker</code>的用法。如上述示例部分源码如下:</li></ol><pre><code class="js">function mouseDownHandler(e){
isPressed = true;
x = e.offsetX;
y = e.offsetY;
}
function throttle(fn,wait = 100){
let done = false;
return (...args) => {
if(!done){
fn.call(this,args);
done = true;
}
setTimeout(() => {
done = false;
},wait);
}
}</code></pre><h2>22. drag-n-drop</h2><p>效果如图所示:</p><p><img src="/img/bVcT1sq" alt="" title=""></p><ul><li><a href="https://link.segmentfault.com/?enc=b25EsiUNmejcgmhimTpWkg%3D%3D.hJFMEnJ2rFTJN%2BHFC4rJoJ6eGAqYbWrFYS31Qe5X2v00ckKO94JSPCwzDYmV%2BRYnn5qWbYD5hzY3qAtcsvoi2gEJ1lsx7klaA7gOzj6b2fo%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=l6YMW61ztB%2FmFrhl6wkazg%3D%3D.X4pZ2wTnXSYasONB7Oao%2FzJBYFXcr6VJz9wOF%2F%2BmLvD996abkwPf9PUAy6h4rD9CtFZyyo1ecDxqIKzFuKZR7Q%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>CSS</code>弹性盒子布局。</li><li>JavaScript:<code>drag event API</code> + 随机生成图片。如上述示例部分源码如下:</li></ol><pre><code class="js">function onDragStart(){
this.classList.add('drag-move');
setTimeout(() => this.className = "invisible",200);
}
function onDragEnd(){
this.classList.add("drag-fill");
}
function onDragOver(e){
e.preventDefault();
}
function onDragEnter(e){
e.preventDefault();
this.classList.add('drag-active');
}
function onDragLeave(){
this.className = "drag-item";
}
function onDrop(){
this.className = "drag-item";
this.appendChild(dragFill);
}</code></pre><h2>23. content-placholder</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481543" alt="23.gif" title="23.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=6mE587KfRYWs%2FdxHFaUzqg%3D%3D.8JM%2FysDU1l4wLe1v5ZUp8N0hdEgvxPQ%2B2iCXAC7PpUm%2BN1LJJJqvW5T%2BgQcND0hEO6odN0vOK0mMRP4nQ7xbVvhwuPdbYClkaLn9dP9tdls%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=2MAh%2BB1vlZHd%2BDDpXlPbEQ%3D%3D.belEx0XK23U94w3oVoC2InRQg53m7R%2FkpsNLxNXrNmK5ML5Ai2H5GSYG%2F4SLTnNQk7Hup3OvO%2Bn5fyT%2FZo6uEQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + <code>CSS</code>卡片样式。</li><li>JavaScript:骨架屏加载效果。如上述示例部分源码如下:</li></ol><pre><code class="js">animated_bgs.forEach(bg => bg.classList.remove("animated-bg"));
animated_bgs_texts.forEach(text => text.classList.remove("animated-bg-text"));</code></pre><h2>24. sticky-navbar</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040482403" alt="24.gif" title="24.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=vT3Ywqw5z3%2Brc7GSQHgokQ%3D%3D.m5V0w3nf%2FqQk5YhhFFHxmJokF9PtOiKo28T1VFNesHZX8Qr%2BADW5PyQmSWAzOOe3OwT1sIrCyTgK%2BAeEExTU%2BGrwJ%2FpEFkyfuzfDeM82%2Bv4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=hLhUvjPkcFqe3jDFmzKkDg%3D%3D.GsKJYWxdJh8l4BwM9gxtCM7YHvYehHYNMqoMgYDgOigonIGcnYvJtINXD2JnO%2B%2B0Ag685%2BgMm3EzRzJ%2FLGZ0cw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + 固定定位导航。</li><li>JavaScript:滚动事件。如上述示例部分源码如下:</li></ol><pre><code class="js">window.addEventListener("scroll",e => {
if(window.scrollY > nav.offsetHeight + 100) {
nav.classList.add("active");
}else{
nav.classList.remove("active");
}
})</code></pre><blockquote>PS:这里也做了移动端的布局。</blockquote><h2>25. double-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481545" alt="25.gif" title="25.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=9v3lUBIfHzepk365xNfsZw%3D%3D.p2nKjK%2BGZnMQoRIfgpi8Ji1xq6VowZJE14VGku%2BBwYzCaSa2WGIFnF%2BflOWqgIqASapj40Ua9frWJxNkFBw0VJkRg6vAgVtnROlqv2uDl2s%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=I2qjq0TeS2R0L1xgnUq3EA%3D%3D.0Iv6VghAbCrpP5%2FL1Kpq%2FrnvOyFGFBLMwWPxIdcZHRutfa4UxswLrfbexnyKFf1qYbNSlCXIH4VpbPgvjWg1bQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变 + 位移变换。</li><li>JavaScript:轮播图的实现思路,主要还是利用<code>transformY</code>移动端使用<code>transformX</code>。如上述示例部分源码如下:</li></ol><pre><code class="js">function setTransform(){
let translate = isHorizontal() ? "translateX" : "translateY";
leftSlide.style.transform = `${ translate }(${position * currentIndex}px)`;
rightSlide.style.transform = `${ translate }(${-(position * currentIndex)}px)`;
}</code></pre><h2>26. toast-notification</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481546" alt="26.gif" title="26.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=yaVAlICKnvwUNo18sGPK%2BA%3D%3D.gOShyqTAN4t8JGOzWv8ewcgJteDz5RY7NAwlSumQSWnkg5438inhKOZrylO3m5Gy0nAQrTH87kw4qpNEAxwtgj20L0wvLwpYVHq4VToX9dM%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=t1O%2BR1%2FP%2BdAx%2FHNU8lXV%2Fg%3D%3D.osfEXDuLL7%2FWM0xriTwFw%2B%2FbZ2uyio10mq%2BtRmZlLZXd8vFAXa6PXzYtAYMfDrmLH19ZBJBuFAIpnwAVXxqKQg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + 消息提示框的基本样式。</li><li>JavaScript:封装一个随机创建消息提示框。如上述示例部分源码如下:</li></ol><pre><code class="js">function createNotification({message = null,type = null,auto = false,autoTime = 1000,left = 0,top = 0}){
const toastItem = createEle("div");
let closeItem = null;
if(!auto){
closeItem = createEle("span");
closeItem.innerHTML = "&times;";
closeItem.className = "toast-close-btn";
}
toastItem.className = `toast toast-${type}`;
toastItem.textContent = message;
if(closeItem)toastItem.appendChild(closeItem);
container.appendChild(toastItem);
const leftValue = (left - toastItem.clientWidth) <= 0 ? 0 : left - toastItem.clientWidth - 30;
const topValue = (top - toastItem.clientHeight) <= 0 ? 0 : top - toastItem.clientHeight - 30;
toastItem.style.left = leftValue + 'px';
toastItem.style.top = topValue + 'px';
if(auto){
setTimeout(() => {
toastItem.remove();
},autoTime);
}else{
closeItem.addEventListener("click",() => {
toastItem.remove();
});
}
}</code></pre><p>消息提示框实现思路可以参考<a href="https://link.segmentfault.com/?enc=GUoh7ZqgpNKPNja0ou713w%3D%3D.T7eh%2BeNee2p7RMoedKYsZEmWQMgsrps5hMl1jgRWmW45KkOI2hkno61oB5ORMSod" rel="nofollow">这篇文章</a>。</p><h2>27. github-profiles</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481547" alt="27.gif" title="27.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=yYyzhSzTOWgreM1YAtM%2Fxw%3D%3D.EQqpXrXIRuadJQcrNS6gS3xZlEm%2Fjfp9GI2yR%2BdTqDS2h05DiBpCBhDksPtvJEf8lfLswQkZNkgMLjZyftOVC92BWdoFiamh6Yf%2BrwE3FDE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=LJo6j1mgPNqk5rkG8jBZ%2BA%3D%3D.HOfpHPCvgOLL1ZPdZfP%2BkrN%2FYLAif%2BWVsgQPYNFfeHRvcLCW1ShmL%2BH6iWzQr9WfxDNLY8SSlrCyXTbTaKm1UA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式。</li><li>JavaScript:<code>try-catch</code>处理异常语法 + <code>axios API</code>请求<code>github API</code> + <code>async-await</code>语法。如上述示例部分源码如下:</li></ol><pre><code class="js">async function getRepoList(username){
try {
const { data } = await axios(githubAPI + username + '/repos?sort=created');
addRepoList(data);
} catch (error) {
if(error.response.status == 404){
createErrorCard("查找仓库出错!");
}
}
}</code></pre><h2>28. double-click-heart</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481548" alt="28.gif" title="28.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=tlkfBnIeXaXvB3dxcRf%2FUw%3D%3D.nbTd6sfR7htjM9MxKsKBJ62uZR81g%2F4qsMm%2BwuSYxp7KI%2BPFXAwICERqSjn7Qkj41BXncNjRSXMo8koIHYBE90EknFr1t1ZXUBQCj62d2N4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=q%2Bh2CZWcgxkKLqenmbnerQ%3D%3D.ByU2%2BBq7BPHQrlHfgpFAETOKmgaLgOMd6w3Fe%2Fxl8Boni6lByMpetpleiFFCDAiYHya5ldoDHSTMsDQ%2FNfd%2FZA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>画爱心。</li><li>JavaScript:事件次数的计算。如上述示例部分源码如下:</li></ol><pre><code class="js">function clickHandler(e){
if(clickTime === 0){
clickTime = Date.now();
}else{
if(Date.now() - clickTime < 400){
createHeart(e);
clickTime = 0;
}else{
clickTime = Date.now();
}
}
}</code></pre><h2>29. auto-text-effect</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481549" alt="29.gif" title="29.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=%2B3KCJlLm3v7p8mOB%2BVyznw%3D%3D.BNpW16WGXRg9%2BhN4DDpffym0sXkpScnyCtaHVDJTdDNT%2BMPlr0v6CeiqU7GSz%2BJfJjAt6j0sqFCXxhyR7Uvxrkmko6MwLFNP88jjP5kBA0c%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=7CHRP7BVMrNwyjJw8NAZLA%3D%3D.tGLwAtDjkkZk%2B%2B5Rw7K403h6LGfEZsW59JsoWPNpirh5kbGAsntPj3wpe%2BJu1csXRbY%2BeEUOO6a3twG%2FrtOdMA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式。</li><li>JavaScript:定时器 + 定时器时间的计算。如上述示例部分源码如下:</li></ol><pre><code class="js">let time = 300 / speed.value;
writeText();
function writeText(){
text.innerHTML = string.slice(0,idx);
idx++;
if(idx > string.length){
idx = 1;
}
setTimeout(writeText,time);
}
speed.addEventListener("input",e => time = 300 / e.target.value);</code></pre><h2>30. password generator</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481550" alt="30.gif" title="30.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=Z4GN0A8izLjyjoSM5kM9gA%3D%3D.sgcM9b2joh1kSC74KpRBvU6R8OHxutAVG0wb9uEtL9qcDqPz3GE297hYa1lpEEXgVJH461YwIZMCPNn47eDYkzAHcf41mKMWSYl0IJhKVk0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=jfsiOwaDkaNIsDCaff9Qng%3D%3D.D2V6BuqWGy06AuYsm3wFPXMARNqMTO7ns2vsRsLhiFLZ5AS3Iy%2FngHV%2F%2BPNxmrjCfUARIAm7WsJlc9AjCj0yvw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局。</li><li>JavaScript:中英文切换 + 选择框事件监听 + 随机数 + <code>copy API</code>。如上述示例部分源码如下:</li></ol><pre><code class="js">function getRandomLower(){
// a ~ z 的code为 97 ~ 122
// 可根据charCodeAt()方法获取
return String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
function getRandomUpper(){
// A ~ Z 的code为 65 ~ 90
// 可根据charCodeAt()方法获取
return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}
function getRandomNumber(){
// 0 ~ 9的code为48 ~ 57
// 可根据charCodeAt()方法获取
return String.fromCharCode(Math.floor(Math.random() * 10) + 48);
}</code></pre><h2>31. good-cheap-fast</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481551" alt="31.gif" title="31.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=w4KFnkjbmddYBbrS%2FndFiA%3D%3D.TMd2N7rjqjzyjREBIKGe%2FN%2BIdX5DxJOiJyZI%2F7D3UdgvpAOQtvDfuDyHY6IkWsGPeHC%2FI9%2F0IE7MnwjdMf9w%2BQpVJbPqs7vuX8qzb1PMup8%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=fIBCSg6Dm46V4L4OUW%2F02Q%3D%3D.IXCtQ9uct3LfovGBpGS4BsKYcq8ehtZuIKxXjMi9fh%2Bdkxnzsth9F%2FuScJgsy%2Fkf3LcFfjfIM5tH0XApGwJlkg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>实现开关小组件样式 + 基本布局样式。</li><li>JavaScript:<code>input</code>的<code>change</code>事件的监听。如上述示例部分源码如下:</li></ol><pre><code class="js">const checkBoxElements = document.querySelectorAll(".switch-container input[type='checkbox']");
checkBoxElements.forEach(item => item.addEventListener("change",e => {
const index = Array.from(checkBoxElements).indexOf(e.target);
if(Array.from(checkBoxElements).every(v => v.checked)){
if(index === 0){
checkBoxElements[2].checked = false;
}else if(index === 1){
checkBoxElements[0].checked = false;
}else{
checkBoxElements[1].checked = false;
}
}
}));</code></pre><h2>32. notes-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481552" alt="32.gif" title="32.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=C1SGKwO1p5MDWEFQWNgQLQ%3D%3D.bYzV9ycDp8BtRG5%2F7oCcpPyrxddeSBvjiwUgtpmDrXdkuznn%2BCrDv4XUK4BcX6HWNRaXV6cPMfKlaC%2BsUbWtb4WsORumzaIsm%2BjgJa4TLQQ%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=pTY4ynVup2tUHMxHVRkTUg%3D%3D.uR%2FjhU2aG50AeOeI%2Fvf8vfWrkC2FFfFo4oIk93iWVp5BaGAvJkKhvMTjychRMIPxbXKGdK2kT2lDfgFoycFQXQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + 卡片布局。</li><li>JavaScript:<code>marked.js</code>的使用 + <code>localStorage API</code>存储数据 + 鼠标光标位置的计算。如上述示例部分源码如下:</li></ol><pre><code class="js"> on($(".edit", note), "click", e => {
const isFocus = textarea.getAttribute("data-focus");
if (isFocus === "false") {
textarea.setAttribute("data-focus","true");
if(textarea.classList.contains("hidden")){
textarea.classList.remove("hidden");
}
if(!focusStatus){
textarea.value = notes[index].text;
}
const text = textarea.value.trim();
// console.log(text);
if (textarea.setSelectionRange) {
textarea.focus();
textarea.setSelectionRange(text.length, text.length);
}else if (textarea.createTextRange) {
const range = textarea.createTextRange();
range.collapse(true);
range.moveEnd('character', text.length);
range.moveStart('character', text.length);
range.select();
}
} else {
textarea.setAttribute("data-focus","false");
notes[index].text = textarea.value;
main.innerHTML = marked(notes[index].text);
setData("notes", notes);
}
});</code></pre><h2>33. animated-countdown</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481553" alt="33.gif" title="33.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=zLORxFBVpcUNRorlU91Q3w%3D%3D.qj78taNhrno0L6L0d4QvDX4SGseKxDYlgx1IKmpEgjdw%2BpabDFHlJfdh8FLTKnk30YN36cVrybhmgMecLgLPM2oN75S%2BuvFNn1mFfioZWHM%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=FA2DcWv2s%2BMtgnykYzSt%2Fg%3D%3D.96zECtLSYNpDwuo0DGH6Et9a%2B8vbLCv0lUQclFAnbAmarydlu%2FeyTZB30gX3f7iLVrThDPx2QPFw0D0XRyTFZw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + 位移、旋转、缩放动画。</li><li>JavaScript:动画事件 + 动画的创建与重置。如上述示例部分源码如下:</li></ol><pre><code class="js">function runAnimation(){
const numArr = $$("span",numGroup);
const nextToLast = numArr.length - 1;
numArr[0].classList.add("in");
numArr.forEach((num,index) => {
num.addEventListener("animationend",e => {
const {animationName} = e;
if(animationName === "goIn" && index !== nextToLast){
num.classList.remove("in");
num.classList.add("out");
}else if(animationName === "goOut" && num.nextElementSibling){
num.nextElementSibling.classList.add("in");
}else{
counter.classList.add("hide");
final.classList.add("show");
}
})
})
}
function resetAnimation(){
counter.classList.remove("hide");
final.classList.remove("show");
const numArr = $$("span",numGroup);
if(numArr){
numArr.forEach(num => num.classList.value = '');
numArr[0].classList.add("in");
}
}</code></pre><h2>34. image-carousel</h2><p>效果如图所示:</p><p><img src="/img/bVcT1rY" alt="" title=""></p><ul><li><a href="https://link.segmentfault.com/?enc=zPstyrGFZyrJuo9Wdnle0Q%3D%3D.D8klkMV64duvqBA2Z7CcCM7tCWTSGBayTDC5gmGvplsXW3evdHgAsWgpKWzD5ZB2dy27ocPeAY8rKAe%2BZFRE%2FiuN3ETQvbFqKXKIPY%2FJhWs%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=YQ07%2Bf%2Fa7S7FQDQOa58ugQ%3D%3D.iJOrEDo6w%2FGVMGaMzBRSmxijjUuqMmrJ4WUqJRA7eppXI%2FOYVb8Y3nW3HbLtptmH2so0mckPmCpmmmEH%2FYt5pw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局。</li><li>JavaScript:定时器实现轮播。如上述示例部分源码如下:</li></ol><pre><code class="js">function changeCarouselItem(){
if(currentIndex > carouselItems.length - 1){
currentIndex = 0;
}else if(currentIndex < 0){
currentIndex = carouselItems.length - 1;
}
carousel.style.transform = `translateX(-${ currentIndex * carouselContainer.offsetWidth }px)`;
}</code></pre><blockquote>PS:这里额外踩了一个定时器的坑,也就是说,比如我们使用setTimeout模拟实现setInterval方法在这里是会出现问题的,我在js代码里添加了注释说明。</blockquote><pre><code class="js">// let interval = mySetInterval(run,1000);
// Why use this method can't achieve the desired effect?
// Use this method as follow to replace window.setInterval,clicked the prev button more to get the stranger effect.
// Maybe this method does not conform to the specification,So make sure to use window.setInterval.
// function mySetInterval(handler,time = 1000){
// let timer = null;
// const interval = () => {
// handler();
// timer = setTimeout(interval,time);
// }
// interval();
// return {
// clearMyInterval(){
// clearTimeout(timer);
// }
// }
// }</code></pre><p>这是因为我们用<code>setTimeout</code>实现的定时器并不符合规范,<code>setInterval</code>默认会有<code>10ms</code>的延迟执行。<br><a href="https://link.segmentfault.com/?enc=2MjSH0JkyUyvpo2UBq%2B3PQ%3D%3D.bz2P%2B6atX4PZr2I5keypxHolalWLVxP4%2Ba8y%2Fp5flQcul1hDKlqz1ml6BcybrnLnRshhxHXQmPG21FAZe70RDSjqs%2FDHgPKAxYcDCHu81bw%3D" rel="nofollow">参考规范</a>。</p><h2>35. hover board</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481555" alt="35.gif" title="35.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=5vK7%2FIKiH7kU9wzWT4k9yg%3D%3D.6ecP%2FMjQCDoK%2FRW20E8vKTUpQuII3k9UWASJZjscVHYIY8J4itj339CBCAMuJIdlppb922Q9jYMRnkbpenJKlUmDt350rGKxgDEw9l2coao%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=ndZYV8G8jvRWDiHtBAR47Q%3D%3D.TQPE%2BGGcOtLrhXZuKVT%2FxKL3Q08NBBWr3jhrkFE4vna3dR4ymrrOOEGjNJ28N4oPw%2FDkLrM2Q%2FkI%2B7W5LD%2FI7Q%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式 + <code>CSS</code>渐变。</li><li>JavaScript:动态创建元素 + 悬浮事件。如上述示例部分源码如下:</li></ol><pre><code class="js">function setColor(element){
element.style.background = `linear-gradient(135deg, ${ randomColor() } 10%, ${ randomColor() } 100%)`;
element.style.boxShadow = `0 0 2px ${ randomColor() },0 0 10px ${ randomColor() }`;
}
function removeColor(element){
element.style.background = `linear-gradient(135deg, #1d064e 10%, #10031a 100%)`;
element.style.boxShadow = `0 0 2px #736a85`;
}</code></pre><h2>36. pokedex</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481556" alt="36.gif" title="36.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=oARXfiSBjYnHTKHdMMd%2BHA%3D%3D.ElstftpRU%2FunIFCcgL1ewDQ0PN85y3%2FfzCWkAmZ8WmoFug7nmDWygYRRtgdhMwgdVMV1OcnjtxNB%2FAipyIesDpBup1n0%2FS2UuG%2FyEomxkyI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=aiJWMXo3jxYfSzp9KFF9HQ%3D%3D.CKwyVaJZxWdovmiF4nw4alV%2B0dJnMpf0xGgbomR8cqHwOQYgr83CIfyZhC40sGIs78%2BZK9oif9r7QD5l%2FCNm2g%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局 + <code>CSS</code>渐变。</li><li>JavaScript:<code>fetch API</code>接口请求 + 创建卡片。如上述示例部分源码如下:</li></ol><pre><code class="js">function createPokemon(pokemon){
const pokemonItem = document.createElement("div");
pokemonItem.classList.add("pokedex");
const name = pokemon.name[0].toUpperCase() + pokemon.name.slice(1).toLowerCase();
const id = pokemon.id.toString().padStart(3,"0");
const poke_types = pokemon.types.map(type => type.type.name);
const type = main_types.find(type => poke_types.indexOf(type) > -1);
const color = colors[type];
pokemonItem.style.background = `linear-gradient(135deg, ${ color } 10%, ${ randomColor() } 100%)`;
pokemonItem.innerHTML = `
<div class="pokedex-avatar">
<img src="https://pokeres.bastionbot.org/images/pokemon/${pokemon.id}.png" alt="the pokemon">
</div>
<div class="info">
<span class="number">#${ id }</span>
<h3 class="name">${ name }</h3>
<small class="type">Type:<span>${ type }</span></small>
</div>`;
container.appendChild(pokemonItem);
}</code></pre><blockquote>特别说明:接口似乎不太稳定,也许是我网络原因,图片没有加载出来。</blockquote><h2>37. mobile-tab-navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481557" alt="37.gif" title="37.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=nMxpcIXP%2BD%2BOGAxMj0Yjng%3D%3D.%2FmcYQEFTfLNi%2BFRZD9P0BOmYL2Ls3bago4%2BNMniEjSiUSuXDdEMt57yKB6RRfduqgjUOWE60s9wBHa92Ur6EsvYG%2F6AiwtCjBUW0ZelXuRE%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=nji9URtmd4y4WuAcVsuqbA%3D%3D.M5dmuKclBkrkx5hZMESIqF0DE3w89nKQLF95z8rfCgYP6hsEU1Ze8PfGY0jW158uWkw0lIohZgXNt%2Belcd68XA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局 + <code>CSS</code>渐变实现手机布局。</li><li>JavaScript:感觉就是移动端实现一个轮播图切换,利用<code>opacity</code>的设置,没什么好说的。如上述示例部分源码如下:</li></ol><pre><code class="js">function hideAllElement(nodeList){
nodeList.forEach(item => item.classList.remove("active"));
}
navItems.forEach((item,index) => {
item.addEventListener("click",() => {
hideAllElement(navItems);
hideAllElement(carouselItems);
item.classList.add("active");
carouselItems[index].classList.add("active");
})
})</code></pre><h2>38. password-strength-background</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481558" alt="38.gif" title="38.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=HcL%2Fr46Z0ppYDkjjPW25ZA%3D%3D.n5graQb4ZKpwGCd1DX3%2FovW%2FCRwgOgkd%2F%2FORicO7GCoSYXQeBvwNcOmezaH6THIq9c9FKXQmTfWcF8%2BJzKhaJq8Xjsrm5dTm1jDXRQia8YI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=hAZeVVl026EsLVHSh3Y5Iw%3D%3D.a945R%2Bd9eBhPcaaMRrXjAai22esflXlg%2F2wdU1ib%2Be9weNVECKLPwYOvnToHrF2K7K8maUIP%2Blwrwn87M3f10Q%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:其实主要是使用<code>tailwind.css</code>这个原子化<code>CSS</code>框架。</li><li>JavaScript:监听输入框事件,然后改变背景模糊度。如上述示例部分源码如下:</li></ol><pre><code class="js">password.addEventListener("input",e => {
const { value } = e.target;
const blur = 20 - value.length * 2;
background.style.filter = `blur(${ blur }px)`;
});</code></pre><h2>39. 3D-background-boxes</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481559" alt="39.gif" title="39.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=7Ga6S6DXd112absYfrrYYg%3D%3D.fIM8mRkCzDxP29e5jV4XIQRO3dYPSI%2FWE3gZPgo13JGSMwQZ2aEJpuyXaMm4yqeErUCoLjQ6XtKA%2F5GCDs%2FsH6YNUQBw3dZSF75nyuEibe0%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=FZmdb3ETUdOhBgKdzK0Mxw%3D%3D.%2FHG2q%2FmOfHX%2B5zRRJTlTBYEcrosrLKaoxX4JFD%2BpUbcX%2FS9eNdKv3C3X2HA%2F9blBOPyPyV2bSoaStuGskt5V7Q%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>vw、vh</code>布局 + <code>skew</code>倾斜变换。</li><li>JavaScript:循环 + 背景图定位的设置。如上述示例部分源码如下:</li></ol><pre><code class="js">function createBox(){
for(let i = 0;i < 4;i++){
for(let j = 0;j < 4;j++){
const box = document.createElement("div");
box.classList.add("box");
box.style.backgroundPosition = `${ -j * 15 }vw ${ -i * 15 }vh`;
boxContainer.appendChild(box);
}
}
}</code></pre><h2>40. Verify Your Account</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481560" alt="40.gif" title="40.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=5R6CoKKDQnZuNzJRrNgd%2Fw%3D%3D.%2Fu%2FFv90EI0t5t8mlc0OGUDPZOsTroD0v4tYPdcyk5r4KanBUfEBVtJYmTmt2ZxTVynk%2FKOnef2wTJmXQ5F5ehEWEnzu2n7wG9S%2FXKh04d0c%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=k1k71a4XwnTh0Z6ylR5WSg%3D%3D.SOi2420gh78T2fuzwX7guh4sGcmDOS5f7jK%2Fh72YGz62jbaI2szev7O5pVvB5hwO2bATLfLZ0GfsM5lesj%2B%2BEA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局 + <code>input</code>的一些特别样式设置。</li><li>JavaScript:<code>JavaScript focus</code>事件。如上述示例部分源码如下:</li></ol><pre><code class="css">.code-container .code:focus {
border-color: #2397ef;
}
.code::-webkit-outer-spin-button,
.code::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.code:valid {
border-color: #034775;
box-shadow: 0 10px 10px -5px rgba(0, 0, 0, 0.25);
}</code></pre><pre><code class="js">function onFocusHandler(nodeList){
onFocus(nodeList[0]);
nodeList.forEach((node,index) => {
node.addEventListener("keydown",e => {
// console.log(e.key);
if(e.key >= 0 && e.key <= 9){
nodeList[index].value = "";
setTimeout(() => onFocus(nodeList[index + 1]),0);
}else{
setTimeout(() => onFocus(nodeList[index - 1]),0);
}
})
});
}
function onFocus(node){
if(!node)return;
const { nodeName } = node;
return nodeName && nodeName.toLowerCase() === "input" && node.focus();
}</code></pre><h2>41. live-user-filter</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481561" alt="41.gif" title="41.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=VZHDwJC%2F3lyP2lPpkMSktQ%3D%3D.nJmRZ2Ogsn82ZF2V3NztAhAqroYvL2fq8EUclY0LuBAQANZFJLQoHD17NBNTKcTnJxJK%2Bbm1BAPmU5Lyzn20BQysojf0kpb9xWpG7DeLfTU%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=rfdGxkSZekugStXHte0dog%3D%3D.ylpODF38OPkmqwpXNVZphq1CA8pODO5RXQ04yUeVE9yumvc6zqgh8SyjaqSnQTQ%2BJj%2B4mTXTzM5FDE5CXVD44g%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局 + 滚动条样式。</li><li>JavaScript:<code>fetch API</code>接口请求 + <code>input</code>事件过滤数据。如上述示例部分源码如下:</li></ol><pre><code class="js">async function requestData(){
const res = await fetch('https://randomuser.me/api?results=50');
const { results } = await res.json();
result.innerHTML = "";
results.forEach(user => {
const listItem = document.createElement("li");
filterData.push(listItem);
const { picture:{ large },name:{ first,last },location:{ city,country } } = user;
listItem.innerHTML = `
<img src="${ large }" alt="${ first + ' ' + last }" />
<div class="user-info">
<h4>${ first } ${ last }</h4>
<p>${ city },${ country }</p>
</div>
`;
result.appendChild(listItem);
})
}</code></pre><h2>42. feedback-ui-design</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481562" alt="42.gif" title="42.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=oJgAtHvh7gPE0oa%2FR235Mg%3D%3D.8FzpyVnnRe4xvBZFIrmqt9yiSuponIv%2Bwvnd4gtS9D1N9Y41gl40C94T%2BdRTDg7ckdwIt3fMYFh5EfI9J%2BTb5Z568xIbmCNVumRAw0WuhK4%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=uNWjl92Vb3zqFiaiuXxWNA%3D%3D.XZivODKbVmHzOx%2FRu4XIeqk2EbB1umOX2dZP%2BW58b57zQiynK3NqJWn%2B9iyQmatBOGPSUAo5vDsCB1LzVt9gog%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>画爱心 + 基本样式布局。</li><li>JavaScript:选项卡切换功能的实现。如上述示例部分源码如下:</li></ol><pre><code class="js">ratingItem.forEach(item => {
item.addEventListener("click",e => {
siblings(item).forEach(sib => sib.classList.remove("active"));
item.classList.add("active");
selectRating = item.innerText;
})
});
send.addEventListener("click",() => {
selectRatingItem.innerText = selectRating;
sendPage.classList.add("hide");
receivePage.classList.remove("hide");
});
back.addEventListener("click",() => {
selectRating = $(".rating.active").innerText;
selectRatingItem.innerText = $(".rating.active").innerText;
sendPage.classList.remove("hide");
receivePage.classList.add("hide");
});</code></pre><h2>43. custom-range-slider</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481563" alt="43.gif" title="43.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=4cdUAz%2BEyo1qQjQ7yLbSfQ%3D%3D.ZQ9kylVaHsPmvGi7uum8vxTAlOLlcHG1T8knCYI2SqMU5iSd01HV4d08nG3RUOTGjzScsjEb4jz%2BEsE3orbcWsZqNqNQQ5cayd5vmVMRnHI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=cfLQk%2Fzh4TfVLdfD5VXk0A%3D%3D.Ap%2FsXka0la89nvBOx%2BTNubriWG2g9OYFsc0%2BTVIm0ikFhYpZohFZ%2F2q46KEw1W16CItZZeuyrbe%2FXZadfK2kzw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>input</code>元素的样式设置(兼容写法) + 基本样式布局。</li><li>JavaScript:<code>input range</code>元素的<code>input</code>事件监听。如上述示例部分源码如下:</li></ol><pre><code class="css">input[type="range"]::-webkit-slider-runnable-track {
background-image: linear-gradient(135deg, #E2B0FF 10%, #9F44D3 100%);
width: 100%;
height: 12px;
cursor: pointer;
border-radius: 4px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background-image: linear-gradient(135deg, #d9e2e6 10%, #e4e1e4 100%);
border: 1px solid #b233e4;
cursor: pointer;
margin-top: -6px;
}</code></pre><pre><code class="js">range.addEventListener("input",e => {
// string to the number
const value = +e.target.value;
const label = e.target.nextElementSibling;
const range_width = getStyle(e.target,"width");//XXpx
const label_width = getStyle(label,"width");//XXpx
// Due to contain px,slice the width
const num_width = +range_width.slice(0,range_width.length - 2);
const num_label_width = +label_width.slice(0,label_width.length - 2);
const min = +e.target.min;
const max = +e.target.max;
const left = value * (num_width / max) - num_label_width / 2 + scale(value,min,max,10,-10);
label.style.left = `${left}px`;
label.innerHTML = value;
});</code></pre><h2>44. netflix-mobile-navigation</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481564" alt="44.gif" title="44.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=lDNsnD8RIJis17oTt4qzcA%3D%3D.orh2j0ixOKonJp5x9XN86njQk8nbRjexTF6UhxPfd8%2BdDefzT4HWYdvkan5gUTlngkVQSWWaEd1PR%2Bu%2FUh10L2XIpjhsfgTq3%2BS9gH4Sl78%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=VZSB71bAOKY3bNqy48NIPg%3D%3D.OuhojVli%2F%2Fr4rh5Vy%2BkRCOrcAn1efP5oEaraHMjgC669QyWr3Szl%2FjNi0BG7vDULWkN4LENREECi5KKbl9UZaA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:导航宽度的变化 + <code>CSS</code>渐变 + 基本样式布局。</li><li>JavaScript:点击按钮切换导航栏的显隐。如上述示例部分源码如下:</li></ol><pre><code class="js">function changeNav(type){
navList.forEach(nav => nav.classList[type === "open" ? "add" : "remove"]("visible"));
}</code></pre><h2>45. quiz-app</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481565" alt="45.gif" title="45.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=PC7WOow%2Bt8eD9fGs9KYj0A%3D%3D.m%2BwNurAkacHS6%2B3cwcHQb0RO5TkrcSnOOKTP%2BjGgGf5CRGpI9WsJ0Sq7FuD6dpRdDquIWfMOx5sUrJQOv0dZZG6vbqgcaJUQeN%2Fh1lf0pIg%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=jFoWbJjiDAmvYWkwrakFig%3D%3D.zfj9Y7bNAcSvDTotP1y6tNaz%2BCi05Wyhlyru9mQT5BUVroYLi7oIoIjeUaVABeS231%2B0Y7U3NvRZ4VORQTS3Rw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:卡片布局 + 基本样式。</li><li>JavaScript:表单提交 + 选择题的计算。如上述示例部分源码如下:</li></ol><pre><code class="js">submit.addEventListener("click",() => {
const answer = getSelected();
if(answer){
if(answer === quizData[currentQuestion].correct){
score++;
}
currentQuestion++;
if(currentQuestion > quizData.length - 1){
quizContainer.innerHTML = `
<h2>You answered ${ score } / ${ quizData.length } questions correctly!</h2>
<button type="button" class="btn" onclick="location.reload()">reload</button>
`
}else{
loadQuiz();
}
}
})</code></pre><h2>46. testimonial-box-switcher</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481566" alt="46.gif" title="46.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=Ks1HFyYwqrtGYAh1uI75nQ%3D%3D.LTQ%2BjAxnsl%2BQ65PxeIGEprId3ZxdjohPX6rjT%2B16i4khIu%2BjkaHWXuQGBb4TSYt3%2B5nwFmRv50TloDRTBtCVOGhJl80pMkRwKqks7fr%2BoGo%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=oip9JElSLLwDUhweJWCbWg%3D%3D.Y7pU%2BqaVdiBcng2w24rkKsqQE79svCcSm%2BGdNAyv4KOYlM9eDAVrgE2C3c9DMIyHjV9Fn2EBhSw00Wx16SOzCQ%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>动画 + 宽度的改变实现进度条 + <code>font-awesome</code>字体库的使用 + 基本样式布局。</li><li>JavaScript:定时器的用法,注意定时器的执行时间与设置进度条的执行动画时间一样。如上述示例部分源码如下:</li></ol><pre><code class="css">.progress-bar {
width: 100%;
height: 4px;
background-color: #fff;
animation: grow 10s linear infinite;
transform-origin: left;
}
@keyframes grow {
0% {
transform: scaleX(0);
}
}</code></pre><pre><code class="js">function updateTestimonial(){
const { text,name,position,photo } = testimonials[currentIndex];
const renderElements = [testimonial,username,role];
userImage.setAttribute("src",photo);
order.innerHTML = currentIndex + 1;
[text,name,position].forEach((item,index) => renderElements[index].innerHTML = item);
currentIndex++;
if(currentIndex > testimonials.length - 1){
currentIndex = 0;
}
}</code></pre><blockquote>特别说明:CSS也是可以实现进度条的。</blockquote><h2>47. random-image-feed</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481567" alt="47.gif" title="47.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=Swr4cUnp4Ihnu6%2BGdL0wTA%3D%3D.Xhf8qTgVsDh%2BBSqamSwSmLb8kl1vVWmGO56ETiXgV15tSBfp0jlap4wa6vEug3UDP6q28MhYxI8%2BshuaZuai3FPj0Tsiywe9R%2Bmz%2BAGkYqI%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=vJZbm%2BXOjM8coFdjPp48nA%3D%3D.wo1Rroq2unIrCHABFj2j1FKg8l7w%2BDk%2FwEXfJcM%2FIgJww1vRs3xn0CbdGMMtvst65Aokq%2Bd%2BJZio0lrbGvTDxA%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:图片布局 + 基本样式布局 + <code>CSS</code>提示框的实现(定位 + 伪元素) + <code>CSS</code>实现回到顶部效果。</li><li>JavaScript:随机数 + (滚动事件的监听)控制回到顶部按钮的显隐。如上述示例部分源码如下:</li></ol><pre><code class="js">function onLoad(rows = 5) {
container.innerHTML = "";
for (let i = 0; i < rows * 3; i++) {
const imageItem = document.createElement("img");
imageItem.src = `${refreshURL}${getRandomSize()}`;
imageItem.alt = "random image-" + i;
imageItem.loading = "lazy";
container.appendChild(imageItem);
}
}
function getRandomSize() {
return `${getRandomValue()}x${getRandomValue()}`;
}
function getRandomValue() {
return Math.floor(Math.random() * 10) + 300;
}
window.onload = () => {
changeBtn.addEventListener("click", () => onLoad());
window.addEventListener("scroll", () => {
const { scrollTop, scrollHeight } = document.documentElement || document.body;
const { clientHeight } = flexCenter;
back.style.display = scrollTop + clientHeight > scrollHeight ? 'block' : 'none';
})
onLoad();
}</code></pre><h2>48. todo-list</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481568" alt="48.gif" title="48.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=edq9aI9z48tYy3Fzz3qL%2BA%3D%3D.H8UDeORWxPVPLkMupgg3NGNnPiIgQUhHxzOtXV%2B1HfknOwwLn8X2%2BW6JENeLXbZCdYQECn33%2FwK%2BBXt1g6jUz5eS4pNsymxnbt87HTXI58I%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=K3mNzAnhKCszuCkvejRGew%3D%3D.EbDmu%2B5RCX7R%2Br6jZL84ZUVvaT6Sc1wxowAiFbPoVgh1JbZlptNBXbBCd8NS3jaZ9xt9YI%2Boy%2BM9ZPUJnvnrVg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局 + <code>CSS</code>渐变。</li><li>JavaScript:<code>keydown</code>与<code>contextmenu</code>事件的监听 + <code>localStorage API</code>存储数据。如上述示例部分源码如下:</li></ol><pre><code class="js">function addTodo(todo){
let inputValue = input.value;
if(todo){
inputValue = todo.text;
}
if(inputValue){
const liItem = document.createElement("li");
liItem.innerText = inputValue;
if(todo && todo.complete){
liItem.classList.add("complete");
}
liItem.addEventListener("click",() => {
liItem.classList.toggle("complete");
updateList();
});
liItem.addEventListener("contextmenu",(e) => {
e.preventDefault();
liItem.remove();
updateList();
});
todoList.appendChild(liItem);
input.value = "";
updateList();
}
}
function updateList(){
const listItem = $$("li",todoList);
const saveTodoData = [];
listItem.forEach(item => {
saveTodoData.push({
text:item.innerText,
complete:item.classList.contains("complete")
})
});
localStorage.setItem("todoData",JSON.stringify(saveTodoData));
}</code></pre><h2>49. insect-catch-game</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481569" alt="49.gif" title="49.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=1OxdqBY%2FbzJgXot8eL7xBA%3D%3D.kaYB8xhdlQBqm1AJz1DVbR019eBBHKK2skLschIoPEBf6k7700LnmkWxAL3jMCxg7Qa2vwqXw3FyLtAuyI3ptOFB2mOznVfUYEUyLLj5iog%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=qdihQAtcaY3PiFZ%2BaOmivg%3D%3D.MWhnMDgc2rcmxk1GpPe3BSazW76FUlHn8ewLmv4Y%2FWppOJxFrSNoxvI4T8nhNpAvlSQrWX1IBO4Dk6bZSBYtdw%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:基本样式布局。</li><li>JavaScript:中英文切换 + 替换元素中的文本(不包含标签元素) + 定时器的用法。如上述示例部分源码如下:</li></ol><pre><code class="js">function replaceText(el,text,wrapSymbol = ""){
let newText = [];
if(wrapSymbol){
// why not use split method?,because it can filter the wrap symbol.
const getIndex = (txt) => txt.search(new RegExp("\\" + wrapSymbol));
let searchIndex = getIndex(text),i = 0,len = text.length;
while(searchIndex !== -1){
newText.push(text.slice(i,searchIndex) + wrapSymbol);
i = searchIndex;
if(getIndex(text.slice(searchIndex + 1)) === -1){
newText.push(text.slice(searchIndex + 1,len));
}
searchIndex = getIndex(text.slice(searchIndex + 1));
}
}
const walk = (el,handler) => {
const children = Array.from(el.childNodes);
let wrapIndex = children.findIndex(node => node.nodeName.toLowerCase() === "br");
children.forEach((node,index) => {
if(node.nodeType === 3 && node.textContent.replace(/\s+/,"")){
wrapSymbol ? handler(node,newText[index - wrapIndex < 0 ? 0 : index - wrapIndex]) : handler(node,text);;
}
});
}
walk(el,(node,txt) => {
const parent = node.parentNode;
parent.insertBefore(document.createTextNode(txt),node);
parent.removeChild(node);
});
}</code></pre><p>以上工具函数的实现参考<a href="https://link.segmentfault.com/?enc=%2F1JRiSNMtVG4IsjbqVlfMw%3D%3D.F34GkcughWJYFWkduuyvX%2BzVbI7a0lPTR7GjYxouBoowo%2BUK%2FaRfOGZYFHTeyVqjP3N4jKgDSUnGfXUlmmjZBdgz148hWgYvlbUaooh%2BVBkym%2BTz8TfjjcKxqWe7XHmE" rel="nofollow">这里来实现的</a>。</p><blockquote>特别说明:这个示例算是一个比较综合的示例了。</blockquote><h2>50. kinetic-loader</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481570" alt="50.gif" title="50.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=HVhjzONTPqGzOWOHrsMPbA%3D%3D.3uavx4xOMTl9MnIuCVpnzxy1mkbePl4LSKEjsk4rxTiYGjRzIK1CsM2chasr%2Bi5cSCc0IgTFEmvLa%2BePgYnC4Cwhwn%2BqfZWNU0l%2BVxDwQ0A%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=fyx%2Fy4m%2FUwCLVqGdsQm7tw%3D%3D.C7OfcZTIm7BQgpWRRB4XwWyJmvIcsNInFlnwpRSxVoG4NsoTGoeM4MLkAaHdAKr3tu1XoeCW9woocDR2BCWybg%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:CSS旋转动画 + 基本样式布局。<br>如上述示例部分源码如下:</li></ol><pre><code class="js">@keyframes rotateA {
0%,25% {
transform: rotate(0deg);
}
50%,75% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotateB {
0%,25% {
transform: rotate(90deg);
}
50%,75% {
transform: rotate(270deg);
}
100% {
transform: rotate(450deg);
}
}</code></pre><p>最后一天,额外的实现了一个<code>404</code>效果,算是特别结尾吧,是不是很花哨(<code>^_^</code>)。这算是一个<code>CSS</code>动画的综合使用,如下所示:</p><h2>51. 404 page</h2><p>效果如图所示:</p><p><img src="/img/remote/1460000040481571" alt="51.gif" title="51.gif"></p><ul><li><a href="https://link.segmentfault.com/?enc=zFiIAtc2vNStmyhk71%2FLlg%3D%3D.sGA%2BnmcNvq2D50X4IE9rJtMIULZPXdkrH7NQX0F4EbqmTAzuQuN8twZIN7Dr%2Be7CkCkz6OyQK7eISxRUy%2BFz6lzi%2BMgoag3ycapRmUsd9aA%3D" rel="nofollow">源码</a></li><li><a href="https://link.segmentfault.com/?enc=%2FmgowkxVdbHfUfRp6sPsSw%3D%3D.9UZUSfvGeSvlfpiC2VzV3OqIJ53FYlBzLVkoK5UFCIhCQSd%2BtOaBjNKwNQ2ZBY9Dw5fU4zzBGjgzIIJ8lIKy8A%3D%3D" rel="nofollow">在线示例</a></li><li>[ ] 知识点总结:</li></ul><ol><li>CSS:<code>CSS</code>动画的用法 + 基本样式布局 + <code>svg</code>图标元素的样式设置。<br>如上述示例部分源码如下:</li></ol><pre><code class="js">@keyframes background {
from {
background: linear-gradient(135deg,#e0e0e0 10%,#ffffff 90%);
}
to {
background: linear-gradient(135deg,#ffffff 10%,#e0e0e0 90%);
}
}
@keyframes stampSlide {
0% {
transform: rotateX(90deg) rotateZ(-90deg) translateZ(-200px) translateY(130px);
}
100% {
transform: rotateX(90deg) rotateZ(-90deg) translateZ(-200px) translateY(-4000px);
}
}
@keyframes slide {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-200px);
}
}
@keyframes roll {
0% {
transform: rotateZ(0deg);
}
85% {
transform: rotateZ(90deg);
}
87% {
transform: rotateZ(88deg);
}
90% {
transform: rotateZ(90deg);
}
100% {
transform: rotateZ(90deg);
}
}
@keyframes zeroFour {
0% {
content:"4";
}
100% {
content:"0";
}
}
@keyframes draw {
0% {
stroke-dasharray: 30 150;
stroke-dashoffset: 42;
}
100% {
stroke:rgba(8, 69, 131, 0.9);
stroke-dasharray: 224;
stroke-dashoffset: 0;
}
}</code></pre><blockquote>特别说明:以上前50个示例参考<a href="https://link.segmentfault.com/?enc=5hw4%2B0GhyJJ8qMhukulwkA%3D%3D.PZ%2BnJXlCUKcUZpu8J59%2Fy6q%2BAL4mh75crsYyF%2B2PKQE%3D" rel="nofollow">https://50projects50days.com/</a>实现的,不同于原项目,代码实现不一样,很多示例我也做了相应的扩展。</blockquote>
2021年,让我们手写一个mini版本的vue2.x和vue3.x框架
https://segmentfault.com/a/1190000040236708
2021-06-24T22:15:48+08:00
2021-06-24T22:15:48+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
51
<h2>mini版本的vue.js2.X版本框架</h2><h3>模板代码</h3><p>首先我们看一下我们要实现的模板代码:</p><pre><code class="html"><div id="app">
<h3>{{ msg }}</h3>
<p>{{ count }}</p>
<h1>v-text</h1>
<p v-text="msg"></p>
<input type="text" v-model="count">
<button type="button" v-on:click="increase">add+</button>
<button type="button" v-on:click="changeMessage">change message!</button>
<button type="button" v-on:click="recoverMessage">recoverMessage!</button>
</div></code></pre><h3>逻辑代码</h3><p>然后就是我们要编写的javascript代码。</p><pre><code class="js">const app = new miniVue({
el:"#app",
data:{
msg:"hello,mini vue.js",
count:666
},
methods:{
increase(){
this.count++;
},
changeMessage(){
this.msg = "hello,eveningwater!";
},
recoverMessage(){
console.log(this)
this.msg = "hello,mini vue.js";
}
}
});</code></pre><h3>运行效果</h3><p>我们来看一下实际运行效果如下所示:</p><p><img src="/img/bVcSZz1" alt="" title=""></p><p>思考一下,我们要实现如上的功能应该怎么做呢?你也可以单独打开以上示例:</p><p><a href="https://link.segmentfault.com/?enc=Yg887htOZG3Ghrbmd6aWvg%3D%3D.Ga6b9kpSq3BnKSpQLbqF%2Fa%2BckwxYl5TFFWtnNGHgjRqtzH9cn2dbB0MuYPUZGHj%2BaGmJ3AjG7iL2X92waK6k4DTfCXAEqqBs32bcpW0QKgs%3D" rel="nofollow">点击此处</a>。</p><h2>源码实现-2.x</h2><h3>miniVue类</h3><p>首先,不管三七二十一,既然是实例化一个<code>mini-vue</code>,那么我们先定义一个类,并且它的参数一定是一个属性配置对象。如下:</p><pre><code class="js"> class miniVue {
constructor(options = {}){
//后续要做的事情
}
}</code></pre><p>现在,让我们先初始化一些属性,比如data,methods,options等等。</p><pre><code class="js">//在miniVue构造函数的内部
//保存根元素,能简便就尽量简便,不考虑数组情况
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
this.$methods = options.methods;
this.$data = options.data;
this.$options = options;</code></pre><p>初始化完了之后,我们再来思考一个问题,我们是不是可以通过在vue内部使用this访问到vue定义的数据对象呢?那么我们应该如何实现这一个功能呢?这个功能有一个专业的名词,叫做<b>代理(proxy)</b>。</p><h3>代理数据</h3><p>因此我们来实现一下这个功能,很明显在这个miniVue类的内部定义一个proxy方法。如下:</p><pre><code class="js">//this.$data.xxx -> this.xxx;
//proxy代理实例上的data对象
proxy(data){
//后续代码
}</code></pre><p>接下来,我们需要知道一个api,即<code>Object.defineProperty</code>,通过这个方法来完成这个代理方法。如下:</p><pre><code class="js">//proxy方法内部
// 因为我们是代理每一个属性,所以我们需要将所有属性拿到
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get:() => {
return data[key];
},
set:(newValue){
//这里我们需要判断一下如果值没有做改变就不用赋值,需要排除NaN的情况
if(newValue === data[key] || _isNaN(newValue,data[key]))return;
data[key] = newValue;
}
})
})</code></pre><p>接下来,我们来看一下这个<code>_isNaN</code>工具方法的实现,如下:</p><pre><code class="js">function _isNaN(a,b){
return Number.isNaN(a) && Number.isNaN(b);
}</code></pre><p>定义好了之后,我们只需要在miniVue类的构造函数中调用一次即可。如下:</p><pre><code class="js">// 构造函数内部
this.proxy(this.$data);</code></pre><p>代理就这样完成了,让我们继续下一步。</p><h3>数据响应式观察者observer类</h3><p>我们需要对数据的每一个属性都定义一个响应式对象,用来监听数据的改变,所以我们需要一个类来管理它,我们就给它取个名字叫<code>Observer</code>。如下:</p><pre><code class="js">class Observer {
constructor(data){
//后续实现
}
}</code></pre><p>我们需要给每一个数据都添加响应式对象,并且转换成getter和setter函数,这里我们又用到了<code>Object.defineProperty</code>方法,我们需要在getter函数中收集依赖,在setter函数中发送通知,用来通知依赖进行更新。我们用一个方法来专门去执行定义响应式对象的方法,叫walk,如下:</p><pre><code class="js">//再次申明,不考虑数组,只考虑对象
walk(data){
if(typeof data !== 'object' || !data)return;
// 数据的每一个属性都调用定义响应式对象的方法
Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));
}</code></pre><p>接下来我们来看<code>defineReactive</code>方法的实现,同样也是使用<code>Object.defineProperty</code>方法来定义响应式对象,如下所示:</p><pre><code class="js">defineReactive(data,key,value){
// 获取当前this,以避免后续用vm的时候,this指向不对
const vm = this;
// 递归调用walk方法,因为对象里面还有可能是对象
this.walk(value);
//实例化收集依赖的类
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get(){
// 收集依赖,依赖存在Dep类上
Dep.target && Dep.add(Dep.target);
return value;
},
set(newValue){
// 这里也判断一下
if(newValue === value || __isNaN(value,newValue))return;
// 否则改变值
value = newValue;
// newValue也有可能是对象,所以递归
vm.walk(newValue);
// 通知Dep类
dep.notify();
}
})
}</code></pre><p><code>Observer</code>类完成了之后,我们需要在miniVue类的构造函数中实例化一下它,如下:</p><pre><code class="js">//在miniVue构造函数内部
new Observer(this.$data);</code></pre><p>好的,让我们继续下一步。</p><h3>依赖类</h3><p><code>defineReactive</code>方法内部用到了<code>Dep</code>类,接下来,我们来定义这个类。如下:</p><pre><code class="js">class Dep {
constructor(){
//后续代码
}
}</code></pre><p>接下来,我们来思考一下,依赖类里面,我们需要做什么,首先根据<code>defineReactive</code>中,我们很明显就知道会有<code>add</code>方法和<code>notify</code>方法,并且我们需要一种数据结构来存储依赖,vue源码用的是队列,而在这里为了简单化,我们使用ES6的set数据结构。如下:</p><pre><code class="js">//构造函数内部
this.deps = new Set();</code></pre><p>接下来,就需要实现<code>add</code>方法和<code>notify</code>方法,事实上这里还会有删除依赖的方法,但是这里为了最简便,我们只需要一个<code>add</code>和<code>notify</code>方法即可。如下:</p><pre><code class="js">add(dep){
//判断dep是否存在并且是否存在update方法,然后添加到存储的依赖数据结构中
if(dep && dep.update)this.deps.add(dep);
}
notify(){
// 发布通知无非是遍历一道dep,然后调用每一个dep的update方法,使得每一个依赖都会进行更新
this.deps.forEach(dep => dep.update())
}</code></pre><p>Dep类算是完了,接下来我们就需要另一个类。</p><h3>Watcher类</h3><p>那就是为了管理每一个组件实例的类,确保每个组件实例可以由这个类来发送视图更新以及状态流转的操作。这个类,我们把它叫做<code>Watcher</code>。</p><pre><code class="js">class Watcher {
//3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
constructor(vm,key,cb){
//后续代码
}
}</code></pre><p>再次思考一下,我们的Watcher类需要做哪些事情呢?我们先来思考一下<code>Watcher</code>的用法,我们是不是会像如下这样来写:</p><pre><code class="js">//3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
new Watcher(vm,key,cb);</code></pre><p>ok,知道了使用方式之后,我们就可以在构造函数内部初始化一些东西了。如下:</p><pre><code class="js">//构造函数内部
this.vm = vm;
this.key = key;
this.cb = cb;
//依赖类
Dep.target = this;
// 我们用一个变量来存储旧值,也就是未变更之前的值
this.__old = vm[key];
Dep.target = null;</code></pre><p>然后Watcher类就多了一个update方法,接下来让我们来看一下这个方法的实现吧。如下:</p><pre><code class="js">update(){
//获取新的值
let newValue = this.vm[this.key];
//与旧值做比较,如果没有改变就无需执行下一步
if(newValue === this.__old || __isNaN(newValue,this.__old))return;
//把新的值回调出去
this.cb(newValue);
//执行完之后,需要更新一下旧值的存储
this.__old = newValue;
}</code></pre><h3>编译类compiler类</h3><h4>初始化</h4><p>到了这一步,我们就算是完全脱离vue源码了,因为vue源码的编译十分复杂,涉及到diff算法以及虚拟节点vNode,而我们这里致力于将其最简化,所以单独写一个Compiler类来编译。如下:</p><pre><code class="js">class Compiler {
constructor(vm){
//后续代码
}
}</code></pre><blockquote>注意:这里的编译是我们自己根据流程来实现的,与vue源码并没有任何关联,vue也有compiler,但是与我们实现的完全不同。</blockquote><p>定义好了之后,我们在miniVue类的构造函数中实例化一下这个编译类即可。如下:</p><pre><code class="js">//在miniVue构造函数内部
new Compiler(this);</code></pre><p>好的,我们也看到了使用方式,所以接下来我们来完善这个编译类的构造函数内部的一些初始化操作。如下:</p><pre><code class="js">//编译类构造函数内部
//根元素
this.el = vm.$el;
//事件方法
this.methods = vm.$methods;
//当前组件实例
this.vm = vm;
//调用编译函数开始编译
this.compile(vm.$el);</code></pre><h4>compile方法</h4><p>初始化操作算是完成了,接下来我们来看compile方法的内部。思考一下,在这个方法的内部,我们是不是需要拿到所有的节点,然后对比是文本还是元素节点去分别进行编译呢?如下:</p><pre><code class="js">compile(el){
//拿到所有子节点(包含文本节点)
let childNodes = el.childNodes;
//转成数组
Array.from(childNodes).forEach(node => {
//判断是文本节点还是元素节点分别执行不同的编译方法
if(this.isTextNode(node)){
this.compileText(node);
}else if(this.isElementNode(node)){
this.compileElement(node);
}
//递归判断node下是否还含有子节点,如果有的话继续编译
if(node.childNodes && node.childNodes.length)this.compile(node);
})
}</code></pre><p>这里,我们需要2个辅助方法,判断是文本节点还是元素节点,其实我们可以使用节点的nodeType属性来进行判断,由于文本节点的nodeType值为3,而元素节点的nodeType值为1。所以这2个辅助方法我们就可以实现如下:</p><pre><code class="js">isTextNode(node){
return node.nodeType === 3;
}
isElementNode(node){
return node.nodeType === 1;
}</code></pre><h4>编译文本节点</h4><p>接下来,我们下来看<code>compileText</code>编译文本节点的方法。如下:</p><pre><code class="js">//{{ count }}数据结构是类似如此的
compileText(node){
//后续代码
}</code></pre><p>接下来,让我们思考一下,我们编译文本节点,无非就是把文本节点中的<code>{{ count }}</code>映射成为0,而文本节点不就是node.textContent属性吗?所以此时我们可以想到根据正则来匹配<code>{{}}</code>中的count值,然后对应替换成数据中的count值,然后我们再调用一次Watcher类,如果更新了,就再次更改这个node.textContent的值。如下:</p><pre><code class="js">compileText(node){
//定义正则,匹配{{}}中的count
let reg = /\{\{(.+?)\}\}/g;
let value = node.textContent;
//判断是否含有{{}}
if(reg.test(value)){
//拿到{{}}中的count,由于我们是匹配一个捕获组,所以我们可以根据RegExp类的$1属性来获取这个count
let key = RegExp.$1.trim();
node.textContent = value.replace(reg,this.vm[key]);
//如果更新了值,还要做更改
new Watcher(this.vm,key,newValue => {
node.textContent = newValue;
})
}
}</code></pre><p>编译文本节点到此为止了,接下来我们来看编译元素节点的方法。</p><h4>编译元素节点</h4><h5>指令</h5><p>首先,让我们想一下,我们编译元素节点无非是想要根据元素节点上的指令来分别执行不同的操作,所以我们编译元素节点就只需要判断是否含有相关指令即可,这里我们只考虑了<code>v-text,v-model,v-on:click</code>这三个指令。让我们来看看compileElement方法吧。</p><pre><code class="js">compileElement(node){
//指令不就是一堆属性吗,所以我们只需要获取属性即可
const attrs = node.attributes;
if(attrs.length){
Array.from(attrs).forEach(attr => {
//这里由于我们拿到的attributes可能包含不是指令的属性,所以我们需要先做一次判断
if(this.isDirective(attr)){
//根据v-来截取一下后缀属性名,例如v-on:click,subStr(5)即可截取到click,v-text与v-model则subStr(2)截取到text和model即可
let attrName = attr.indexOf(':') > -1 ? attr.subStr(5) : attr.subStr(2);
let key = attr.value;
//单独定义一个update方法来区分这些
this.update(node,attrName,key,this.vm[key]);
}
})
}
}</code></pre><p>这里又涉及到了一个<code>isDirective</code>辅助方法,我们可以使用<code>startsWith</code>方法,判断是否含有<code>v-</code>值即可认定这个属性就是一个指令。如下:</p><pre><code class="js">isDirective(dir){
return dir.startsWith('v-');
}</code></pre><p>接下来,我们来看最后的<code>update</code>方法。如下:</p><pre><code class="js">update(node,attrName,key,value){
//后续代码
}</code></pre><p>最后,让我们来思考一下,我们update里面需要做什么。很显然,我们是不是需要判断是哪种指令来执行不同的操作?如下:</p><pre><code class="js">//update函数内部
if(attrName === 'text'){
//执行v-text的操作
}else if(attrName === 'model'){
//执行v-model的操作
}else if(attrName === 'click'){
//执行v-on:click的操作
}</code></pre><h5>v-text指令</h5><p>好的,我们知道,根据前面的编译文本元素节点的方法,我们就知道这个指令的用法同前面编译文本元素节点。所以这个判断里面就好写了,如下:</p><pre><code class="js">//attrName === 'text'内部
node.textContent = value;
new Watcher(this.vm,key,newValue => {
node.textContent = newValue;
})</code></pre><h5>v-model指令</h5><p>v-model指令实现的是双向绑定,我们都知道双向绑定是更改输入框的value值,并且通过监听input事件来实现。所以这个判断,我们也很好写了,如下:</p><pre><code class="js">//attrName === 'model'内部
node.value = value;
new Watcher(this.vm,key,newValue => {
node.value = newValue;
});
node.addEventListener('input',(e) => {
this.vm[key] = node.value;
})</code></pre><h5>v-on:click指令</h5><p>v-on:click指令就是将事件绑定到methods内定义的函数,为了确保this指向当前组件实例,我们需要通过bind方法改变一下this指向。如下:</p><pre><code class="js">//attrName === 'click'内部
node.addEventListener(attrName,this.methods[key].bind(this.vm));</code></pre><p>到此为止,我们一个mini版本的vue2.x就算是实现了。继续下一节,学习vue3.x版本的mini实现吧。</p><h2>mini版本的vue.js3.x框架</h2><h3>模板代码</h3><p>首先我们看一下我们要实现的模板代码:</p><pre><code class="html"><div id="app"></div></code></pre><h3>逻辑代码</h3><p>然后就是我们要编写的javascript代码。</p><pre><code class="js">const App = {
$data:null,
setup(){
let count = ref(0);
let time = reactive({ second:0 });
let com = computed(() => `${ count.value + time.second }`);
setInterval(() => {
time.second++;
},1000);
setInterval(() => {
count.value++;
},2000);
return {
count,
time,
com
}
},
render(){
return `
<h1>How reactive?</h1>
<p>this is reactive work:${ this.$data.time.second }</p>
<p>this is ref work:${ this.$data.count.value }</p>
<p>this is computed work:${ this.$data.com.value }</p>
`
}
}
mount(App,document.querySelector("#app"));</code></pre><h3>运行效果</h3><p>我们来看一下实际运行效果如下所示:</p><p><img src="/img/bVcSZz3" alt="" title=""></p><p>思考一下,我们要实现如上的功能应该怎么做呢?你也可以单独打开以上示例:</p><p><a href="https://link.segmentfault.com/?enc=%2FugHp0w%2FRI9JV2Z1UZ3v9g%3D%3D.lAHx7EQE%2BfW5cKwrjCefUBOJjB7MK%2Bwc6wzIA19scqmTbvTCFM3oG1%2Bah5%2BtjZNELs0DzyAtbqS7cW%2Fw9FJ67%2BmACipBpTqglXkz%2FiVgpZ4%3D" rel="nofollow">点击此处</a>。</p><h2>源码实现-3.x</h2><h3>与vue2.x做比较</h3><p>事实上,vue3.x的实现思想与vue2.x差不多,只不过vue3.x的实现方式有些不同,在vue3.x,把收集依赖的方法称作是副作用<code>effect</code>。vue3.x更像是函数式编程了,每一个功能都是一个函数,比如定义响应式对象,那就是reactive方法,再比如computed,同样的也是computed方法...废话不多说,让我们来看一下吧!</p><h3>reactive方法</h3><p>首先,我们来看一下vue3.x的响应式方法,在这里,我们仍然只考虑处理对象。如下:</p><pre><code class="js">function reactive(data){
if(!isObject(data))return;
//后续代码
}</code></pre><p>接下来我们需要使用到es6的<strong>proxy</strong>API,我们需要熟悉这个API的用法,如果不熟悉,请点击<a>此处</a>查看。</p><p>我们还是在getter中收集依赖,setter中触发依赖,收集依赖与触发依赖,我们都分别定义为2个方法,即track和trigger方法。如下:</p><pre><code class="js">function reactive(data){
if(!isObject(data))return;
return new Proxy(data,{
get(target,key,receiver){
//反射api
const ret = Reflect.get(target,key,receiver);
//收集依赖
track(target,key);
return isObject(ret) ? reactive(ret) : ret;
},
set(target,key,val,receiver){
Reflect.set(target,key,val,receiver);
//触发依赖方法
trigger(target,key);
return true;
},
deleteProperty(target,key,receiver){
const ret = Reflect.deleteProperty(target,key,receiver);
trigger(target,key);
return ret;
}
})
}</code></pre><h3>track方法</h3><p>track方法就是用来收集依赖的。我们用es6的weakMap数据结构来存储依赖,然后为了简便化用一个全局变量来表示依赖。如下:</p><pre><code class="js">//全局变量表示依赖
let activeEffect;
//存储依赖的数据结构
let targetMap = new WeakMap();
//每一个依赖又是一个map结构,每一个map存储一个副作用函数即effect函数
function track(target,key){
//拿到依赖
let depsMap = targetMap.get(target);
// 如果依赖不存在则初始化
if(!depsMap)targetMap.set(target,(depsMap = new Map()));
//拿到具体的依赖,是一个set结构
let dep = depsMap.get(key);
if(!dep)depsMap.set(key,(dep = new Set()));
//如果没有依赖,则存储再set数据结构中
if(!dep.has(activeEffect))dep.add(activeEffect)
}</code></pre><p>收集依赖就这么简单,需要注意的是,这里涉及到了es6的三种数据结构即WeakMap,Map,Set。下一步我们就来看如何触发依赖。</p><h3>trigger方法</h3><p>trigger方法很明显就是拿出所有依赖,每一个依赖就是一个副作用函数,所以直接调用即可。</p><pre><code class="js">function trigger(target,key){
const depsMap = targetMap.get(target);
//存储依赖的数据结构都拿不到,则代表没有依赖,直接返回
if(!depsMap)return;
depsMap.get(key).forEach(effect => effect && effect());
}</code></pre><p>接下来,我们来实现一下这个副作用函数,也即effect。</p><h3>effect方法</h3><p>副作用函数的作用也很简单,就是执行每一个回调函数。所以该方法有2个参数,第一个是回调函数,第二个则是一个配置对象。如下:</p><pre><code class="js">function effect(handler,options = {}){
const __effect = function(...args){
activeEffect = __effect;
return handler(...args);
}
//配置对象有一个lazy属性,用于computed计算属性的实现,因为计算属性是懒加载的,也就是延迟执行
//也就是说如果不是一个计算属性的回调函数,则立即执行副作用函数
if(!options.lazy){
__effect();
}
return __effect;
}</code></pre><p>副作用函数就是如此简单的实现了,接下来我们来看一下computed的实现。</p><h3>computed的实现</h3><p>既然谈到了计算属性,所以我们就定义了一个computed函数。我们来看一下:</p><pre><code class="js">function computed(handler){
// 只考虑函数的情况
// 延迟计算 const c = computed(() => `${ count.value}!`)
let _computed;
//可以看到computed就是一个添加了lazy为true的配置对象的副作用函数
const run = effect(handler,{ lazy:true });
_computed = {
//get 访问器
get value(){
return run();
}
}
return _computed;
}</code></pre><p>到此为止,vue3.x的响应式算是基本实现了,接下来要实现vue3.x的mount以及compile。还有一点,我们以上只是处理了引用类型的响应式,但实际上vue3.x还提供了一个ref方法用来处理基本类型的响应式。因此,我们仍然可以实现基本类型的响应式。</p><h3>ref方法</h3><p>那么,我们应该如何来实现基本类型的响应式呢?试想一下,为什么vue3.x中定义基本类型,如果修改值,需要修改xxx.value来完成。如下:</p><pre><code class="js">const count = ref(0);
//修改
count.value = 1;</code></pre><p>从以上代码,我们不难得出基本类型的封装原理,实际上就是将基本类型包装成一个对象。因此,我们很快可以写出如下代码:</p><pre><code class="js">function ref(target){
let value = target;
const obj = {
get value(){
//收集依赖
track(obj,'value');
return value;
},
set value(newValue){
if(value === newValue)return;
value = newValue;
//触发依赖
trigger(obj,'value');
}
}
return obj;
}</code></pre><p>这就是基本类型的响应式实现原理,接下来我们来看一下mount方法的实现。</p><h3>mount方法</h3><p>mount方法实现挂载,而我们的副作用函数就是在这里执行。它有2个参数,第一个参数即一个vue组件,第二个参数则是挂载的DOM根元素。所以,我们可以很快写出以下代码:</p><pre><code class="js">function mount(instance,el){
effect(function(){
instance.$data && update(instance,el);
});
//setup返回的数据就是实例上的数据
instance.$data = instance.setup();
//这里的update实际上就是编译函数
update(instance,el);
}</code></pre><p>这样就是实现了一个简单的挂载,接下来我们来看一下编译函数的实现。</p><h3>update编译函数</h3><p>这里为了简便化,我们实现的编译函数就比较简单,直接就将定义在组件上的render函数给赋值给根元素的<code>innerHTML</code>。如下:</p><pre><code class="js">//这是最简单的编译函数
function update(instance,el){
el.innerHTML = instance.render();
}</code></pre><p>如此一来,一个简单的mini-vue3.x就这样实现了,怎么样,不到100行代码就搞定了,还是比较简单的。</p><p><a href="https://link.segmentfault.com/?enc=m5pK38V3DY0ZrU7ECCDW6w%3D%3D.vfgGJxraxnlbWoUkrZDh6WJtDQz29KqJU7RX8p%2BHYcRtoTDFA44HyQc3%2BrvrCQYJowAATX36rFbOWntXqhWc3w%3D%3D" rel="nofollow">详细文档收录网站</a>。</p>
相逢即是缘丨我与思否不得不说的故事
https://segmentfault.com/a/1190000040231540
2021-06-24T10:51:02+08:00
2021-06-24T10:51:02+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
4
<p>第一次加入思否还是因为问题而进入的,不得不说思否的问答做的很棒,有很多热心的大佬解决问题,所以我被思否深深的吸引了,也加入了回答问题的大军,我的想法很简单,能把我所理解到的解答出来,对我来说即是一种提升。</p><p>后来,又知道了思否的写作功能,然后我开始写文章记录我在平时工作中遇到的问题,又或者是一些知识点总结,当然不得不承认,由于一些意外因素,导致我不得不中断一些文章的写作,这是我比较大的失败!例如:</p><p><a href="https://segmentfault.com/a/1190000017099496">从零开始学习vue</a><br><a href="https://segmentfault.com/a/1190000018233346">算法入门</a></p><p>这两篇文章,算法入门这篇文章是我还没有深入理解到散列表,所以我没有续写,从零开始学习vue是因为我感觉这样写的意义不是很大,所以中断了。</p><p>再到后来,很荣幸加入了思否的讲师,感谢思否给了我这个平台让我提升自己。以下是以上线的课程:</p><p><a href="https://ke.segmentfault.com/course/1650000039393407">玩转typescript之验证登录</a><br><a href="https://ke.segmentfault.com/course/1650000039798692">玩转typescript之电影选座</a></p><p>当然还有未上线的课程,例如:</p><p>vuex源码分析<br>手写一个mini-compiler<br>实现我的个人项目颜色选择器</p><p>这些课程都在陆续筹划中,也欢迎大家多多支持。最后,还是祝思否9周年节日快乐,哈哈,借用一下大佬的文章<a href="https://segmentfault.com/a/1190000040220161">多种语言祝思否生日快乐</a>。</p><p>C:</p><pre><code class="c">#include<stdio.h>
int main(void)
{
printf("生日快乐");
return 0;
}</code></pre><p>python:</p><pre><code class="python">print("生日快乐")</code></pre><p>java:</p><pre><code class="java"> public static void main(String[] args) {
System.out.println("生日快乐");
}</code></pre><p>C++:</p><pre><code class="C++">#include<iostream>
using namespace std;
int main(){
cout<<"生日快乐";
return 0;
}</code></pre><p>C#:</p><pre><code class="C#">Console.Write("生日快乐");</code></pre><p>JavaScript:</p><pre><code class="JS">console.log("生日快乐");</code></pre><p>PHP:</p><pre><code class="PHP"><?php
echo "<h2>生日快乐</h2>";
?></code></pre><p>Objective-C:</p><pre><code class="Objective-C">NSLog(@"%@",@"生日快乐");</code></pre><p>Dart:</p><pre><code class="dart">void main() {
print("生日快乐");
}</code></pre><p>Kotlin:</p><pre><code class="Kotlin">println("生日快乐");</code></pre><p>scala:</p><pre><code class="scala">print("生日快乐");</code></pre><p>typescript:</p><pre><code class="typescript">console.log("生日快乐");</code></pre><p>以下为补充的:</p><p>Rust:</p><pre><code class="rust">println!("生日快乐");</code></pre><p><a href="https://segmentfault.com/a/1190000040098074">本文参与了 SegmentFault 思否征文「思否9周年」,欢迎正在阅读的你一起加入。</a></p>
记录一次蚂蚁金服的前端面试经历
https://segmentfault.com/a/1190000039879036
2021-04-22T20:30:22+08:00
2021-04-22T20:30:22+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
7
<p>坐标成都,大专,4年前端,分享一次自己在蚂蚁金服的面试经历。</p><h3>一面</h3><ul><li>1.自我介绍</li></ul><p>答:我是XXX,经历。。。。</p><ul><li>2.vue data响应式的实现</li></ul><p>答:啪啦啪啦说了一大堆,感觉我很兴奋。</p><ul><li>3.vue computed的实现</li></ul><p>答:好吧到这里我就卡住了,因为我当时比较紧张了,尽管我内心不停的劝告自己,然后脑海一片空白。</p><ul><li>4.你在css方面的擅长?</li></ul><p>答:我擅长的是css中的定位。</p><ul><li>5.请详细介绍下定位?</li></ul><p>答:static,fixed,relative,absolute,sticky,inherit。然后就它们的用法分别做了介绍。</p><ul><li>6.你知道浏览器缓存吗?</li><li>7.你还有什么想问的吗?<br>略。</li></ul><h3>二面</h3><ul><li>1.webpack的配置方式和编译过程。</li></ul><p>答:这个题比较大,我感觉我答的也不是很好。</p><ul><li>2.解析URL参数。</li></ul><p>我当时的思路就是这样,通过截取到字符串后面的参数,然后通过正则去进行匹配,分别匹配参数名和参数值,它们一定是匹配到二个数组,然后遍历其中一个数组,就可以了。以下是我面试完之后根据思路实现的完整代码,当然面试的时候是肯定写不到这么完整的。</p><pre><code class="js"> var getURLParam = function(url){
let res = {};
if(url.lastIndexOf("?") === -1)return res;
let param = decodeURIComponent(url.slice(url.lastIndexOf("?") + 1));
let keys = param.match(/\w+\=|\=/g);
keys && (keys = keys.map(k => k.replace(/\=/g,"")));
let values = param.match(/(\=(\w+|\s*)\&)|(\=(\w+|\s*))/g);
values && (values = values.map(v => v.replace(/\&|\=/g,"")));
keys && keys.forEach((k,i) => res[k] = values[i]);
return res;
}</code></pre><ul><li>3.实现通用的批量更新策略。</li></ul><pre><code class="js"> let notifyFn;
function fn1(){
notifyFn();
statement1;
statement2;
}
function fn2(){
statement3;
notifyFn();
statement4;
}
function fn3(){
statement5;
statement6;
notifyFn();
}
async function onMount(){
notifyFn = update(() => {
//在statement6执行完之后执行
})
fn1();
await Promise.resolve();
fn2();
await Promise.resolve();
fn3();
}</code></pre><p>这道题,我第一次看到的时候是一脸懵逼的状态,完全看不懂这道题的考察点是什么,尽管我问了一下面试官,面试官给我讲了一番,我还是没有听明白。面试官说我们先跳过这一道题,来分析下一道题。</p><ul><li>4.React基于单向数据流。对于组件间的通信支持不够好。现需要模拟一个全局的EventStore。使得可以满足以下条件,以支持组件间的通信。</li></ul><pre><code class="js"> class Event {
}</code></pre><pre><code class="js">// 用法
const loader = new Event();
loader.bind("loaded",event => console.log(event));//注册事件
loader.trigger("loaded",{ data:"data" });//触发事件
loader.unbind("loaded");//注销事件</code></pre><p>这道题,我看着就像是实现一个事件派发器,也给面试官说了自己的实现思路。</p><ul><li>5.你还有什么想问的吗?</li></ul><p>略。</p><p>一面我其实还是做足了准备的,二面由于我自己都觉得我的表现不是很好,所以很显然二天后的我就收到了面试没通过的邮件,我详细分析了一番,最后做出如下总结。</p><h3>面试总结:</h3><p>我的二面太急切仓促,没做准备,然后二面面试也没有很好的表现出自己的亮点和能力,最后问问题一环我竟然还没问问题,只是要求面试官可不可以把题记录下来(我本意就想下来研究一番,尤其是第三题)。</p><p>事实上,除了第四题我算是以前写过一个类似的,其他题我都是没有准备过而临场发挥。因此,我掉分的关键不在于做题,而是我的表现。我直接读不懂题,然后面试官给我讲了一番,我仍然没有明白,也没有去思考为什么,所以可能这就让面试官以为我放弃了这道题。</p><p>整场二面,我在其他三道题上分析的还算中规中矩,但在第三道题,我就很慌张,没有表现出冷静思考的样子,所以这里应该就是我掉分的关键点。</p><p>整场蚂蚁金服面试就这样结束了,经过这一次面试也让我知道了自己的不足之处,那就是很容易紧张,心慌。</p><p>作为一个程序员,最应该有的态度,就是不停专研,不惧困难,遇事沉着冷静,临危不乱,才能更好的展现自己。而我并没有做到,所以我将在接下来的时间好好准备,充分提升自己,我想希望我能在半年后继续参加面试。</p><p>学无止境,路还很长,我还会有很长的路要走。面试过后,经过自己的总结和反思,我终于明白面试官想要考察的不是题的答案本身,而是我有没有解决问题的决心,有没有冷静分析问题的能力,还有自己的思考和理解。</p><p>我每天也在练习一道算法题,督促自己好好学习,并记录下来自己的思路,然后整理成了文档网站,欢迎查看<a href="https://link.segmentfault.com/?enc=RMG65yaEa1vNVwTLcn5iwg%3D%3D.hfM8Wk4eBKG2rRufNkVkjCkuExkamVYA8faOOm8PEDxlTQFGYq8O2iUBWv28fFmZ" rel="nofollow">剑指offer算法题</a>。</p><p>我的开源项目一个使用原生JavaScript编写的颜色选择器,灵活自定义扩展,修改配置对象还能自动更新<a href="https://link.segmentfault.com/?enc=S18m%2FrIDPCQHdHTOzPasMw%3D%3D.ssFZouZmwGxDIhytOcsSFLS08VnAKy5pyHejrzI0iy6fZ76D2IxOIsoEAv4UV4N0" rel="nofollow">ew-color-picker</a>。</p><p>我的文档已经完善了不少,详细介绍了各个api的使用,希望能够我提issue,这个项目还有很多不完善的地方。<a href="https://link.segmentfault.com/?enc=gf0bfy7uteX4Jp0OuHK0ug%3D%3D.8EOo94aTbFPekE6E1tjOfbeJ3deHguqJZtclrBzkD%2BHH2AATHW%2B%2FezpV6yZhY2h9" rel="nofollow">ewColorPicker文档网站</a>。</p><p>我在思否录制上线的课程<a href="https://ke.sifou.com/course/1650000039393407?utm_source=recommend_web-live-new">玩转typescript1</a>,<a href="https://ke.segmentfault.com/course/1650000039798692#nav-live-intro">玩转typescript2</a>适用于有一定基础的前端,还望大家多多支持,谢谢。</p>
深入JavaScript中的this对象
https://segmentfault.com/a/1190000039757880
2021-04-01T16:14:04+08:00
2021-04-01T16:14:04+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
9
<h2>this 对象详解</h2><p>this关键字是函数当中最重要的一个知识点。它在JavaScript中的表现也会有一些细微的不同,在严格和非严格模式之下也会有一些差别。</p><p>绝大多数情况下,this的指向由函数的调用方式决定。它不能被赋值,并且每次函数调用,它也有可能会不同。<code>ES5</code>引入了<code>bind</code>方法来设置函数的<code>this</code>值,而不需要考虑函数的调用方式,<code>ES6</code>的箭头函数不提供自身的<code>this</code>绑定,它的<code>this</code>由当前上下文决定。</p><pre><code class="js"> const obj = {
name:"hello,world!",
getName(){
return this.name;
}
}
console.log(obj.getName());//"hello,world!"</code></pre><h3>语法:</h3><pre><code class="js"> this</code></pre><h3>值</h3><p>它的值是当前上下文(global,function,eval)中的一个属性,在非严格模式下,它总是指向一个对象,而在严格模式下,它可以被设置成任意值。</p><h3>描述</h3><h4>全局上下文</h4><p>全局上下文即全局对象,例如在浏览器环境当中,this始终指的是window对象,不论是否是严格模式。来看如下一个示例:</p><pre><code class="js"> //在浏览器环境中,window对象就是全局对象
console.log(this === window);//true
//不用标识符定义一个变量,也会自动将该变量添加到window对象中,作为window对象的一个属性
a = 250;
console.log(this.a);//250
this.message = "hello,world!";
console.log(message);
console.log(window.message);
//都是打印的"hello,world!"</code></pre><blockquote>笔记:可以始终使用<code>globalThis</code>来获取一个全局对象,无论你的代码是否在当前上下文运行。</blockquote><pre><code class="js"> var obj = {
func:function(){
console.log(this);
console.log(globalThis);
}
}
obj.func();//先打印obj对象,再打印window对象,浏览器环境中</code></pre><h4>函数上下文</h4><p>在函数内部,this取决于它被调用的方式。例如以下的非严格模式下,没有手动去通过设置调用方式,并且是在全局环境下调用的,所以this指向全局对象。</p><pre><code class="js"> function fn(){
return this;
}
//在浏览器环境中
console.log(fn() === window);//true
//在node.js环境中
console.log(fn() === globalThis);//true</code></pre><p>然而,在严格模式下,如果没有为this设置值,那么this会保持为undefined。如:</p><pre><code class="js"> function fn(){
'use strict';
return this;
}
console.log(fn() === undefined) //true</code></pre><blockquote>tips:上例中,因为fn是直接被调用的,也就是并不是作为对象的属性来调用(window.fn),所以this应是undefined,有些浏览器在最初支持严格模式的时候并没有正确的实现这个功能,所以错误的返回了window对象。</blockquote><p>如果想要改变this值,需要使用<code>call</code>或<code>apply</code>方法。如例:</p><pre><code class="js"> var obj = { value:"this is custom object!"};
var value = "this is global object!";
var getThis = function(){
return this.value;
}
console.log(getThis());//"this is global object!"
console.log(getThis.apply(obj))//"this is custom object!"
console.log(getThis.call(obj))//"this is custom object!"</code></pre><h4>类上下文</h4><p>尽管<code>ES6</code>的类和函数有些相似,this的表现也会类似,但也有一些区别和注意事项。</p><p>在类当中,this就是一个常规的类对象,类里面定义的非静态的方法都会被添加到this对象的原型当中。例:</p><pre><code class="js"> class Test {
constructor(){
const p = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(p));
}
getName(){}
getValue(){}
static getNameAndValue(){}
}
new Test();//["constructor","getName","getValue"]</code></pre><blockquote>tips:静态方法不是this的属性,它们只是类自身的属性。</blockquote><p>比如,我们要调用以上的<code>getNameAndValue</code>方法,我们可以像如下这样调用:</p><pre><code class="js"> Test.getNameAndValue();
//或者
const test = new Test();
test.constructor.getNameAndValue();</code></pre><h4>派生类</h4><p>在派生类当中,不会像基类那样,有初始的绑定。什么是派生类?也就是继承基类的类。例如:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {}
//这里的test就是一个派生类</code></pre><p>在派生类的构造函数当中,如果不使用super绑定this,则在使用this的过程中会报错<code>Must call super constructor in derived class before accessing 'this' or returning from derived constructor</code>。大致意思就是要有一个super绑定。如:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
console.log(this);
}
}
//ReferenceError</code></pre><p>但是如果我们稍微改一下,如下:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
super();//这时候会生成一个this绑定
console.log(this);
}
}
//Test,继承了基类的属性和方法,相当于执行this = new Base()</code></pre><p>派生类不能在没有super方法的构造函数中返回一个除对象以外的值,或者说是有super方法的前面直接返回一个对象以外的值也是不行的,除非根本就没有构造函数。如:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
return 1;
super();
}
}
//TypeError</code></pre><p>但是下面的示例不会出错:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
return {};
super();
}
}</code></pre><p>下面示例会报错:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
return 1;
}
}
//TypeError</code></pre><p>下面示例不会报错:</p><pre><code class="js">class Base {
constructor(){
this.key = "base";
}
}
class Test extends Base {
constructor(){
return {};
}
}</code></pre><h4>this和对象之间的转换</h4><p>在非严格模式下,如果调用call或apply方法,传入的第一个参数,也就是被用作this的值不是一个对象,则会尝试被转换为对象。基本类型值,如null何undefined会被转换成全局对象,而像其他的基本类型值则会使用对应的构造函数来转换成对象。例如number类型数字1就会调用new Number(1),string类型'test'就会调用new String('test')。</p><p>例如:</p><pre><code class="js">function sum(c,d){
return this.a + this.b + c + d;
}
var a = 3,b = 4;
var count = {
a:1,
b:2
}
//call方法后面的参数直接被用作函数的参数
console.log(sum.call(count,3,4));//10
console.log(sum.call(count,'3',4))//'334'
console.log(sum.call(null,3,4));//14
console.log(sum.call(undefined,'3',4));//'734'
console.log(sum.call(1,3,4));//new Number(1)上没有a和b属性,所以是this.a + this.b就是NaN,即两个undefined相加
console.log(sum.call('',1,'2'))//'NaN2'
//apply方法参数只能传数组参数
//TypeError
// console.log(sum.apply(count,3,4));
// console.log(sum.apply(count,'3',4))
// console.log(sum.apply(null,3,4));
// console.log(sum.apply(undefined,'3',4));
// console.log(sum.apply(1,3,4));
// console.log(sum.apply('',1,'2'))
//必须这样传
console.log(sum.apply(count,[3,4]));//10
console.log(sum.apply(count,['3',4]))//'334'
console.log(sum.apply(null,[3,4]));//14
console.log(sum.apply(undefined,['3',4]));//'734'
console.log(sum.apply(1,[3,4]));//new Number(1)上没有a和b属性,所以是this.a + this.b就是NaN,即两个undefined相加
console.log(sum.apply('',[1,'2']))//'NaN2'</code></pre><p>再来看一个示例如下:</p><pre><code class="js">function test(){
console.log(Object.prototype.toString.call(this))
}
console.log(test.call(7));//[object Number]
console.log(test.call(undefined));//[object global],在浏览器环境下指向为[Object window]
console.log(test.apply('123'));//[object String]</code></pre><p>根据以上示例,我们就可以知道了利用Object.prototype.toString方法来判断一个对象的类型。如可以封装一个函数如下:</p><pre><code class="js">function isObject(value){
return Object.prototype.toString.call(value) === '[object Object]';
}
//等价于
function isObject(value){
return Object.prototype.toString.apply(value) === '[object Object]';
}
//等价于
function isObject(value){
return {}.toString.call(value) === '[object Object]';
}
//等价于
function isObject(value){
return {}.toString.apply(value) === '[object Object]';
}</code></pre><h4>bind方法</h4><p><code>ES5</code>引入了<code>bind</code>方法,该方法为<code>Function</code>的原型对象上的一个属性,在一个函数<code>fn</code>中调用<code>fn.bind(object)</code>将会创建一个和该函数相同作用域以及相同函数体的函数,但是它的<code>this</code>值将被绑定到<code>bind</code>方法的第一个参数,无论这个新创建的函数以什么方式调用。如:</p><pre><code class="js">function fn(){
var value = "test";
return this.value;
}
var obj = {
value:"objName"
}
var newFn = fn.bind(obj);
console.log(fn.bind(obj)());//objName
console.log(newFn());//objName
var bindObj = {
value:"bind",
f:fn,
g:newFn,
h:fn.bind(bindObj)
}
var newBind = {
a:fn.bind(bindObj)
}
console.log(bindObj.f());//bind
console.log(bindObj.g());//objName
console.log(bindObj.h());//undefined
console.log(newBind.a());//bind</code></pre><h4>箭头函数</h4><p>在箭头函数中,this与封闭环境当中的上下文的this绑定一致,在全局环境中,那它的this就是全局对象。如:</p><pre><code class="js">var obj = {
a:() => {
return this;
},
b:function(){
var x = () => { return this;};
return x();
}
}
console.log(obj.a());//global
console.log(obj.b());//obj</code></pre><blockquote>注意:无论使用call,apply还是bind其中的哪一种方法,都不能改变箭头函数的this指向,因为都将被忽略,但是仍然可以传递参数,理论上第一个参数设置为null或者undefined为最佳实践。</blockquote><p>如:</p><pre><code class="js"> //在浏览器环境下globalObject是window对象
let globalObject = this;
let getThis = () => this;
console.log(getThis() === globalObject);//true
let obj = {
getThis:getThis
}
console.log(obj.getThis() === globalObject);//true
console.log(obj.getThis.call(obj) === globalObject);//true
console.log(obj.getThis.apply(obj) === globalObject);//true
// 使用bind并未改变this指向
console.log(obj.getThis.bind(obj)() === globalObject);//true</code></pre><p>也就是说,无论如何,箭头函数的this都指向它的封闭环境中的this。如下:</p><pre><code class="js">var obj = {
a:() => {
return this;
},
b:function(){
var x = () => { return this;};
return x();
}
}
console.log(obj.a());//global在浏览器环境下是window对象
console.log(obj.b());//obj</code></pre><h4>作为某个对象</h4><p>当调用某个对象中的函数中的方法时,在访问该函数中的this对象,将会指向这个对象。例如:</p><pre><code class="js"> var value = "this is a global value!";
var obj = {
value:"this is a custom object value!",
getValue:function(){
return this.value;
}
}
console.log(obj.getValue());//"this is a custom object value!"</code></pre><p>这样的行为方式完全不会受函数定义的方式和位置影响,例如:</p><pre><code class="js"> var value = "this is a global value!";
var obj = {
value:"this is a custom object value!",
getValue:getValue
}
function getValue(){
return this.value;
}
console.log(obj.getValue());//"this is a custom object value!"</code></pre><p>此外,它只受最接近的引用对象的影响。如:</p><pre><code class="js"> var value = "this is a global value!";
var obj = {
value:"this is a custom object value!",
getValue:getValue
}
obj.b = {
value:"this is b object value!",
getValue:getValue
}
function getValue(){
return this.value;
}
console.log(obj.b.getValue());//"this is b object value!"</code></pre><h4>对象原型链中的this</h4><p>在对象的原型链中,this同样也指向的是调用这个方法的对象,实际上也就相当于该方法在这个对象上一样。如:</p><pre><code class="js"> var obj = {
sum:function(){
return this.a + this.b;
}
}
var newObj = Object.create(obj);
newObj.a = 1;
newObj.b = 2;
console.log(newObj.sum());//3
console.log(obj.sum());//NaN</code></pre><p>上例中,newObj对象继承了obj的sum方法,并且我们未newObj添加了a和b属性,如果我们调用newObj的sum方法,this实际上指向的就是newObj这个对象,所以我们可以得到结果为3,但是我们调用obj.sum方法的时候,this指向的是obj,obj对象并没有a和b属性,所以也就是两个undefined相加,就会是NaN。obj就作为了newObj的原型对象,这也是原型链当中的一个非常重要的特点。</p><blockquote>注意:Object.create()方法表示创建一个新对象,会以第一个参数作为新对象的原型对象,第一个参数只能为null或者新对象,不能为其它基本类型的值,如undefined,1,''等。</blockquote><h4>getter或setter中的this</h4><p>在一个对象的<code>setter</code>或者<code>getter</code>中同样的this指向设置或者获取这个属性的对象。如:</p><pre><code class="js"> function average(){
return (this.a + this.b + this.c) / 3;
}
var obj = {
a:1,
b:2,
c:3
get sum:function(){
return this.a + this.b + this.c;
}
}
Object.defineProperty(obj,'average',{
get:average,
enumerable:true,
configurable:true
});
console.log(obj.average,obj.sum);//2,6</code></pre><h4>构造函数中的this对象</h4><p>当一个函数被当做构造函数调用时(使用new关键字),this指向的就是实例化的那个对象。</p><blockquote>注意:尽管构造函数返回的默认值就是this指向的那个对象,但是也可以手动设置成返回其它的对象,如果手动设置的值不是一个对象,则返回this对象。</blockquote><p>如:</p><pre><code class="js"> function C(){
this.a = 1;
}
var c1 = new C();
console.log(c1.a);//1
function C2(){
var obj = {
a:2
}
this.a = 3;
return obj;
}
var c2 = new C2();
console.log(c2.a);//2</code></pre><p>在上例中实例化的c2的构造函数C2中,由于手动的设置了返回的对象<code>obj</code>,所以导致<code>this.a = 3</code>这条语句被忽略,从而得到结果为2,就好像"僵尸"代码。当然也不能算是"僵尸"代码,因为实际上它是被执行了的,只不过对外部没有造成影响,所以可以被忽略。</p><h4>作为一个DOM事件处理函数</h4><p>当函数是一个DOM事件处理函数,它的this就指向触发事件的元素(有一些浏览器在使用非addEventListener动态添加函数时不遵守这个约定)。如:</p><pre><code class="js">function changeStyle(e){
console.log(this === e.currentTarget);//true
console.log(this === e.target);//true
//将背景色更改为红色
this.style.setProperty('background',"#f00");
}
// 获取文档中所有的DOM元素
var elements = document.getElementsByTagName('*');
for(let i = 0,len = elements.length;i < len;i++){
//为每个获取到的元素添加事件
elements[i].addEventListener('click',changeStyle,false);
}</code></pre><h4>内联事件中的this</h4><p>当在内联事件中调用函数时,this指向的就是这个元素。但只有最外层的代码才指向这个元素,如果是内部嵌套函数中没有指定this,则指向全局对象。如:</p><pre><code class="html"> <button type="button" onclick="document.writeln(this.tagName.toLowerCase())">clicked me</button>
<!-- 点击按钮会在页面中出现button --></code></pre><pre><code class="html"><button type="button" onclick="document.writeln((function(){return this})())">clicked me</button>
<!-- 在浏览器环境下页面会写入[object Window] --></code></pre><h4>在类中更改this绑定</h4><p>类中的this取决于如何调用,但实际上在开发当中,我们手动的去绑定this为该类实例是一个很有用的方式,我们可以在构造函数中去更改this绑定。如:</p><pre><code class="js">class Animal {
constructor(){
//利用bind方法让this指向实例化的类对象
this.getAnimalName = this.getAnimalName.bind(this);
}
getAnimalName(){
console.log("The animal name is ",this.animalName);
}
getAnimalNameAgain(){
console.log("The animal name is ",this.animalName);
}
get animalName(){
return "dog";
}
}
class Bird {
get animalName(){
return "bird";
}
}
let animal = new Animal();
console.log(animal.getAnimalName());//The animal name is dog;
let bird = new Bird();
bird.getAnimalName = animal.getAnimalName;
console.log(bird.getAnimalName());//The animal name is dog;
bird.getAnimalNameAgain = animal.getAnimalNameAgain;
console.log(bird.getAnimalNameAgain());//The animal name is bird;</code></pre><p>在这个示例中,我们始终将<code>getAnimalName</code>方法的this绑定到实例化的<code>Animal</code>类对象上,所以尽管在<code>Bird</code>类中定义了一个<code>animalName</code>属性,我们在调用<code>getAnimalName</code>方法的时候,始终得到的就是<code>Animal</code>中的<code>animalName</code>属性。所以第二个打印仍然是<code>dog</code>。</p><blockquote>注意:在类的内部总是使用的严格模式,所以调用一个this值为undefined的方法会抛出错误。</blockquote><p>打个广告,我在思否上线的课程<a href="https://ke.sifou.com/course/1650000039393407?utm_source=recommend_web-live-new">玩转typescript1</a>,<a href="https://ke.segmentfault.com/course/1650000039798692#nav-live-intro">玩转typescript2</a>适用于有一定基础的前端,还望大家多多支持,谢谢。</p>
一个灵活高度自定义的JavaScript颜色选择器
https://segmentfault.com/a/1190000039670041
2021-03-19T10:24:40+08:00
2021-03-19T10:24:40+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
28
<h2>ew-color-picker</h2><p>这是一个用<code>javascript</code>编写的灵活的,高度自定义的颜色选择器。</p><h2>使用场景</h2><p>这个颜色选择器适用于中小型项目,例如主题的切换。不同于组件库中的颜色选择器组件,它的配置自主化,根据用户的需求来自定义。</p><h2>优点</h2><p>html5 的原生颜色选择器样式不好看,而组件库的颜色选择器不够灵活多变,这样一来,就有了这个颜色选择器的诞生。</p><p>我们先来尝尝鲜,看看一个简单的示例:</p><pre><code class="html"><!-引入颜色选择器的css样式-->
<link
rel="stylesheet"
href="https://www.unpkg.com/ew-color-picker/dist/ew-color-picker.min.css"
/>
<!--引入插件JavaScript-->
<script src="https://www.unpkg.com/ew-color-picker/dist/ew-color-picker.min.js"></script></code></pre><p>然后在页面中放一个元素:</p><pre><code class="html"><div></div></code></pre><p>在<code>javascript</code>中,我们只需要如下代码:</p><pre><code class="js">const color = new ewColorPicker("div");</code></pre><p>如此一来,一个简单的颜色选择器就出现在页面上了。可能大多数人不大喜欢实例化的方式,那么我们也提供了一个方法来创建它:</p><pre><code class="js">const color = ewColorPicker.createColorPicker("div");</code></pre><p>这样也可以创建一个颜色选择器实例。</p><blockquote><p>tips:需要注意的就是,这些功能都是 1.6.7 版本加上的,所以请使用最新版本的 js,实际上,以上展示的引入链接会自动帮我们引入最新版本的 js,使用最新版本的 js,确保我们在使用当中不会出现 bug 以及使用新功能,只要我在,这个插件就会自动更新,只要能想到的东西,都会加上去。</p><p>tips:还需要说明一点的是,为了遵循一个颜色选择器对应一个实例,所以,当传入的 dom 元素是多个的话,也会取第一个 dom 元素来实例化。例如传的是<code>div</code>元素,如果页面中有多个 div 元素,那实际上在颜色选择器内部获取到的 div 元素就是多个,但始终都会取第一个 div 元素来实例化。如果想要实例化多个颜色选择器,我们则可以像如下代码那样使用</p></blockquote><pre><code class="js">const elements = document.querySelectorAll("div");
elements.forEach((item) => new ewColorPicker(item));</code></pre><p>我们也提供了一个方法<code>getDefaultConfig</code>来获取颜色选择器实例的默认配置对象。如下:</p><pre><code class="js">ewColorPicker.getDefaultConfig();</code></pre><blockquote>tips:还需要注意的就是,传入的 dom 元素不能是'html','head','body','meta','title','link','style','script'这些特殊的元素,否则插件会在控制台给出一个错误提示。</blockquote><p>tips:最新 1.7.1 版本允许添加到 body 元素中,当然还是不建议如此做,这个添加有些许 bug。</p><p>这都是最简单的用法,可能这样不太直观,请看如下一个简单的示例:</p><p><a href="https://codepen.io/eveningwater/pen/JjbKagQ">demo1</a></p><p>看到这里,也许会有人疑问,这怎么就灵活多变,高度自定义呢?别着急,让我们继续。</p><h2>自定义配置</h2><p>我们来看一个配置对象,如下所示:</p><pre><code class="js">{
el:'.demo2',//绑定选择器的dom元素
alpha:true,//是否开启透明度
hue:false,//是否开启色调
size:{
width:100,
height:50
},//颜色选择器类型,有四个字符串值normal,medium,small,mini或者一个对象自定义宽高,如果自定义宽高,最小宽高为25px
predefineColor:['#223456','rgba(122,35,77,.5)'],//预定义颜色是一个数组
disabled:false,//是否禁止所有的点击
defaultColor:'#eeff22',//默认颜色
pickerAnimation:'opacity',//或者'height',开启颜色选择器面板的动画
pickerAnimationTime:300,//动画执行时间,默认是200,最大动画时间为10000
sure:function(color){
console.log(color);
},//点击确定按钮的回调
clear:function(){
console.log(this)
},//点击清空按钮的回调
togglePicker:(el,flag,context) => {
console.log('当前根元素',el);
console.log('当前颜色选择器实例对象',context);
if(flag){
console.log("opened");
}else{
console.log("closed");
}
},//点击色块事件回调,需要注意该事件触发必须要将hasBox设置为true
isLog:false, //是否开启打印信息,默认是true如果不指定该值的话
changeColor:(color) => {
console.log('颜色值改变时触发:',color);
},
hasBox:true //默认为true,或者为false,表示是否显示颜色选择器
isClickOutside:true, //默认为true,或者设置为false,表示是否允许点击颜色选择器区域之外关闭颜色选择器
hasClear:true,//是否显示清空按钮,默认为true
hasSure:true, //是否显示确定按钮,默认为true,不建议设置为false
hasColorInput:true, //是否显示输入框,默认为true,不建议设置为false
boxDisabled:true,//默认是false,设置为true并且hasBox为true,禁止点击色块打开颜色选择器
openChangeColorMode:true,//是否打开颜色切换模式,注意打开这个模式必须要将alpha和hue设置为true
hueDirection:"horizontal",//或者vertical,默认是垂直布局显示,表示hue色阶柱是水平还是垂直布局显示
alphaDirection:"horizontal",//或者vertical,默认是垂直布局显示,表示透明度柱是水平还是垂直布局显示
lang:"zh",//或en,表示启用中文模式还是英文模式
clearText:"清空",//清空按钮文本,如果想要自定义该值,需要设置userDefineText为true
sureText:"确定",//确定按钮文本,如果想要自定义该值,则需要设置userDefineText为true
userDefineText:false,//默认为false,设置为true之后,lang属性的切换将无效
}</code></pre><p>我们先来分析第一个配置属性<code>hue</code>,或许我们看到一个完整的配置颜色选择器,应该是如下图所示:</p><p><img src="/img/remote/1460000039670044" alt="1616080898(1).jpg" title="1616080898(1).jpg"></p><p>我们来着重分析一下每一块代表什么:</p><p><img src="/img/remote/1460000039670045" alt="1.png" title="1.png"></p><p>根据上图分析,我们也知道了<code>hue</code>的属性就是控制最右边的色阶柱的显隐,显然默认是显示的。</p><blockquote>tips:如果是自定义配置,那么传入的元素在配置对象中就是 el 属性,例如,我们只需要一个颜色面板。那么我们可以编写如下代码:</blockquote><pre><code class="js">const color = new ewColorPicker({
el: "div",
hue: false,
});</code></pre><p>如此一来,我们就会得到如下所示的颜色选择器:</p><p><img src="/img/remote/1460000039670046" alt="2.png" title="2.png"></p><p>正如图中所示,就一个红色的面板可供选择,这确实是一个不好的选择,不过没关系,我们提供了<code>updateColor</code>方法去手动改变颜色值。代码如下:</p><pre><code class="js">//color为实例化的颜色选择器实例
color.updateColor("#0ff");</code></pre><p>当然,使用这个方法的前提是颜色选择器面板必须显示当中,并且传入的参数还要是一个正确格式的颜色,否则会在控制台给出错误提示,也不会生效。</p><p>请看如下一个示例:</p><p><a href="https://codepen.io/eveningwater/pen/eYBwzEK">demo</a></p><p>好,让我们接着来看第二个配置属性<code>alpha</code>,很显然这个属性是为了控制透明度柱子的显隐的,默认是不显示的。例如,我们可以这样修改:</p><pre><code class="js">const color = new ewColorPicker({
el: "div",
hue: false,
alpha: true,
});</code></pre><p>以上代码就会得到如下图所示的一个颜色选择器:</p><p><img src="/img/remote/1460000039670043" alt="3.png" title="3.png"></p><p>可能很多同学注意到了从前面的一个示例中设置了这样一个<code>hasBox</code>属性,它的默认值是<code>true</code>,很显然这个值是控制色块的显隐的,如果该值为<code>false</code>,那么颜色面板默认就会显示。所以,我们提供了两个方法<code>openPicker</code>和<code>closePicker</code>来手动控制颜色面板的关闭,(PS:点击目标元素区域之外关闭稍后再说)。如下所示:</p><pre><code class="js">//color为颜色选择器实例
color.openPicker(openPickerAni); //参数为即height或opacity两种字符串值,等同后续的openPickerAni配置属性
color.closePicker(openPickerAni);</code></pre><p>我们来看如下一个手动控制颜色选择器关闭的示例:</p><p><a href="https://codepen.io/eveningwater/pen/abBgZax">demo</a></p><p>在以上示例中,可能有人注意到了<code>isClickOutside</code>这个属性,没错这个属性也是一个布尔值,默认为<code>true</code>,表示点击颜色面板之外的区域,就会关闭颜色面板。来看如下的示例:</p><pre><code class="html"><div id="demo"></div>
<button type="button" id="openClickOutsideBtn">
开启|关闭目标区域元素点击事件
</button></code></pre><pre><code class="css">button {
float: right;
}</code></pre><pre><code class="js">const color = new ewColorPicker({
el: "#demo",
isClickOutside: false,
});
document.getElementById("openClickOutsideBtn").onclick = function () {
color1.config.isClickOutside = !color1.config.isClickOutside;
};</code></pre><p><a href="https://codepen.io/eveningwater/pen/oNYrpBx">demo</a></p><p>让我们继续,我们可以看到<code>size</code>属性,他的值可以是字符串值,也可以是对象。字符串值主要为这四个<code>normal,medium,small,mini</code>中的其中一个,或者也可以自定义一个对象<code>{ width:25;height:25 }</code>,默认值是<code>normal</code>。当然设置该值的前提是将<code>hasBox</code>属性设置为<code>true</code>,盒子元素都不显示,设置该值有什么用呢?后面的<code>openPickerAni</code>属性与<code>openPicker</code>方法也是同样的必须要将<code>hasBox</code>设置为<code>true</code>,这也是默认将该值设置为<code>true</code>的原因。让我们来看看如下一个示例:</p><pre><code class="html"><div></div>
<div></div>
<div></div>
<div></div>
<div></div></code></pre><pre><code class="js">const colors = document.querySelectorAll("div");
const colorConfigs = [
{
size: "normal",
},
{
size: "medium",
},
{
size: "small",
},
{
size: "mini",
},
{
size: {
width: 25,
height: 25,
},
},
];
colors.forEach((item, index) => {
new ewColorPicker({
el: item,
...colorConfigs[index],
});
});</code></pre><p>可以看到运行效果如下图所示:</p><p><img src="/img/remote/1460000039670049" alt="4.png" title="4.png"></p><p>让我们接着看下一个属性<code>predefineColor</code>,顾名思义,这个属性是一个数组,代表预定义颜色,每一个数组项必须是一个合格的颜色值,否则是不会渲染到颜色选择器上的。来看如下一个示例:</p><pre><code class="html"><div></div></code></pre><pre><code class="js">const color = new ewColorPicker({
el: "div",
predefineColor: ["#2396ef", "#fff", "rgba(134,11,12,1)", "#666"],
alpha: true,
});</code></pre><p>然后我们可以看到如下图所示:</p><p><img src="/img/remote/1460000039670050" alt="5.png" title="5.png"></p><p>让我们接着看下一个属性,<code>disabled</code>,这个属性的作用就是禁止点击盒子块元素打开颜色面板,也就是说如果<code>hasBox</code>为<code>false</code>的话,请忽略这个属性。</p><blockquote>tips:后续可能会考虑增加颜色面板的禁止点击事件等等。</blockquote><p>这个很简单没什么好说的,所以就不举例了。让我们接着来看下一个属性<code>defaultColor</code>,即默认显示的颜色值。如果检测到的颜色值不符合格式,则会在控制台给出错误提示,比如看这样一个示例:</p><pre><code class="html"><div></div></code></pre><pre><code class="js">const color = new ewColorPicker({
el: "div",
predefineColor: ["#2396ef", "#fff", "rgba(134,11,12,1)", "#666"],
alpha: true,
defaultColor: "#123",
});</code></pre><p>如下图所示:</p><p><img src="/img/remote/1460000039670048" alt="6.png" title="6.png"></p><blockquote>tips:或许这个检测颜色值是否合格的机制有些许问题,后续会优化。</blockquote><p>让我们接着来看下一个属性<code>openPickerAni</code>,它就只有两个值,和前面手动开启或关闭颜色选择器方法的参数值一样,这里就不必赘述,当然想要该属性生效不能将<code>hasBox</code>设置为<code>false</code>。</p><p>同样的<code>openPicker</code>也是针对<code>hasBox</code>为<code>true</code>而生效的,它是点击色块元素的回调,它有两个回调的参数。即<code>el</code>和<code>context</code>,也就是元素本身和元素本身的实例对象。</p><pre><code class="js">const color = new ewColorPicker({
el: "div",
openPicker: (el, context) => {
//可以通过context.config.pickerFlag来判断是打开还是关闭
},
});</code></pre><p>同理<code>clear</code>和<code>sure</code>就是清空按钮和确定按钮的回调,要让这两个回调生效,就不能将<code>hasClear</code>和<code>hasSure</code>设置为 false,因为这两个配置属性分别是空值清空和确定的显隐的。其中<code>hasClear</code>的回调参数为<code>defaultColor</code>,该值为空就为空,以及元素本身实例对象<code>context</code>。而<code>sure</code>的回调参数则是<code>color</code>值和元素本身实例对象。请看如下写法:</p><pre><code class="js">const color = new ewColorPicker({
el: "div",
clear: (defaultColor, context) => {
console.log(defaultColor, context);
},
sure: (color, context) => {
console.log(color, context);
},
});</code></pre><p>除了这两个回调之外,我们还额外增加了一个回调即<code>changeColor</code>函数,顾名思义,这个函数的作用就是当颜色值改变时触发,比如点击色阶柱改变色彩,点击透明度柱改变透明度等等。请看如下代码:</p><pre><code class="js">const color = new ewColorPicker({
el: "div",
changeColor: (color) => {
//颜色值只要改变时就触发,回调参数为改变后的颜色值
},
});</code></pre><p>还有一个<code>isLog</code>属性,这个属性的默认值是<code>true</code>,表示会在控制台打印一些信息,请忽略这个属性,啊哈哈,后续考虑将它的默认值设置为<code>false</code>。</p><p>最后一个就是<code>hasColorInput</code>属性,表示是否显示输入框,这在自定义输入框(比如和 element ui 的输入框绑定在一起)和颜色选择器绑定中十分有效,如果想要使用它,就不推荐设置为<code>false</code>。</p><p>我们来看一个示例如下:</p><pre><code class="html"><div></div></code></pre><pre><code class="js">const color = new ewColorPicker({
el: "div",
hasColorInput: false,
hasSure: false,
hasBox: false,
hasClear: false,
alpha: true,
});</code></pre><p>效果如下图所示:</p><p><img src="/img/remote/1460000039670047" alt="7.png" title="7.png"></p><p>目前最新版本为 1.6.8,后续还会考虑加更多的功能,只要你有需求,跟我提,我觉得合理就会加,如果觉得本颜色选择器可以帮助到你,还望给个<code>star</code>,<a href="https://link.segmentfault.com/?enc=%2FDK3aVr17BeUFczodc9%2FWg%3D%3D.EACFt6Wc97Jih1l%2Buu9J8QZWAVazoS76ttTO2IQCWdkzrKcFcY%2Ff9vnbOzDQUpxw" rel="nofollow">源码</a>。</p><p>更多描述可以参见<a href="https://link.segmentfault.com/?enc=HyJC4AjqgfIglEs6EQXmRg%3D%3D.emeakvyTNAlkrfreX2t4ym9BnMqkyVaMkX7umaUoHp5gQZt75iL80Q0cm91IS2r%2B" rel="nofollow">文档官网</a>和码云站点<a href="https://link.segmentfault.com/?enc=gdFmPnBvuPkkt456QCyqBg%3D%3D.U6g6Rk9w8ahQaz7fMAO6tjLXCSy1ruINjuvP%2Fg4ft8y4jUw8ns4bqvdmR1alBDRP" rel="nofollow">文档官网</a>。</p><blockquote>tips:如果 github 访问太慢,可以访问码云站点的官网。</blockquote><p>最后,后续有空的话,我会考虑写文章来分析这个颜色选择器的实现原理。</p><h2>更新日志</h2><p><a href="https://link.segmentfault.com/?enc=VUDH0UwjTSVMkhtYW02WGw%3D%3D.VVkkIiXlbmVs2ph8wJ%2BXO35V1VZCCsPYSnfth%2Bh%2FSdv2rkYWm%2Fxqq%2BPrK2e%2FvWz4" rel="nofollow">ew-color-picker</a></p><p>打个广告,我在思否上线的课程<a href="https://ke.sifou.com/course/1650000039393407?utm_source=recommend_web-live-new">玩转 typescript1</a>,<a href="https://ke.segmentfault.com/course/1650000039798692#nav-live-intro">玩转 typescript2</a>,<a href="https://ke.segmentfault.com/course/1650000040358952">从零开始实现一个mini版本的编译器</a>适用于有一定基础的前端,还望大家多多支持,谢谢。</p>
2天用vue3.0实现《掘金 - 2020年度人气创作者榜单》网站
https://segmentfault.com/a/1190000039129377
2021-01-30T10:17:01+08:00
2021-01-30T10:17:01+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
1
<p>初看到<a href="https://link.segmentfault.com/?enc=Mx4I5%2F8bDsy5CLMjRbTk%2Fg%3D%3D.p%2FcdH0WbzU0rq2scm9A%2BtHNCYDv6b5mEP5blldIFi6o%3D" rel="nofollow">掘金 - 2020年度人气创作者榜单</a>这个网站,感觉整体界面效果给我一种清爽的感觉,于是花了点时间琢磨如何实现。目前实现的功能有:列表展示,搜索,无限加载(与原网站有些区别,加了loading效果),活动介绍,tab切换。通过这些,我对vue3.0的composition api有了一定的认知,下面让我们来看看吧!</p><blockquote>ps:个人认为原网站应该是使用react.js写的</blockquote><p>直接请求该网站的数据接口,应该是会报跨域问题的。于是我想了一个办法,就是通过<code>node.js</code>来爬取数据。下面来看看代码:</p><h2>node后端爬取数据</h2><p>代码如下:</p><pre><code class="js">const superagent = require('superagent');
const express = require('express');
const app = express();
const port = 8081;
function isObject(value) {
return value && typeof value === 'object';
}
function getApi(url, params,method) {
return new Promise((resolve) => {
if (!isObject(params)) {
return resolve(setResponse(400, null, '请传入参数!'));
} else {
let paramMethod = method.toLowerCase() === 'post' ? 'send' : 'query';
superagent(method,url)[paramMethod](params).set('X-Agent', 'Juejin/Web').end((err, supRes) => {
if (err) {
return resolve(setResponse(400, null, err));
}
let data = JSON.parse(supRes.text);
resolve(setResponse(data.err_no === 0 ? 200 : data.err_no, data.data, data.err_msg));
});
}
})
}
app.use(express.json());
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
//允许的header类型
res.header("Access-Control-Allow-Headers", "content-type");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
if (req.method.toLowerCase() == 'options') {
res.send(200);
} else {
next();
}
});
function setResponse(code, data, message) {
return {
code: code,
data: data,
message: message
}
}
app.post('/info', (req, res) => {
const params = req.body;
getApi('https://api.juejin.cn/list_api/v1/annual/info', params,'post').then(data => {
res.send(JSON.stringify(data));
})
})
app.post('/list', (req, res) => {
const params = req.body;
getApi('https://api.juejin.cn/list_api/v1/annual/list', params,'post').then(data => {
res.send(JSON.stringify(data));
});
})
app.get('/user',(req,res) => {
const params = req.query;
getApi('https://api.juejin.cn/user_api/v1/user/get',params,'get').then(data => {
res.send(JSON.stringify(data));
})
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
</code></pre><p>以上只是爬了主要的三个接口,如<code>list</code>接口,<code>info</code>接口以及<code>user</code>接口。当然还有登录功能没有写,掘金应该是通过<code>cookie</code>技术去实现判断用户是否登录的,当从掘金打开,跳往该网站,会向浏览器的<code>cookie</code>存储用户相关登录信息。如下图所示:</p><p><img src="/img/remote/1460000039129379" alt="" title=""></p><p><img src="/img/remote/1460000039129383" alt="" title=""></p><p>这一个功能的实现思路知道即可,源码不会实现。然后在该网站去获取<code>cookie</code>并传递参数给<code>user</code>接口既可以获取登录相关信息。</p><p>以上代码思路也很简单,就是通过搭建一个本地服务器,然后爬取该网站的三个主要的接口,主要使用了<code>superagent</code>这个库来进行爬取。相关API可以参考<a href="https://link.segmentfault.com/?enc=2i3e2caizC3FvoBW1K2Cgg%3D%3D.U0%2BjTENUPjLPh%2FNp0VRxHqJG1LcPkEe%2FjvZtnMq3L3AxHFi%2B%2BNYhMhYHwcdDjAYT" rel="nofollow">superagent文档</a>。然后就是允许跨域的设置,用了<code>node</code>框架<code>express</code>。没什么技术难点。</p><h2>web前端</h2><p>技术点:vue3.0,typescript,vue-cli4.0,axios,less</p><p>首先分析一下页面,主要分为首页和活动介绍页。其中<code>Header</code>和<code>Footer</code>组件作为一个公共组件,这是毋庸置疑的。当然,这两个组件的代码也比较简单,可以不做分析。如下:</p><blockquote>Header</blockquote><pre><code class="ts"><template>
<div class="header">
<div class="header-logo"></div>
<div class="header-screen"></div>
<div class="header-cascade"></div>
<div class="header-person"></div>
<div class="header-python"></div>
<div class="header-vue"></div>
<div class="header-react"></div>
<div class="header-phone"></div>
<div class="header-phone-wolpe"></div>
<div class="header-bug"></div>
<div class="header-coffee"></div>
<div class="header-year"></div>
<div class="header-title"></div>
</div>
</template>
</code></pre><p>显然,<code>Header</code>组件主要考查<code>CSS</code>布局,好吧,虽然可以说是模仿写了一遍布局(所有布局都是同理,没什么好说的),但也算是抄袭了(PS:希望掘金技术团队不介意吧)。</p><blockquote>Footer</blockquote><pre><code class="ts"><template>
<div class="footer">
<ul class="footer-web">
<li v-for="(web, index) in footerWebNavList" :key="web.text + index">
<template v-if="web.url">
<a :href="web.url" target="_blank">{{ web.text }}</a>
</template>
<template v-else>{{ web.text }}</template>
</li>
</ul>
<div class="footer-app">
<ul
class="footer-app-item"
v-for="(app, index) in footerAppNavList"
:key="app + index"
>
<li
v-for="(app_item, app_index) in app"
:key="app_item.text + app_index"
>
<template v-if="app_item.url">
<a :href="app_item.url" target="_blank">{{ app_item.text }}</a>
</template>
<template v-else>{{ app_item.text }}</template>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from "vue";
interface FooterItem {
text: string;
url?: string;
}
type FooterList = Array<FooterItem>;
export default {
setup() {
const state = reactive({
footerWebNavList: [
{
text: "@2020掘金",
},
{
text: "关于我们",
url: "https://juejin.cn/about",
},
{
text: "营业执照",
url: "https://juejin.cn/license",
},
{
text: "用户协议",
url: "https://juejin.cn/terms",
},
{
text: "京ICP备18012699号-3",
url: "https://beian.miit.gov.cn/",
},
{
text: "京公网案备11010802026719号",
url:
"http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11010802026719",
},
{
text: "北京北比信息技术有限公司版权所有",
},
],
footerAppNavList: [] as any[],
});
const first: FooterList = state.footerWebNavList.slice(0, 4);
const second: FooterList = state.footerWebNavList.slice(4);
state.footerAppNavList = [first, second];
return {
...toRefs(state),
};
},
};
</script></code></pre><p>这个组件难度也不大,就是把导航数据归纳到一起了而已。</p><p>活动介绍页面也比较简单,就一个<code>tab</code>组件,然后其它都是图片布局。</p><pre><code class="ts"><template>
<div class="info-container">
<Header />
<div class="pc-info"></div>
<div>
<div class="home-button-container">
<router-link to="/">
<div class="home-button"></div>
</router-link>
</div>
<div class="info-box">
<div class="info-title"></div>
<div class="info-box1"></div>
<div class="info-box2"></div>
<div class="info-box3"></div>
<div class="info-box4">
<div class="info-prizes">
<div class="info-prizes-tab">
<div
class="info-prizes-tab1"
:style="{ 'z-index': curInfoTab === 0 ? 3 : 1 }"
@click="onChangeInfoTab(0)"
></div>
<div
class="info-prizes-tab2"
:style="{ 'z-index': curInfoTab === 1 ? 3 : 1 }"
@click="onChangeInfoTab(1)"
></div>
</div>
<div>
<img
:src="require('../assets/' + (curInfoTab === 0 ? 'individual' : 'group') + '_prize_web.png')"
alt="图片加载中"
style="width: 100%"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from "vue";
import Header from "../components/Header.vue";
export default {
components: {
Header,
},
setup() {
const state = reactive({
curInfoTab: 0,
});
const onChangeInfoTab = (value: number) => {
state.curInfoTab = value;
};
return {
...toRefs(state),
onChangeInfoTab,
};
},
};
</script>
</code></pre><p>当然后续代码我就不一一展示了,我主要总结一下所用到的技术知识点。</p><p>首先是<code>vuex</code>,<code>vue2</code>熟练使用的话,其实<code>vue3</code>语法也差别不大。</p><pre><code class="ts">import { useStore } from "vuex";
// store.state
// store.dispath(方法名,数据)</code></pre><p>主要如果子组件想通过事件传递给父组件,则需要通过<code>mitt</code>插件,譬如搜索组件的代码实现如下:</p><pre><code class="ts">import mitt from 'mitt';
export const emitter = mitt();
export default {
setup() {
const state = reactive({
keyword:""
})
const refState = toRefs(state);
const onSearch = () => {
if(!state.keyword)return alert('请输入你喜欢的作者名!');
//传递给父组件
emitter.emit('on-search',state.keyword);
}
return {
...refState,
onSearch
};
},
};</code></pre><p>其它的都是<code>vue3.0</code>的语法了,比如<code>watch</code>监听等等,更多源码在<a href="https://link.segmentfault.com/?enc=GrFAXjCKJDr7cVRBnsOSjQ%3D%3D.PbI%2BvEA%2BdoXSFJBnDXsfdJDMnalk%2BULXcX4g%2Br64u%2FUZYHH5UgAHS%2BXD5emkAF4RuFk4CVeQPuPybmJZ7ilVjQ%3D%3D" rel="nofollow">这里</a>。</p><blockquote>PS:不知道到时间了掘金官方会不会停止相关数据接口的服务,所以下一步,我可能会考虑写静态数据,然后把axios封装一下,当然代码还有些粗糙,因为实现的有些匆忙,后续会做优化。</blockquote><p>最后,附上部分效果图:</p><p><img src="/img/remote/1460000039129384" alt="" title=""></p><p><img src="/img/remote/1460000039129381" alt="" title=""></p><p><img src="/img/remote/1460000039129380" alt="" title=""></p><p><img src="/img/remote/1460000039129382" alt="" title=""></p><p><img src="/img/remote/1460000039129387" alt="" title=""></p><p><img src="/img/remote/1460000039129388" alt="" title=""></p><p><img src="/img/remote/1460000039129386" alt="" title=""></p><p><img src="/img/remote/1460000039129389" alt="" title=""></p><p><img src="/img/remote/1460000039129385" alt="" title=""></p>
从零开始编写一个时间线组件
https://segmentfault.com/a/1190000023909490
2020-09-06T16:44:27+08:00
2020-09-06T16:44:27+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
6
<h2>一.搭建开发环境</h2><blockquote>PS:npm速度慢可使用cnpm</blockquote><p>第一步,让我们先把项目环境搭建好,首先打开命令窗口,执行如下命令:</p><pre><code class="js">npm init</code></pre><p>搭建好了<code>package.json</code>文件之后,接下来开始装依赖包,我们需要用到<code>webpack webpack-cli</code>来打包项目,执行如下命令:</p><pre><code class="js">npm install webpack webpack-cli --save-dev</code></pre><p>在编写代码时,我们需要用到<code>es6</code>的语法,因此我们还需要安装<code>@babel/core @babel/cli @babel/preset-env babel-loader</code>依赖来处理<code>es6</code>兼容语法。继续执行如下命令:</p><pre><code class="js">npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader</code></pre><p>接下来,创建一个<code>babel.config.json</code>文件,然后写入如下代码:</p><pre><code class="js">{
"presets": [
[
"@babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
"useBuiltIns": "usage",
}
]
]
}</code></pre><p>这只是一个默认的配置,也可以自行根据需求来进行配置,更多信息详见<a href="https://link.segmentfault.com/?enc=lec4rIuekvhoOTflDph%2FcQ%3D%3D.KRpk%2FiBtc6vlgVzVAYcBVb0Xj%2Fig9j7GYfb1AU8yheI%3D" rel="nofollow">babel文档</a>。</p><p>这还没有结束,我们还需要搭建<code>vue</code>的开发环境,我们需要编译<code>.vue</code>,所以我们需要安装<code>vue-loader vue-template-compiler vue</code>等依赖包,继续执行如下命令:</p><pre><code class="js">npm install vue vue-loader vue-template-compiler --save-dev</code></pre><p>我们目前所需要的依赖就暂时搭建完成,接下来在页面根目录创建一个<code>index.html</code>文件,写入如下代码:</p><pre><code class="html">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-cli</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="/build.js"></script>
</html></code></pre><p>在这里,我们注意到了我们打包最后引入的文件为<code>build.js</code>文件,接下来我们开始编写<code>webpack</code>的配置,在根目录下继续创建一个<code>webpack.config.js</code>文件,代码如下:</p><pre><code class="js">
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode:"development",
entry:'./main.js',
output:{
path:__dirname,
filename:'build.js'
},
module:{
rules:[
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
},
plugins: [
new VueLoaderPlugin()
]
}</code></pre><p>在导出后面还需要加上这一段<code>js</code>代码,如下:</p><pre><code class="js"> resolve: {
alias: {
'vue': 'vue/dist/vue.js'
}
}</code></pre><p>为什么要加上这一个配置,这个后面会说明原因,这里暂时先放置,继续在根目录下分别创建一个<code>App.vue</code>与<code>main.js</code>文件。代码分别如下:</p><pre><code>import Vue from 'vue';
import App from './App.vue'
Vue.config.productionTip = false;
var vm = new Vue({
el: "#app",
// render:(h) => { return h(App)},
components: {
App
},
template: "<App />"
})
</code></pre><pre><code><template>
<div id="app">
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
data() {
return {
msg: "hello,vue.js!"
};
},
mounted() {
},
methods: {
}
};
</script>
</code></pre><p>接下来,执行命令<code>webpack</code>,然后我们就可以看到页面中会生成一个<code>build.js</code>文件,然后运行<code>index.html</code>文件,我们就可以在浏览器页面上看到<code>hello,vue.js!</code>的字符串,稍等,我们似乎忘记了什么,一般在开发中,谁会给你运行<code>webpack</code>命令来打包,不都是执行<code>npm run build</code>嘛,让我们在<code>package.json</code>中加上这一行代码</p><pre><code class="json">{
"name": "timeline-project",
"version": "1.0.0",
"description": "a component with vue.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack" //这里是添加的代码
},
"keywords": [
"timeline"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.11.5",
"@babel/core": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"babel-loader": "^8.1.0",
"vue": "^2.6.12",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}
</code></pre><p>等等,我们还忘了一件事,别人都可以使用<code>npm run dev</code>命令来在本地运行项目,我们为什么不可以呢?我们需要安装<code>webpack-dev-server</code>依赖,执行如下命令安装:</p><pre><code class="js">npm install webpack-dev-server --save-dev</code></pre><p>安装完成,让我们继续在<code>package.json</code>中添加这样一行代码,如下所示:</p><pre><code class="js">{
"name": "timeline-project",
"version": "1.0.0",
"description": "a component with vue.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server --open --hot --port 8081" //这里是添加的代码
},
"keywords": [
"timeline"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.11.5",
"@babel/core": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"babel-loader": "^8.1.0",
"vue": "^2.6.12",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}</code></pre><p>添加的代码很好理解,就是启动服务热更新,并且端口是<code>8081</code>。接下来让我们尝试运行<code>npm run dev</code>,看看发生了什么!</p><p>真的很棒,我们已经成功运行了<code>vue-cli</code>项目,搭建环境这一步到目前为止也算是成功了。</p><p>前面还提到一个问题,那就是在<code>webpack.config.js</code>中为什么要加上<code>resolve</code>配置,这是因为,如果我们需要在<code>.vue</code>文件中使用<code>components</code>选项来注册一个组件的话,就必须要引入完整的<code>vue.js</code>,也就是编译模板代码,如果我们只用<code>render</code>来创建一个组件,那么就不需要添加这个配置,这就是官网所说的<a href="https://link.segmentfault.com/?enc=hdBWuxtrZYvSsqffyObZDQ%3D%3D.KUbq0H2qswQlXTGryD5CNw6hld1zvdNP7NhijIC66RJCMg8ZsDP00Km7Jvq1jKxqlAjfzIoL32lpJmtyk58rdirVYq83Il6dw2o1RGw2ZPH8MNz9IU5IUu1CtLSf7pQ131WxIs4BxP8U6wFmiD4R%2FFelYAyDerQ3DoopWinwP03DyJk4LyemUoi2%2FqBzH%2FzV5QJDiBNPLBQi0PvO6yjjarEdE6E5dZ2WWh2hMxxxgRk%3D" rel="nofollow">运行时 + 编译器 vs. 只包含运行时</a>。</p><p>在这里我们还要注意一个问题,那就是我们需要处理单文件组件中的<code>css</code>样式,所以我们需要安装<code>css-loader与style-loader</code>依赖。执行如下命令:</p><pre><code class="js">npm install style-loader css-loader --save-dev</code></pre><p>在<code>webpack.config.js</code>中添加如下代码:</p><pre><code class="js">
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode:"development",
entry:'./main.js',
output:{
path:__dirname,
filename:'build.js'
},
resolve: {
// if you don't write this,you can't use components to register component
//only use render to register component
alias: {
'vue': 'vue/dist/vue.js'
}
},
module:{
rules:[
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.css$/,
loader: ["style-loader","css-loader"]
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
</code></pre><p>如果是用<code>less</code>或者<code>stylus</code>或者<code>scss</code>,我们还需要格外安装依赖,例如<code>less</code>需要安装<code>less-loader</code>,这里我们就只用<code>style-loader与css-loader</code>即可,如对<code>less</code>等感兴趣可自行研究。</p><blockquote>特别说明:由于我们所编写的时间线组件并没有用到图标,所以无需添加图标以及图片的处理。</blockquote><h2>二.分析时间线组件结构以及搭建基本架构</h2><p>时间线组件可以分成三部分组成,第一部分即时间线,第二部分即时间戳,第三部分则是内容。我们先来看时间线组件的一个结构:</p><pre><code class="html"><timeline>
<timeline-item></timeline-item>
</timeline></code></pre><p>从上图我们可以看到时间线组件包含两个组件,即<code>timeline</code>与<code>timeline-item</code>组件,接下来我们来分析一下组件的属性有哪些。首先是父组件<code>timeline</code>组件,根据<a href="https://link.segmentfault.com/?enc=%2F0vd7GR9Idd2gA9V%2F46O2Q%3D%3D.Y1gAGDmZsIvfiUdeDf9E8kQcSE%2Bx0Aeb9ByFY6R8u%2Bxk89yAofoTyNp%2B7FQ49odX%2F18ioMaHQy5j1AMjO%2BVvPw%3D%3D" rel="nofollow">element ui官方文档</a>。我们可以看到父组件仅仅只提供了一个<code>reverse</code>属性,即指定节点排序方向,默认为正序,但实际上我们还可以添加一个属性,那就是<code>direction</code>属性,因为<code>element ui</code>默认给的时间线组件只有垂直方向,而并没有水平方向,因此我们提供这个属性来确保时间线组件分为水平时间线和垂直时间线。</p><p>根据以上分析,我们总结如下:</p><pre><code class="js"> direction:'vertical' //或'horizontal'
reverse:true //或false</code></pre><p>接下来,我们来看子组件的属性,它包含<code>时间戳,是否显示时间戳,时间戳位置,节点的类型,节点的图标,节点的颜色以及节点的尺寸</code>。这里我们暂时忽略图标这个选项。因此我们可以将属性定义如下:</p><pre><code class="js"> timestamp:'2020/9/1' //时间戳内容
showTimestamp:true //或false,表示是否显示时间戳
timestampPlacement:'top' //或'bottom',即时间戳的显示位置
nodeColor:'#fff'//节点的颜色值
nodeSize:'size' //节点的尺寸
nodeIcon:'el-icon-more' //节点的图标,在这里我们没有引入element ui组件,因此不添加这个属性,如果要添加这个属性,需要先编写图标组件</code></pre><p>确定了以上属性之后,我们就可以先来编写一个静态的组件元素结构,如下图所示:</p><pre><code class="html"> <!-- 父组件结构 -->
<div class="timeline">
<!-- 子组件结构 -->
<div class="timeline-item">
<!-- 时间线 -->
<div class="timeline-item-tail"></div>
<!-- 时间线上的节点 -->
<div class="timeline-item-node"></div>
<!-- 时间线的时间戳与内容 -->
<div class="timeline-item-wrapper">
<!-- 时间戳,位置在top-->
<div class="timeline-item-timestamp is-top"></div>
<!-- 每一个时间戳对应的内容 -->
<div class="timeline-item-content"></div>
<!-- 时间戳,位置在bottom-->
<div class="timeline-item-timestamp is-bottom"></div>
</div>
</div>
</div></code></pre><p>根据以上代码,我们可以清晰的看到一个时间线的元素构成,为了确保布局方便,我们多写几个子元素,即<code>time-line-item</code>以及它的所有子元素。接下来,我们开始编写静态的样式。如下:</p><pre><code class="css"> .timeline {
font-size: 14px;
margin: 0;
background-color: #ffffff;
}
.timeline-item {
position: relative;
padding-bottom: 20px;
}
.timeline-item-tail {
position: absolute;
}
.is-vertical .timeline-item-tail {
border-left: 3px solid #bdbbbb;
height: 100%;
left: 3px;
}
.is-horizontal .timeline-item .timeline-item-tail {
width: 100%;
border-top: 3px solid #bdbbbb;
top: 5px;
}
.is-horizontal:after {
content: " ";
display: block;
height: 0;
visibility: hidden;
clear: both;
}
.timeline-item.timeline-item-info .timeline-item-tail {
border-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-node {
background-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-content {
color: #44444f;
}
.timeline-item.timeline-item-primary .timeline-item-tail {
border-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-node {
background-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-content {
color: #2396ef;
}
.timeline-item.timeline-item-success .timeline-item-tail {
border-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-node {
background-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-content {
color: #23ef3e;
}
.timeline-item.timeline-item-warning .timeline-item-tail {
border-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-node {
background-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-content {
color: #efae23;
}
.timeline-item.timeline-item-error .timeline-item-tail {
border-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-node {
background-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-content {
color: #ef5223;
}
.is-horizontal .timeline-item {
float: left;
}
.is-horizontal .timeline-item-wrapper {
padding-top: 18px;
left: -28px;
}
.timeline-item-node {
background-color: #e1e6e6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.timeline-item-node-normal {
width: 12px;
height: 12px;
left: -2px;
}
.timeline-item-node-large {
width: 14px;
height: 14px;
left: -4px;
}
.timeline-item-wrapper {
position: relative;
top: -3px;
padding-left: 28px;
}
.timeline-item-content {
font-size: 12px;
color: #dddde0;
line-height: 1;
}
.timeline-item-timestamp {
color: #666;
}
.timeline-item-timestamp.is-top {
margin-bottom: 8px;
padding-top: 6px;
}
.timeline-item-timestamp.is-bottom {
margin-top: 8px;
}
.timeline-item:last-child .timeline-item-tail {
display: none;
}</code></pre><p>接下来我们就要开始实现组件的逻辑封装了,首先我们需要封装<code>timeline</code>组件,为了将该组件归纳到一个目录下,我们先新建一个目录,叫<code>timeline</code>,然后新建一个<code>index.vue</code>文件,并且将我们编写好的<code>css</code>代码给移到该文件下,现在,你看到该文件的代码应该如下所示:</p><pre><code class="html"><script>
export default {
name: "timeline"
}
</script>
<style>
.timeline {
font-size: 14px;
margin: 0;
background-color: #ffffff;
}
.timeline-item {
position: relative;
padding-bottom: 20px;
}
.timeline-item-tail {
position: absolute;
}
.is-vertical .timeline-item-tail {
border-left: 3px solid #bdbbbb;
height: 100%;
left: 3px;
}
.is-horizontal .timeline-item .timeline-item-tail {
width: 100%;
border-top: 3px solid #bdbbbb;
top: 5px;
}
.is-horizontal:after {
content: " ";
display: block;
height: 0;
visibility: hidden;
clear: both;
}
.timeline-item.timeline-item-info .timeline-item-tail {
border-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-node {
background-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-content {
color: #44444f;
}
.timeline-item.timeline-item-primary .timeline-item-tail {
border-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-node {
background-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-content {
color: #2396ef;
}
.timeline-item.timeline-item-success .timeline-item-tail {
border-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-node {
background-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-content {
color: #23ef3e;
}
.timeline-item.timeline-item-warning .timeline-item-tail {
border-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-node {
background-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-content {
color: #efae23;
}
.timeline-item.timeline-item-error .timeline-item-tail {
border-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-node {
background-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-content {
color: #ef5223;
}
.is-horizontal .timeline-item {
float: left;
}
.is-horizontal .timeline-item-wrapper {
padding-top: 18px;
left: -28px;
}
.timeline-item-node {
background-color: #e1e6e6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.timeline-item-node-normal {
width: 12px;
height: 12px;
left: -2px;
}
.timeline-item-node-large {
width: 14px;
height: 14px;
left: -4px;
}
.timeline-item-wrapper {
position: relative;
top: -3px;
padding-left: 28px;
}
.timeline-item-content {
font-size: 12px;
color: #dddde0;
line-height: 1;
}
.timeline-item-timestamp {
color: #666;
}
.timeline-item-timestamp.is-top {
margin-bottom: 8px;
padding-top: 6px;
}
.timeline-item-timestamp.is-bottom {
margin-top: 8px;
}
.timeline-item:last-child .timeline-item-tail {
display: none;
}
</style></code></pre><h2>三.时间线组件逻辑</h2><blockquote>PS:加载sourceMap还需要这样一个配置<code>devtool: 'inline-source-map'</code></blockquote><p>为了确保父子组件共享状态,我们利用<a href="https://link.segmentfault.com/?enc=7GQoK4r5%2FW7c4msfG5OjKg%3D%3D.T%2FCkWlxde6Ltp3utbiTmqa2dvVnydGPyfPPiKuEhK4BqPR3MjmWOmWuF0UUz%2FGnD" rel="nofollow">provide/inject API</a>来传递<code>this</code>对象,如下所示:</p><pre><code class="js"> export default {
name: "timeline",
provide(){
return {
timeline:this
}
}
}</code></pre><p>然后我们开始定义父组件的属性,根据前面所述,它包含两个属性,因此我们定义好在<code>props</code>中,如下所示:</p><pre><code class="js"> import { oneOf } from '../util'
export default {
name: "timeline",
provide(){
return {
timeline:this
}
},
props:{
reverse:{
type:Boolean,
default:false
},
direction:{
type:String,
default:'vertical',
validator:(value) => {
return oneOf(['vertical','horizontal'],value,'vertical');
}
}
}
}</code></pre><p>上面代码需要用到一个工具函数<code>oneOf</code>,顾名思义,就是必须是其中的一项,它有三个参数,第一个参数是匹配的数组,第二个参数是匹配的项,第三个是提供的默认项,该工具函数代码如下:</p><pre><code class="js">export const oneOf = (arr,value,defaultValue) => {
return arr.reduce((r,i) => i === value ? i : r,defaultValue);
}</code></pre><p>其实也不难理解,就是我们想要的值必须是数组的一项,如果不是就返回默认项,工具函数内部代码,我们可以写得更清晰明了一点,如下所示:</p><pre><code class="js">export const oneOf = (arr,value,defaultValue) => {
return arr.reduce((result,item) => {
return item === value ? item : value;
},defaultValue);
}</code></pre><p>以上的代码经过简洁处理就得到了前面的一行代码,如果理解不了,可以采用后者的代码,至于<code>validator</code>验证选项,可参考<a href="https://link.segmentfault.com/?enc=SgWPVp7HD8e%2FQPtYeHbSbw%3D%3D.gyJU04H05ATiQ83gxM5t%2B%2F6cIJTCpteyZGnayQj7necD7RChCxjsJ9RH3LJIFJOHSB0fYySRvDlyCHNVr8%2FNhACEH7URLrYQ18%2BvbBswVNc%3D" rel="nofollow">vue-prop-validator-自定义验证函数</a>。</p><p>接下来,我们在<code>render</code>方法中去渲染这个父组件,代码如下:</p><pre><code class="js">import { oneOf } from '../util'
export default {
name: "timeline",
provide(){
return {
timeline:this
}
},
props:{
reverse:{
type:Boolean,
default:false
},
direction:{
type:String,
default:'vertical',
validator:(value) => {
return oneOf(['vertical','horizontal'],value,'vertical');
}
}
},
//新添加的内容
render(){
const reverse = this.reverse;
const direction = this.direction;
const classes = {
'timeline':true,
'is-reverse':reverse,
['is' + direction]:true
}
const slots = this.$slots.default || [];
if(reverse)slots = slots.reverse();
return (<div class={classes}>{slots}</div>)
}
}</code></pre><p>以上代码似乎不是很好理解,其实也不难理解,首先是获取<code>reverse</code>和<code>direction</code>属性,接着就是设置类名对象,类型对象包含三个,然后就是获取该组件的默认插槽内容,判断如果提供了<code>reverse</code>属性,则调用数组的<code>reverse</code>方法来让<code>slots</code>倒序,在这里我们应该很清晰的明白 <code>slots</code>如果存在,那么则一定是一个<code>vNode</code>节点组成的数组,如果没有,默认就是一个空数组。然后最后返回一个父元素包含该插槽的<code>jsx</code>元素。</p><p>在这里,我们使用了<code>jsx</code>语法,而我们的项目环境当中还并没有添加处理<code>jsx</code>的依赖,所以我们需要再次安装处理<code>jsx</code>语法的依赖<code>babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx</code>,并添加相应的配置。继续在终端输入以下命令安装依赖:</p><pre><code class="js">npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx --save-dev</code></pre><p>然后在<code>babel.config.json</code>中添加一行配置代码如下:</p><pre><code class="js"> "plugins": ["transform-vue-jsx"]</code></pre><p>接着在<code>webpack.config.js</code>中添加一行配置处理如下:</p><pre><code class="js">{
test: /\.(jsx?|babel|es6|js)$/,
loader: 'babel-loader',
exclude: /node_modules/
}</code></pre><p>现在,我们可以看到页面中不会报错,未处理<code>jsx</code>了。</p><p>到此为止,父组件的逻辑代码也就完成了,接下来,让我们在全局里面使用一下该组件,看看是否生效。</p><p>在<code>main.js</code>里面添加如下一行代码:</p><pre><code class="js">import Timeline from './components/timeline/timeline.vue'
Vue.component(Timeline.name,Timeline)</code></pre><p>然后在<code>App.vue</code>里面,我们使用这个组件,代码如下:</p><pre><code class="html"><template>
<div id="app">
<timeline></timeline>
</div>
</template></code></pre><p>ok,似乎看起来没什么问题,让我们继续编写子组件的逻辑代码。</p><p>接下来,新建一个<code>item.vue</code>文件,在该文件中编写如下代码:</p><pre><code class="html"><template>
<div class="timeline-item">
<div class="timeline-item-tail"></div>
<div class="timeline-item-node"
:class="[`timeline-item-node-${ size || ''}`,`timeline-item-node-${type || ''}`]"
:style="{ backgroundColor:color }"
v-if="!$slots.dot"
></div>
<div class="timeline-item-node" v-if="$slots.dot">
<slot name="dot"></slot>
</div>
<div class="timeline-item-wrapper">
<div class="timeline-item-timestamp"
:class="[`is-`+ timestampPlacement ]"
v-if="item.placement === 'top' && showTimestamp"
>{{ timestamp }}</div>
<div class="timeline-item-content"><slot></slot></div>
<div class="timeline-item-timestamp"
:class="[`is-`+ timestampPlacement ]"
v-if="item.placement === 'bottom' && showTimestamp"
>{{ timestamp }}</div>
</div>
</div>
</template>
<script>
export default {
name:"timeline-item",
inject:['timeline'],
props:{
timestamp:String,
showTimestamp:{
type:Boolean,
default:false
},
timestampPlacement:{
type:String,
default:'top',
validator:(value) => {
return oneOf(['top','bottom'],value,'bottom')
}
},
type:{
type:String,
default:'default',
validator:(value) => {
return oneOf(['default','info','primary','success','warning','error'],value,'default');
}
},
size:{
type:String,
default:'normal',
validator:(value) => {
return oneOf(['normal','large'],value,'normal')
}
},
color:String
}
}
</script></code></pre><p>编写完成之后,让我们继续在<code>main.js</code>中引用它,然后在<code>App.vue</code>中使用它。代码分别如下:</p><pre><code class="js"> //main.js
import TimelineItem from './components/timeline/timeline-item.vue'
Vue.component(TimelineItem.name,TimelineItem)
</code></pre><pre><code class="html"><!--App.vue-->
<timeline>
<timeline-item type="default" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">待审核</timeline-item>
<timeline-item type="info" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核中</timeline-item>
<timeline-item type="error" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">审核失败</timeline-item>
<timeline-item type="success" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核完成</timeline-item>
</timeline></code></pre><p>接下来我们就可以看到一个时间线已经完美的展示了,时间线组件算是大功告成了。</p><p>本文档已经录制为视频,地址如下:</p><p><a href="https://link.segmentfault.com/?enc=NKOVlfgfKzE132VthygDaQ%3D%3D.zLS7F1tqwrGWn2dnHpsRlxf3PkiMmUw1JwVZnvl5Cd2x8boXyB5pgEDLb28sSxaF" rel="nofollow">搭建开发环境</a>。<br><a href="https://link.segmentfault.com/?enc=z6LQVzUbv7MJ6DJhzQgPDA%3D%3D.WF3CsTZAk0d4R6407ex9k%2BhFZaD8MpFgtDNB%2FKet8c1Q3thFh5F8sy7Ty8a3qLdm" rel="nofollow">分析与创建时间线基本架构</a>。<br><a href="https://link.segmentfault.com/?enc=s4tkbG%2Bx6y3x1j3UlIzTUQ%3D%3D.3Y%2BujWiX3fzBNjWazQ23TTn6kox%2FX1B46zts4x44gBqH93efjWheoqJ6JWQHgXN%2F" rel="nofollow">编写时间线逻辑</a>。</p><p>源码和文档已经上传到<a href="https://link.segmentfault.com/?enc=u287pEWds3F13ECktuEApQ%3D%3D.zG5b964tGzdnIGEPLfUP%2FKIwXKzwDm6olqrangyTaaSb0NYXXBfoOc%2B9qPoSj7TEbTfXjcAPM61y%2Blzb0lNoXjB3j%2B9BaP0dM9JLSNlzD%2Fs%3D" rel="nofollow">github</a>。</p>
从零开始实现一个消息提示框
https://segmentfault.com/a/1190000022597465
2020-05-10T11:08:24+08:00
2020-05-10T11:08:24+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
10
<h2>引言</h2>
<p>消息提示框在实际应用场景当中比较常见,最常用的就是<a href="https://link.segmentfault.com/?enc=qeG0e%2FHKkHmVHezesWzofQ%3D%3D.P83C1iK6X4TZL0i6Cz8R2TwjWMKGDJr0vZ6%2BM6L9irEhppX%2B2TIOMeOYK%2Fc%2FeAHBE8%2FO4F5t%2Fvyw12xKQ2NrNQ%3D%3D" rel="nofollow">element ui</a>的消息提示框,我们通常都是直接使用它们,但是我们有没有尝试过去探究其实现原理,并自己动手实现呢?为了提升我们的个人能力和竞争力,我们可以尝试来实现这样一个消息提示框。</p>
<h2>实现效果</h2>
<p>我们来查看一下最终实现效果,如下图所示:</p>
<p><img src="/img/remote/1460000022597468" alt="" title=""></p>
<h2>准备工作</h2>
<h3>搭建基本的项目结构</h3>
<p>我们创建一个message文件夹,然后创建一个index.html文件,以及message.js和message.css文件,如下所示:</p>
<p><img src="/img/remote/1460000022597469" alt="" title=""></p>
<h3>对消息提示框进行布局</h3>
<p>在html文件中,我们可以先来实现一个静态的消息提示框,代码如下:</p>
<pre><code class="html"><div class="message">
<p>这是一个提示框</p>
<i class="message-close-btn">&times;</i>
</div></code></pre>
<p>然后再message.css我们写上基本的css代码:</p>
<pre><code class="css">* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 消息提示框容器样式 */
.message {
position: fixed;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
min-width: 300px;
background-color: #edf2fc;
border: 1px solid #edf2fc;
padding: 16px 17px;
top: 25px;
border-radius: 6px;
overflow: hidden;
z-index: 1000;
}
/* 关闭按钮样式 */
.message > .message-close-btn {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
color: #c0c0c4;
font-size: 17px;
cursor: pointer;
}
.message > .message-close-btn:hover,.message > .message-close-btn:active {
color: #909399;
}
/* 消息提示框内容样式 */
.message p {
line-height: 1;
font-size:14px;
color: #909399;
}</code></pre>
<p>有四种提示框,以及让内容居中,我们不外乎是多加一个类名来写css样式,比如内容居中,我们只需要在html消息提示框容器元素加上一个类名,代码如下:</p>
<pre><code class="html"><div class="message message-center">
<p>这是一个提示框</p>
<i class="message-close-btn">&times;</i>
</div> </code></pre>
<p>然后再css文件中加一段如下的css代码即可:</p>
<pre><code class="css">/* 内容居中 */
.message.message-center {
justify-content: center;
}</code></pre>
<p>四种类型的提示框不外乎也是同样的原理,增加一个类名,然后改变的是背景色和字体色,所以html代码如下:</p>
<pre><code class="html"><div class="message message-success">
<p>这是一个成功提示框</p>
<i class="message-close-btn">&times;</i>
</div>
<div class="message message-warning">
<p>这是一个警告提示框</p>
<i class="message-close-btn">&times;</i>
</div>
<div class="message message-error">
<p>这是一个错误提示框</p>
<i class="message-close-btn">&times;</i>
</div></code></pre>
<p>css代码如下:</p>
<pre><code class="css">/* 成功提示框样式 */
.message.message-success {
background-color: #e1f3d8;
border-color:#e1f3d8;
}
.message.message-success p {
color: #67c23a;
}
/* 警告提示框样式 */
.message.message-warning{
background-color: #fdfce6;
border-color: #fdfce6;
}
.message.message-warning p{
color: #e6a23c;
}
/* 错误提示框样式 */
.message.message-error {
background-color: #fef0f0;
border-color: #fef0f0;
}
.message.message-error p {
color: #f56c6c;
}</code></pre>
<p>这样一来,准备工作就完成了,接下来就是我们的重头戏,JavaScript代码,尝试将如上代码注释掉。</p>
<h2>动态实现创建提示框</h2>
<h3>定义四种类型的消息提示框</h3>
<p>我们通过定义一个对象来表示消息提示框的类型,如下所示:</p>
<pre><code class="js">// 消息提示框的四种类型
let typeMap = {
info: "info",
warning: "warning",
success: "success",
error: "error"
}</code></pre>
<h3>添加默认配置项</h3>
<p>我们分析一下需要传入的配置项有内容(content),关闭提示框时间(closeTime),是否显示关闭提示框按钮(showClose),内容居中(center)以及消息提示框类型(type)。所以定义配置项如下:</p>
<pre><code class="js">// 提示框的默认配置项
let messageOption = {
type: "info",
closeTime: 600,
center: false,
showClose: false,
content: "默认内容"
}</code></pre>
<h3>创建一个消息提示框类</h3>
<p>我们通过面向对象的编程思维将消息提示框当做是一个类对象,所以我们只需要创建一个类。虽然可以使用es6的class语法来创建,但是为了方便,我们使用构造函数来实现。创建一个构造函数Message,如下所示:</p>
<pre><code class="js">function Message(option) {
//这里做了一次初始化
this.init(option);
}</code></pre>
<h3>初始化</h3>
<p>创建了消息提示框构造函数之后,我们需要传入配置项,并且我们在函数里做了初始化操作,接下来我们来实现初始化的操作。</p>
<pre><code class="js">Message.prototype.init = function (option) {
//这里创建了提示框元素,并将整个提示框容器元素添加到页面中
document.body.appendChild(this.create(option));
//这里设置提示框的top
this.setTop(document.querySelectorAll('.message'));
//判断如果传入的closeTime大于0,则默认关闭提示框
if (option.closeTime > 0) {
this.close(option.container, option.closeTime);
}
//点击关闭按钮关闭提示框
if (option.close) {
option.close.onclick = (e) => {
this.close(e.currentTarget.parentElement, 0);
}
}
}</code></pre>
<h3>创建提示框</h3>
<p>在前面的初始化操作中,我们做了几个功能,首先创建提示框容器元素,并将提示框容器元素添加到页面bod中。我们还做了动态计算提示框的top以及判断传入的默认关闭时间来关闭提示框,点击关闭按钮关闭提示框。我们来看创建提示框的方法,即create方法的编写操作。如下:</p>
<pre><code class="js">Message.prototype.create = function (option) {
//这里做了一个判断,表示如果设置showClose为false即不显示关闭按钮并且closeTime也为0,即无自动关闭提示框,我们就显示关闭按钮
if(!option.showClose && option.closeTime <=0)option.showClose = true;
//创建容器元素
let element = document.createElement('div');
//设置类名
element.className = `message message-${option.type}`;
if (option.center) element.classList.add('message-center');
//创建关闭按钮元素以及设置类名和内容
let closeBtn = document.createElement('i');
closeBtn.className = 'message-close-btn';
closeBtn.innerHTML = '&times;';
//创建内容元素
let contentElement = document.createElement('p');
contentElement.innerHTML = option.content;
//判断如果显示关闭按钮,则将关闭按钮元素添加到提示框容器元素中
if (closeBtn && option.showClose) element.appendChild(closeBtn);
//将内容元素添加到提示框容器中
element.appendChild(contentElement);
//在配置项对象中存储提示框容器元素以及关闭按钮元素
option.container = element;
option.close = closeBtn;
//返回提示框容器元素
return element;
}</code></pre>
<h3>实现提示框的关闭</h3>
<p>我们可以看到,我们创建了一个close方法,并传入提示框容器元素,来实现关闭一个提示框,接下来我们来实现这个关闭方法。如下所示:</p>
<pre><code class="js">Message.prototype.close = function (messageElement, time) {
//根据传入的时间来延迟关闭,实际上也就是移除元素
setTimeout(() => {
//判断如果传入了提示框容器元素,并且分两种情况,如果是多个提示框容器元素则循环遍历删除,如果是单个提示框容器元素,则直接删除
if (messageElement && messageElement.length) {
for (let i = 0; i < messageElement.length; i++) {
if (messageElement[i].parentElement) {
messageElement[i].parentElement.removeChild(messageElement[i]);
}
}
} else if (messageElement) {
if (messageElement.parentElement) {
messageElement.parentElement.removeChild(messageElement);
}
}
//关闭了提示框容器元素之后,我们重新设置提示框的top值
this.setTop(document.querySelectorAll('.message'));
}, time * 10);
}</code></pre>
<h3>实现提示框的动态top</h3>
<p>最后我们需要实现的是动态计算消息提示框的top,然后不让消息提示框重叠在一起。代码如下:</p>
<pre><code class="js">Message.prototype.setTop = function (messageElement) {
//这里做一个判断的原因就是当点击页面中最后一个提示框的时候,会重新调用一次,这时获取不到提示框容器元素,所以就不执行后续的设置top
if (!messageElement || !messageElement.length) return;
//由于每个提示框的高度一样,所以我们只需获取第一个提示框元素的高度即可
const height = messageElement[0].offsetHeight;
for (let i = 0; i < messageElement.length; i++) {
//每个提示框的top由一个固定值加上它的高度,并且我们要乘以它的一个索引值
messageElement[i].style.top = (25 * (i + 1) + height * i) + 'px';
}
}</code></pre>
<h2>最后,实现封装,让我们可以如同调用element ui那样来调用</h2>
<p>我们想要这样调用<code>$message()</code>或者<code>$message.info()</code>,那么我们可以实现如下:</p>
<pre><code class="js">let $message = {};
window['$message'] = $message = function (option) {
let newMessageOption = null;
if (typeof option === 'string') {
newMessageOption = Object.assign(messageOption, { content: option });
} else if (typeof option === 'object' && !!option) {
newMessageOption = Object.assign(messageOption, option);
}
return new Message(newMessageOption);
}
for (let key in typeMap) {
window['$message'][key] = function (option) {
let newMessageOption = null;
if (typeof option === 'string') {
newMessageOption = Object.assign(messageOption, { content: option,type:typeMap[key] });
} else if (typeof option === 'object' && !!option) {
newMessageOption = Object.assign(JSON.parse(JSON.stringify(messageOption)),option,{ type:typeMap[key] });
}
return new Message(newMessageOption);
}
}</code></pre>
<p>整个逻辑也十分简单,无非就是判断传入的配置项,然后进行合并,并传入实例化的Message中。</p>
<p>如此一来,我们就完成了一个消息提示框。</p>
<h2>录制视频</h2>
<p>如果以上分析还不懂的话,可以查看我录制的一个视频:</p>
<p><a href="https://link.segmentfault.com/?enc=Wna%2FJtVyHx9ul1DUOhExsQ%3D%3D.Ex4Dr3adQ%2BXX6CeImRINCI%2FPZ3jX6VT%2FmOKqMyVHubLnZyEson8a9eBlBrHy3UwXe92iDoiEUKN7JE5Rw8oEeg%3D%3D" rel="nofollow"><img src="/img/remote/1460000022597470" alt="从零开始实现一个消息提示框" title="从零开始实现一个消息提示框"></a></p>
100多行代码实现js或者jquery版的类似juejin的预览图片功能
https://segmentfault.com/a/1190000022139449
2020-03-25T11:51:57+08:00
2020-03-25T11:51:57+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
7
<h3>前言</h3>
<p>预览图片是一个很常用的业务功能,比如掘金的预览图片功能,下面我们就来模拟实现一个类似掘金的简单预览图片功能(PS:最终实现动画效果不如掘金,可自行扩展,还有就是嵌套的元素与掘金的方式也有区别)。</p>
<h3>创建一个构造函数</h3>
<p>首先新建一个<code>js</code>文件,命名为<code>viewer.js</code>,然后在这个<code>js</code>文件中,我们来创建一个函数,命名为<code>ewViewer</code>。然后参数是一个图片元素,可以是<code>jquery</code>的图片元素,也可以是<code>js dom</code>图片元素,我们做一个判断然后分别调用不同的实现方法,如下所示:</p>
<pre><code class="js">function ewViewer(el){
//判断页面内容是否存在jquery并且传入的el元素是否是一个jquerydom云阿苏
if(typeof window.jQuery !== 'undefined' && el instanceof window.jQuery){
this.previewjQuery(el);
}else{
this.previewJS(el);
}
return this;
}</code></pre>
<h3>实现js的预览图</h3>
<p>实现的思路就是放置两个元素,第一个是遮罩层元素,并且设置相关的样式,子元素就是存放图片的元素,我们通过设置子元素的<code>background-image</code>属性来显示图片,代码如下(每一步的注释都解释了):</p>
<pre><code class="js">ewViewer.prototype.previewJS = function(el){
const isDom = function(e){
return typeof HTMLElement === 'object' ? e instanceof HTMLElement : e && typeof e === 'object' && e.nodeType === 1 && typeof e.nodeName === 'string' || e instanceof HTMLCollection || e instanceof NodeList;
}
// 如果传入的不是一个dom元素则不执行后续代码
if(!isDom(el))return;
const curElement = document.querySelector('#preview-img-mask');
// 判断当前遮罩层元素是否存在
if(!curElement){
// 创建遮罩层元素
const element = document.createElement('div');
// 克隆一个节点
const child = element.cloneNode(true);
element.id = "preview-img-mask";
// 添加遮罩层元素样式
element.style.cssText += `position:fixed;left:0;top:0;right:0;display:none;z-index:10000;overflow:auto;width:100%;height:100%;background:rgba(0,0,0,0.5)`;
// 子元素添加样式
child.style.cssText += `background-repeat: no-repeat;background-position: center;background-size: contain;`;
element.appendChild(child);
document.body.appendChild(element);
}
// 重新获取遮罩层元素
const maskLayer = document.querySelector('#preview-img-mask');
// 判断如果传入的元素不是一个图片元素则不执行如下代码
if (el.length || el.tagName.toLowerCase().indexOf('img') === -1)return;
// 获取图片的原始宽高
var imgNaturalWidth = el.naturalWidth,
imgNaturalHeight = el.naturalHeight;
// 移动端以及页面缩放做判断
const isMobile = window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
// 获取页面宽度
const pageWidth = (function(){
return window.innerWidth;
})();
// 如果当前页面宽度小于600,或是移动端设备,则将原始宽高缩放0.6倍
if (isMobile || pageWidth <= 600) {
imgNaturalWidth = imgNaturalWidth * 0.6;
imgNaturalHeight = imgNaturalHeight * 0.6;
}
// 获取页面宽高
var viewportWidth = window.innerWidth,
viewportHeight = window.innerHeight;
// 获取图片元素的src路径
var imgUrl = el.getAttribute('src');
// 显示遮罩层
setTimeout(() => {
maskLayer.style.display = "block";
},600);
// 获取遮罩层的子元素
var maskLayerDiv = maskLayer.querySelector('div');
// 设置子元素的样式
maskLayerDiv.style.cssText += `background-image:url('${imgUrl}');background-size:${imgNaturalWidth}px ${imgNaturalHeight}px;width:0;height:0;`;
// 判断图片宽度是否大于页面宽度,然后设置子元素的宽度
if (imgNaturalWidth > viewportWidth) {
maskLayerDiv.style.width = imgNaturalWidth + "px";
} else {
maskLayerDiv.style.width = "100%";
}
// 判断图片高度是否大于页面高度,然后设置子元素的高度
if (imgNaturalHeight > viewportHeight) {
maskLayerDiv.style.height = imgNaturalHeight + "px";
} else {
maskLayerDiv.style.height = "100%";
}
// 点击遮罩层关闭预览
maskLayer.onclick = function(){
setTimeout(() => {
this.style.display = 'none';
},600);
}
}</code></pre>
<h3>实现jquery版</h3>
<p>实现思路类似js版本,只不过是有些代码做了变换而已。</p>
<pre><code class="js">ewViewer.prototype.previewjQuery = function(el){
const curElement = $('#preview-img-mask');
// 判断如果页面不存在遮罩层元素,则添加该元素
if (!curElement.length) {
$(`<div id="preview-img-mask"><div></div></div>`).appendTo("body");
}
// 获取遮罩层元素
var maskLayer = $('#preview-img-mask');
// 设置遮罩层样式
maskLayer.css({
position:'fixed',
left:0,
top:0,
right:0,
display:'none',
'z-index':10000,
overflow:'auto',
width:'100%',
height:'100%',
background:'rgba(0,0,0,0.5)'
});
// 判断传入的如果不是一个img元素
if (el.prop('tagName').toLowerCase().indexOf('img') === -1)return;
var imgNaturalWidth = el[0].naturalWidth,
imgNaturalHeight = el[0].naturalHeight;
// 移动端以及页面缩放做判断
const isMobile = window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
// 获取页面宽度
const pageWidth = (function(){
return $(window).width();
})();
// 判断页面宽度是否小于600以及是否是移动端
if (isMobile || pageWidth <= 600) {
imgNaturalWidth = imgNaturalWidth * 0.6;
imgNaturalHeight = imgNaturalHeight * 0.6;
}
// 获取页面的宽高
var viewportWidth = $(window).width(),
viewportHeight = $(window).height();
// 获取图片的路径
var imgUrl = el.attr('src');
maskLayer.fadeIn(600);
// 获取子元素
var maskLayerDiv = maskLayer.children();
// 设置子元素样式
maskLayerDiv.css({
"background-image": "url(" + imgUrl + ")",
"background-size": imgNaturalWidth + "px " + imgNaturalHeight + "px",
"width": "",
"height": "",
"background-repeat": "no-repeat",
"background-position": "center",
"background-size": "contain"
});
// 判断图片原始宽高与页面宽高,从而决定子元素的宽高
if (imgNaturalWidth > viewportWidth) {
maskLayerDiv.css('width', imgNaturalWidth);
} else {
maskLayerDiv.css('width', '100%');
}
if (imgNaturalHeight > viewportHeight) {
maskLayerDiv.css('height', imgNaturalHeight);
} else {
maskLayerDiv.css('height', '100%');
}
// 点击遮罩层关闭
maskLayer.off("click").on("click", function () {
$(this).fadeOut(600);
});
}</code></pre>
<h3>调用方式</h3>
<p>而且我们还做了判断,在移动端设备以及页面宽度小于<code>600px</code>的时候,我们将按照原图的宽高比例缩放<code>0.6</code>倍显示。调用方式也很简单,如以下一个示例:</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预览图</title>
</head>
<body>
<img src="/static/image/1.jpg" alt="">
<img src="/static/image/1.jpg" alt="">
</body>
<script src="./viewer.js"></script>
<script src="./static/plugin/jquery.min.js"></script>
<script>
//js方式调用代码如下:
// let imgElements = document.querySelectorAll('img');
// [].slice.call(imgElements).forEach((img) => {
// img.onclick = function(){
// const viewer = new ewViewer(this);
// console.log(viewer);
// }
// });
//jquery调用方式代码如下
$('img').click(function(){
const viewer = new ewViewer($(this));
console.log(viewer);
});
</script>
</html></code></pre>
<h3>最后</h3>
<p>这只是实现简单预览图的第一步,如果可以,还可以在这上面扩展,进而实现<code>viewer.js</code>那样的预览图片库了。不过本文旨在介绍简单的预览功能,后续就不赘述了。</p>
2019,我的个人总结
https://segmentfault.com/a/1190000021962433
2020-03-09T21:50:47+08:00
2020-03-09T21:50:47+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
3
<p>2019年是我变化最大的一年,不仅仅是技术,沟通交流与能力等各方面更让我清晰的认识到了自己的不足之处,学习的路还有很长很长,我有必要写出一篇文章来总结自己的这一年,以怀念我的2019。</p><h3>入职</h3><p>我是3月份加入的这家公司(四川领梵数字科技有限公司),然后直到现在,参与公司的核心产品发布后台管理系统与发布系统小程序的开发以及维护,也包含测试工作。</p><p>起初,我对公司的产品也是一知半解,我的老大给我讲的时候,包括我接任的前端开发的那位大哥也给我讲过。但那个时候,我还是一直处于懵懵懂懂的,隐约知道公司的产品是做展厅的,而我加入进来,主要也是参与发布系统外接一些外包项目。</p><p>但是,我隐约知道一点,就是需要学习<code>pixi.js</code>,这是一款编写2D精灵渲染的引擎。我进来的第一项工作就是熟悉已经写好的架构系统代码,随着开发的深入,我对这个产品也渐渐熟悉了。</p><h3>开发小程序</h3><p>3月到4月一个月的时间我主要就是熟悉发布后台管理系统的代码,以及维护一些和添加一些新功能,4月份到5月份,主要开发了发布系统的小程序的第一版,由于没有设计,尽管基本要求的功能完成了,而且花的时间也不多,也就一个星期的时间,所以第一版小程序就这么直接废弃了。</p><p>第二个星期,开发第二版小程序,这一版有了设计,经过一个月的时间开发和维护,基本功能也完成了,并且上线成功运营当中。</p><p>小程序的模块不算多,大致分为如下模块:</p><ul><li>登录:账号与密码登录与微信授权登录</li><li>首页:播放计划以及相关操作(增删改以及发布终端),每个播放计划包含多个节目(节目操作包含增删 改),节目也可以预览和修改播放时间。(注:每个节目包含多个制作的模板)</li><li>用户:安全中心,反馈,添加终端与激活终端。</li></ul><p>这其中涉及到的基本功能也都包含到了,包括微信授权,支付,扫一扫,短信验证,数据的加密(后台做)与解密(前端做)。</p><p>小程序所用到的技术:uni-app。</p><p>图标是采用设计给的,布局是手写的样式,没有用到插件。</p><p>学到了什么?:参考了iview的row与col组件以及model组件的源码,在此基础上也自己封装了row和col组件以及model组件运用到了小程序当中。详情代码展示如下:</p><p>util.js:</p><pre><code>/*
* 功能:常用函数
* 作者:eveningwater
* 日期:2019/3/6
*/
// 往上查找组件的父组件
export function findComponentUp(context,componentName,componentNames){
componentNames = typeof componentName === 'string' ? [componentName] : componentName;
let parent = context.$parent;
let name = parent.$options.name;
//如果父组件不是传入的组件名,则循环往上查找,直到找到父组件为传入的组件名为止
while(parent && (!name || componentName.indexOf(name) === -1)){
parent = parent.$parent;
if(parent)name = parent.$options.name;
}
return parent;
}
// 往下查找组件的子组件
export function findComponentDown(context,componentName){
const childrens = context.$children;
let children = null;
if(childrens.length){
// 循环遍历数组
// for(const child of childrens){
// const name = child.$options.name;
// if(name === componentName){
// children = child;
// break;
// }else{
// children = findComponentDown(child,componentName);
// if(children)break;
// }
// }
for(let k = 0,len = childrens.length; k < len;k++){
const name = childrens[k].$options.name;
if(name === componentName){
children = childrens[k];
break;
}else{
children = findComponentDown(childrens[k],componentName);
if(children)break;
}
}
}
return children
}
// 查找组件的所有父组件
export function findComponentsUp(context,componentName){
let parents = [];
if(parent){
const name = parent.$options.name;
if(name === componentName)parents.push(parent);
return parents.concat(findComponentsUp(parent,componentName));
}else{
return [];
}
}
// 查找组件的所有子组件
export function findComponentsDown(context, componentName) {
let components = [];
return context.$children.forEach((child) => {
if (child.$options.name === componentName) components.push(child);
let foundChild = findComponentsDown(child, componentName);
return components.concat(foundChild);
})
}
// 查找组件的兄弟组件
export function findComponentsBrother(context, componentName, exceptSelf = true) {
// 找到当前组件的父组件的所有子组件
let childComponents = context.$parent.$children.filter((item) => {
return item.$options.name === componentName;
})
// 所有子组件中包含自身组件的索引
let selfIndex = childComponents.findIndex((item) => {
return context._uid === item._uid;
})
// 是否删除自身组件
if (exceptSelf) childComponents.splice(selfIndex, 1);
return childComponents;
}
</code></pre><p>row.vue:</p><pre><code><template>
<div :style="gutterObject" class="ew-row">
<slot></slot>
</div>
</template>
<script>
import {findComponentDown,findComponentsBrother} from './util.js'
export default {
props:{
gutter:{
type:[String,Number],
default:0
}
},
data() {
return {
};
},
computed:{
// 间隔绑定
gutterObject(){
let gutter = parseInt(this.gutter),style = {};
if(gutter){
style = 'margin-left:'+ gutter / -2 + 'px;' +
'margin-right:' + gutter / -2 + 'px;';
}
return style;
}
},
mounted(){
},
methods:{
updateGutter(gutter){
// 找到当前组件的col组件
let ewCol = findComponentDown(this,'ewCol');
let ewCols = findComponentsBrother(ewCol,'ewCol',false);
if(ewCols.length){
ewCols.forEach((child) => {
if(gutter){
child.gutter = gutter;
}
})
}
}
},
watch:{
'gutter':{
handler(val){
if(val){
// 如果gutter不为0,向row组件下的col组件传递gutter值
this.updateGutter(val)
}
},
deep:true
}
}
}
</script>
<style>
@import './component.css';
</style>
</code></pre><p>col.vue:</p><pre><code><template>
<div :style="gutterObject" :class="classObject" class="ew-col">
<slot></slot>
</div>
</template>
<script>
import { findComponentUp } from './util.js'
export default {
props: {
span: {
type: [String, Number],
default: 0
}
},
data() {
return {
gutter:0
};
},
computed: {
// 区块间隔
gutterObject(){
let gutter = parseInt(this.gutter);
if(gutter){
return 'padding-left:' + gutter / 2 + 'px;' +
'padding-right:' + gutter / 2 + 'px;';
}
},
// 栅格类绑定
classObject() {
let span = parseInt(this.span);
if (span) {
return 'ew-col-span-' + span;
}
}
},
methods:{
updateGutter(){
const ewRow = findComponentUp(this,'ewRow');
if(ewRow){
ewRow.updateGutter(ewRow.gutter);
}
}
},
mounted() {
this.updateGutter();
}
}
</script>
<style>
@import './component.css';
</style>
</code></pre><p>model.vue:</p><pre><code><template>
<view class="modal-mask" @click="$emit('on-ew-close',$event)" :class="className">
<view class="modal-content" @click="$emit('on-ew-open',$event)">
<text class="modal-title" v-if="message.title">{{ message.title }}</text>
<text class="modal-content-text" v-if="message.content">{{ message.content }}</text>
<slot name="content"></slot>
<button type="primary" @click="$emit('on-ew-sure')" class="surebtn" v-if="!showCancel">确定</button>
<ew-row v-else>
<ew-col span="12">
<view @click="$emit('on-ew-sure')" class="surebtn">确定</view>
</ew-col>
<ew-col span="12">
<view @click="$emit('on-ew-cancel')" class="cancelbtn" >取消</view>
</ew-col>
</ew-row>
</view>
</view>
</template>
<script>
export default {
props:{
message:{
type:Object,
default:() => {
return {}
}
},
showCancel:{
type:Boolean,
default:false
},
className:String
},
computed:{
},
data() {
return {
};
},
mounted(){
},
methods:{
}
}
</script>
<style>
@import './component.css';
</style>
</code></pre><p>遇到的问题:在开发当中我遇到了uni-app框架还不是完全支持vue插槽语法。我后面只能将不生效的组件重写一遍。</p><h3>学习pixi.js</h3><p>后面因为要熟悉老大写的引擎代码,所以我就花了一周时间将pixi.js基础学习了一下,并趁着不是太忙的时候,记录了下来,写成了自己的笔记。<a href="https://segmentfault.com/a/1190000020692071">文档内容</a>。</p><h3>发布系统的一些特色功能</h3><p>对于发布系统,我还是挺自豪的,因为里面涉及到了很多功能都比较复杂,这其中包括<code>组件事件编辑器</code>,<code>动画时间轴</code>,<code>素材库</code>,<code>动画设置</code>等。</p><p>先来说明一下发布系统吧,这个产品有些类似易企秀的产品,但与易企秀又有着区别。而且发布系统的主要特色就是可以编辑粒子动画。</p><p>目前一些基本的编辑模板需要用到的组件都开发完成了,也能制作出一些模板。如以下就是我自己使用发布系统所制作出的模板。</p><p><img src="/img/remote/1460000021962436" alt="" title=""></p><p>后期也可以针对需求来完成一些特色的功能,这也是与易企秀的区别所在。</p><p>制作模板所用的引擎就是使用<code>pixi.js</code>编写的。发布系统的代码也是恐怖,就单单一个编辑模板里面所涉及到的代码就有二十多个文件,(PS:本来是没有这么多个文件的,全部代码集中在一个文件中,一个文件有几万行代码,我看着头疼,就一步步的将代码抽离出来,所以花的时间也比较多)。下面展示一下时间轴代码,其它就不展示了。如下图(只展示了其中一个文件夹的文件)所示:</p><p><img src="/img/remote/1460000021962438" alt="" title=""></p><p>时间轴代码:</p><p>timeline.vue:</p><pre><code><template>
<div class="timeline" v-drag ref="timeline" :style="closeStyle">
<!--让时间轴拖拽的元素 -->
<div class="timeline-drag-el"></div>
<!-- 关闭时间轴 -->
<el-tooltip effect="dark" content="关闭" placement="top-start">
<i :class="iconClassName" class="close-timeline" @click="closeTimeLine"></i>
</el-tooltip>
<div class="timeline-content-container" :style="closeStyle">
<!-- 组件管理 -->
<div class="timeline-component-group">
<div class="timeline-component-header">组件名</div>
<div class="timeline-component-name" v-for="(com,index) in componentArr" :key="index" :style ="curComponentIndex === index ? 'background-color:rgb(155, 199, 226);color:#fff;' : ''" @click="selectComponent(com,index)">
{{ com.styles.name }}
<el-tooltip effect="dark" content="播放" placement="top-start">
<i class="el-icon-caret-right play-component-icon" @click="playComponentAnimation(com)"></i>
</el-tooltip>
<el-tooltip effect="dark" content="删除" placement="top-start">
<i class="el-icon-delete delete-component-icon" @click="deleteComponent(com)"></i>
</el-tooltip>
</div>
</div>
<div class="timeline-timeline-container" ref="timeLineScroll">
<!-- 模拟时间轴横向滚动条 -->
<div class="timeline-scrollbar-wrapper">
<div class="timeline-scrollbar" style="width:1479px;">
<div class="timeline-track" style="width:1479px;">
<div class="timeline-thumb" style="width:990px;" :style="moveLeft" @mousedown="changeLeft"></div>
</div>
</div>
</div>
<div class="timeline-container">
<div
class="timeline-content-overview"
ref="timeLineView"
:style="{ 'min-width':'100%',width:spotArr * 195 + 22 + 'px',left:-viewLeft + 'px'}">
<!-- 时间轴刻度线 -->
<div class="timeline-content-iframe">
<div class="timeline-content-marker" v-for="(t,index) in spotArr" :key="index" :style="{ width:'195px'}">
<span class="timeline-text">{{ index + 's' }}</span>
</div>
</div>
<!-- 时间管理 -->
<div class="timeline-layer-container">
<div class="timeline-layer-area" v-for="(com,index) in componentArr" :key="index" :style ="curComponentIndex === index ? 'background-color:rgb(155, 199, 226);color:#fff;' : ''">
<div class="timeline-layer-animation"
v-for = "(node,_index) in com['animation']['group'][0].ani"
:key="_index"
@mousedown="changeDelayOrDuration($event,node,_index)"
:style="computedStyle(com['animation']['group'][0].ani,node,_index)">
<span class="timeline-layer-delay" v-show="node.delay * 1000 >= 150">{{ node.delay * 1000 }}</span>
<span class="timeline-layer-duration">{{ node.duration * 1000 }}</span>
<div class="timeline-layer-resize-handle-delay"></div>
<div class="timeline-layer-resize-handle-duration"></div>
</div>
</div>
</div>
<!-- 时间轴游标 -->
<div class="timeline-drag-handle" :style="{ left:spotLeft +'px'}" @mousedown="changeSpot">
<div class="timline-drag-spot"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="../js/_timeline.js"></script>
<style lang="stylus">
@import '../css/timeline.styl'
</style>
</code></pre><p>_timeline.js:</p><pre><code>
export default {
name: "timeline",
props: ['timeLineData', 'componentIndex'],
data() {
return {
left: 0,//模拟滚动条左偏移量
viewLeft: 0,//刻度线左偏移量
componentArr: [],//组件数组
spotLeft: -10,//拖拽左偏移量
curComponentIndex: this.componentIndex,//当前组件
closeStyle: {},
isCloseTimeLine: false,
iconClassName:"el-icon-remove-outline"
}
},
computed: {
// 模拟的滚动条的左偏移量
moveLeft() {
return {
left: this.left + 'px'
}
},
//刻度线数,也许是一个数组
spotArr() {
return Math.ceil(this.maxLeftOrMaxTime('time')() / 1000) + 1 || 11;
},
maxScrollLeft() {
// 990为滚动轨道的宽度,4px减少偏差
return this.$refs.timeLineScroll.offsetWidth - 994;
},
viewMaxLeft() {
// 一页显示6个刻度线,195为每个刻度线宽度,剩余刚好就是this.spotArr - 6个,所以滚动最大为195 * (this.spotArr - 6)
if (this.spotArr > 6) {
return 195 * (this.spotArr - 6);
} else {
return 0;
}
}
},
mounted() {
// 时间轴数据
if (this.timeLineData) {
this.componentArr = this.timeLineData;
}
},
methods: {
closeTimeLine() {
this.isCloseTimeLine = !this.isCloseTimeLine;
if (this.isCloseTimeLine) {
this.$set(this.closeStyle, 'width', 0);
this.$set(this.closeStyle, 'height', 0);
this.$set(this.closeStyle, 'padding', 0);
this.iconClassName = 'el-icon-full-screen';
} else {
this.closeStyle = {};
this.iconClassName = 'el-icon-remove-outline';
}
},
// 计算宽度与左偏移量
computedStyle(nodeArr, node, index) {
if (index <= 0) {
return { width: 10 * (node.duration * 1000 / 50) + 'px', left: 10 * node.delay * 1000 / 50 + 21 + 'px' }
} else {
//初始左偏移量
let left = 21;
nodeArr.forEach((n, nIndex) => {
if (nIndex <= index) {
left += 10 * (n.delay * 1000 / 50)
}
if (nIndex <= index - 1) {
left += 10 * (n.duration * 1000 / 50);
}
})
return { width: 10 * (node.duration * 1000 / 50) + 'px', left: left + 'px' }
}
},
// 改变左偏移量
changeLeft() {
document.onmousemove = (e) => {
this.left = e.pageX > this.maxScrollLeft ? this.maxScrollLeft : e.pageX <= 0 ? 0 : e.pageX;
this.viewLeft = e.pageX > this.viewMaxLeft ? this.viewMaxLeft : e.pageX <= 0 ? 0 : e.pageX;
}
this.cancelEvent();
},
// 改变延迟或者执行时间
changeDelayOrDuration(event, node, index) {
// 判断拖拽方向
let direction = event.clientX;
// 块的总宽
let total = 10 * (node.duration * 1000 / 50);
// 如果拖拽的是执行时间轴,则改变执行时间,否则改变延迟时间
let isDuration = event.target.className.indexOf('duration') > - 1 ? true : false;
document.onmousemove = (e) => {
if (e.clientX >= direction) {
if (isDuration) {
node.duration = (node.duration * 1000 + 50) / 1000;
} else {
node.delay = (node.delay * 1000 + 50) / 1000;
}
} else {
if (isDuration) {
node.duration = (node.duration * 1000 - 50) / 1000;
} else {
node.delay = (node.delay * 1000 - 50) / 1000;;
}
if (node.delay <= 0) node.delay = 0;
if (node.duration <= 0) node.duration = 0;
}
}
this.cancelEvent();
},
// 拖拽游标的最大值
maxLeftOrMaxTime(type) {
let nodeArr = [];
this.componentArr.forEach((com) => {
if (com['animation']['group'][0]['ani'].length) {
com['animation']['group'][0]['ani'].map((val) => {
if (type.indexOf('left') > -1) {
nodeArr.push(10 * (val.duration * 1000 / 50) + 10 * (val.delay * 1000 / 50));
} else if (type.indexOf('time') > -1) {
nodeArr.push(val.duration * 1000 + val.delay * 1000);
}
})
}
})
return nodeArr.length > 0 ? () => {
return Math.max(...nodeArr) || Math.max.apply(null, nodeArr);
} : () => { return 0 };
},
// 时间轴游标拖动
changeSpot() {
this.spotLeft = -10;
this.left = 0;
this.viewLeft = 0;
document.onmousemove = (e) => {
this.spotLeft = e.pageX <= 0 ? -10 : e.pageX >= this.maxLeftOrMaxTime('left')() ? this.maxLeftOrMaxTime('left')() : e.pageX;
if (e.pageX > this.maxScrollLeft) this.left = e.pageX - this.maxScrollLeft >= this.maxScrollLeft ? this.maxScrollLeft : e.pageX - this.maxScrollLeft;
if (e.pageX > this.viewMaxLeft) this.viewLeft = e.pageX - this.viewMaxLeft >= this.viewMaxLeft ? this.viewMaxLeft : e.pageX - this.viewMaxLeft;
if (e.pageX <= 0) {
this.left = this.viewLeft = 0;
}
}
this.cancelEvent();
},
// 注销鼠标拖拽结束事件
cancelEvent() {
document.onmouseup = (e) => {
document.onmousemove = document.onmouseup = null;
}
},
// 选中组件
selectComponent(item, index) {
this.curComponentIndex = index;
this.$parent.$parent.getCoverage(item.uuid);
},
// 删除组件动画
deleteComponent(item) {
if (this.componentArr.length) {
let idx = this.componentArr.indexOf(item);
this.componentArr[idx].animation.group[0].ani = [];
}
},
// 播放组件动画
playComponentAnimation(item,name) {
this.spotLeft = -10;
let spotMaxLeft = 0;
let time = 0;
let start = null;
let animationName = name ? name : item.animation.group[0].name;
if(!name){
this.$parent.$parent.$refs.iframe.contentWindow.EditorSupport.root.previewAnimation(animationName);
}
if (item.animation.group[0].ani.length) {
item.animation.group[0].ani.forEach((ani) => {
spotMaxLeft += 10 * (ani.duration * 1000 / 50) + 10 * (ani.delay * 1000 / 50);
time += ani.duration + ani.delay;
})
//时间轴游标运动
let play = (t) => {
if (!start) start = t;
let progress = t - start;
if (progress < time * 1000) {
this.spotLeft = progress / 5;
window.requestAnimationFrame(play);
} else {
this.spotLeft = spotMaxLeft;
}
}
if (spotMaxLeft && time) {
window.requestAnimationFrame(play);
}
}
}
}
}</code></pre><p>时间轴到目前为止虽然实现了基本功能,但是并不好用,因为当时开发时间轴的时候,我对需求理解的也不是很到位。当时是参考<a href="https://link.segmentfault.com/?enc=3bXLk7IablNExgm7Eb9Jvw%3D%3D.FZ337GCz8T5%2BYrdwD5WfCOnycWeg16gDLI9xHyQwUL%2Fp0mLqpovDbfrcPs930pt%2BZ8gttW5rXCWyzeY0jVI6QcQDXXncuT8MCnovesyVCKBxm3rdEcEhsBfAeVRzDxV7t64zkAP8IZM4K3Kes0X4TUmMBZz6Zt8R0BvqJEVbH1Z4ffyYDhFtHVXRK2UdMtSjMbMf7CmNmi0WYsRYzfzqAsVSAjjQKsTZEiJIvXG%2FquWseaDujGbfho3ob7po%2BrY6BHp5XgfUMa3NgRNrmZriFw%3D%3D" rel="nofollow">smartslider3</a>这个里面的时间轴的功能来写的。</p><p>遇到的问题非常的多,尤其我影响深刻的是这样一个问题:</p><p>因为一个模板的数据非常的复杂,每个模板里面有一个动画的数据。我在请求模板数据的时候,用了一个变量接受它。代码如下:</p><pre><code>//this.Decrypt方法只是一个已经封装好了的数据解密方法
let res = JSON.parse(JSON.parse(this.Decrypt(result)));
//打印出来两者不是相等的
console.log(this.Decrypt(result),res);</code></pre><p>第一个结果如下图所示:</p><p><img src="/img/remote/1460000021962437" alt="" title=""></p><p>第二个结果如下:</p><p><img src="/img/remote/1460000021962441" alt="" title=""></p><p>针对这个问题,我足足花了三个小时的时间来排查出问题所在,我依次打印出后台返回的数据都是没有问题的。</p><p>但是就是这么奇怪,它就是多了一个<code>ease</code>对象,我真的很好奇,因为我的代码里确实没出现赋值代码,后面,我定位到引擎代码,结果还真的让我找到了问题所在。如下图所示:</p><p><img src="/img/remote/1460000021962440" alt="" title=""></p><p><img src="/img/remote/1460000021962439" alt="" title=""></p><p>因为引擎代码是通过webpack打包,然后我这边通过一个iframe标签来加载这个引擎代码。代码如下:</p><pre><code><iframe ref="iframe" class="iframe" src="../../../static/pixijs/index.html" frameborder="0" scrolling="no" @load="dataInit"></iframe>
</code></pre><p>然后,我要创建一个引擎,就需要调用引擎代码提供的<code>createApp</code>方法,参数就是数据以及<code>width</code>和<code>height</code>。代码如下:</p><pre><code>//这里的this.addData就是前面所说的res
this.$refs.iframe.contentWindow.Main.createApp(this.addData, 888, 500);</code></pre><p>这让我清晰的认识到了<code>js对象</code>的引用特性。</p><h3>其它项目</h3><p>当然还有一些项目,但都是比较小的项目,基本用到的技术都是<code>jquery</code>之类的,而这一年的时间,我花在开发和维护发布后台管理系统的时间是最多的。但其它项目让我影响比较深刻的还是二天的时间完成的一个后台管理系统——报价后台管理系统。</p><p>报价后台管理系统所涉及到的功能不多,所以我完成的也算比较快,这没什么好说的。但我要总结到的就是在这个系统当中,我逐渐的规范化了代码。我将所有接口都规范在了一个文件里面,即<code>api.js</code>。如下:</p><pre><code>const host = '';
const api = {
loginAPI:host + 'UserLogin/login',//登录接口
registerAPI:host + 'UserLogin/addUser',//添加用户接口
exportAPI:host + 'MakeExcel/getJson',//普通用户导出数据接口
addSystemAPI:host + 'ShowProjectController/inserShowProject',//管理员添加系统接口
editSystemAPI:host + 'ShowProjectController/updateShowProject',//管理员编辑系统接口
deleteSystemAPI:host + 'ShowProjectController/deleteShowProject',//管理员删除系统接口
findSystemAPI:host + 'ShowProjectController/selectShowProject',//管理员与用户查询系统接口
lookWareApi: host + "EquipmentController/selectEqui", //查询软件和硬件信息接口
addWareApi: host + "EquipmentController/insertEqui", //添加软件和硬件信息接口
editWareApi: host + "EquipmentController/updateEqui", //修改软件和硬件信息接口
deleteWareApi: host + "EquipmentController/deleteEqui", //删除软件和硬件信息接口
modelApi: host + "ChildController/selectChild", //查软件与硬件型号
addModelApi: host + "ChildController/insertChild", //添加软件与硬件型号
updateModelApi: host + "ChildController/editChild", //修改软件与硬件型号
deleteModelApi: host + "ChildController/deleteChild", //删除软件与硬件型号
}
export default api; </code></pre><p>慢慢的规范化了自己的代码,这才是我最想说的,这也是这个系统带给我的收获。</p><h3>发布系统与小程序的使用文档</h3><p>今年2月份到3月份我的主要工作也是完成发布系统的使用文档以及小程序的使用文档,采用<code>gitbook</code>搭建的。也遇到了一些坑,都总结成了文章。<a href="https://segmentfault.com/a/1190000021823830">详见</a>。</p><h3>工作之余编写个人网站的文档</h3><p>工作之余闲下来之后,我就写自己的个人网站里的文档,不停的学习,目前已完成了<code>HTML</code>与<code>pixi.js</code>的总结,<code>CSS</code>也快要完成了,希望今年能把<code>javascript</code>以及<code>vue.js</code>完成。</p><p>如有兴趣可前往我的<a href="https://link.segmentfault.com/?enc=VQhvKsoOHrdlK%2B1StQUN6g%3D%3D.cAKzxEM95rrrsDKia8GVaKwH%2Ba6DccgzR8w6NZg6sqs%3D" rel="nofollow">个人网站</a>查看。</p><h3>最后</h3><p>学如逆水行舟,不进则退。从2017年毕业到现在已经二年多了,其实我对自己的未来也还是有些迷茫的,因为这些都不是我想要的,我特别想做一个自由职业者,能够做出一个别样的开源项目来,那才是我的目标,所以我用闲余时间做了一个<a href="https://link.segmentfault.com/?enc=Mqli7P%2Fv68JHXB36tpHy4w%3D%3D.6FJk6G1DSg67JJ8qQSsl9f3CJ0OJm%2FwjcdoCeJmok2PjWh8J%2B3N98j16M8ieHFRT" rel="nofollow">ew-color-picker</a>的开源项目,但是,这只是一个简单的开始,也是我对开源项目的初步涉猎,我相信我还有很长的路要走,给自己一句鼓励:加油,努力每天学习一点点,每天就进步一点点。</p>
gitbook踩坑记以及(源文件名大于系统长度)删除限制的小技巧删除分享
https://segmentfault.com/a/1190000021823830
2020-02-24T12:09:02+08:00
2020-02-24T12:09:02+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<h2>1.gitbook踩坑记</h2>
<p>当从<code>github</code>上<code>clone</code>下来<code>gitbook</code>搭建的项目,并且<code>gitbook -V</code>查看<code>gitbook</code>已经安装好,然后<code>gitbook init</code>(其它命令同理)运行报如下图错:</p>
<p><img src="/img/bVbDJv4" alt="image.png" title="image.png"></p>
<p>不要惊慌,镇定,让我们来解决这个问题。</p>
<p>迅速查找到<code>gitbook</code>的全局安装目录<code>.gitbook</code>目录,通常情况下在<code>C</code>盘管理员用户中,如下图所示:</p>
<p><img src="/img/bVbDJwa" alt="image.png" title="image.png"></p>
<p>打开<code>version</code>目录以及<code>gitbook</code>版本号<code>3.2.3</code>(这里是<code>3.2.3</code>)目录,打开之后找到<code>node_modules</code>文件夹以及<code>package.json</code>文件,删除它们即可。如下图所示:</p>
<p><img src="/img/bVbDJwx" alt="image.png" title="image.png"></p>
<p>然后再运行<code>gitbook init</code>命令,如果有安装<code>gitbook</code>插件,则在下一步命令<code>gitbook install</code>,到此为止没出现什么问题。</p>
<p>继续在本地运行该项目,但是输入<code>gitbook serve</code>(本地运行<code>gitbook</code>项目命令)会出现如下错误:</p>
<p><img src="/img/bVbDJwD" alt="image.png" title="image.png"></p>
<p>也不要心慌,继续打开之前的<code>~/gitbook/version/3.2.3</code>目录,找到<code>lib</code>目录,找到<code>output</code>文件夹并打开,再次打开<code>website</code>目录,找到<code>copyPluginAssets.js</code>文件,然后将里面的所有代码<code>confirm:true</code>全部替换为<code>confirm:false</code>,然后就能运行<code>gitbook serve</code>跑起来<code>gitbook</code>项目了。如下图所示:</p>
<p><img src="/img/bVbDJwG" alt="image.png" title="image.png"></p>
<p><img src="/img/bVbDJwI" alt="image.png" title="image.png"></p>
<p><img src="/img/bVbDJwO" alt="image.png" title="image.png"></p>
<p><img src="/img/bVbDJwS" alt="image.png" title="image.png"></p>
<h2>2.(源文件名大于系统长度)删除限制的小技巧删除</h2>
<p>众所周知,在<code>windows</code>系统下删除长文件夹会有限制,也就是如下图所示:</p>
<p><img src="/img/bVbDJw2" alt="image.png" title="image.png"></p>
<p>那么我们应该如何删除该文件夹呢?</p>
<p>首先需要安装一款压缩软件,如:<code>360压缩</code>。然后右键文件夹,选择压缩,然后选择压缩后删除源文件,即可删除该文件夹,然后再删除压缩之后的文件。如下图所示:</p>
<p><img src="/img/bVbDJxn" alt="image.png" title="image.png"></p>
<p>到此为止了。</p>
假如女朋友要求帮她挑选衣服,怎么办?
https://segmentfault.com/a/1190000020710198
2019-10-16T17:43:52+08:00
2019-10-16T17:43:52+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
3
<h3>一.分析需求</h3>
<p>假如你的女朋友发给你一堆衣服的图片,然后问你哪件好看,只能选一件最好看的,你会如何做?为什么不交给程序来进行抉择呢?本文的主题就是开发一个选择程序来解决你的女朋友的选择问题。</p>
<p>页面最终效果如图所示:</p>
<p><img src="/img/remote/1460000020710201" alt="" title=""></p>
<p>我们来总结一下要实现的功能:</p>
<p>可以上传多张图片。(这里不需要写后台也是可以的,只不过如果要存储数据可以选择会话存储,当然本例没有被存储)。<br>点击开始的时候会开始在所有的图片之间来回选择。选择加了一个样式(阴影样式)。<br>点击停止的时候,出现选择的结果。<br>点击重置也就重置选择效果。<br>因为默认数据是以上的5张图。所以一旦点击了上传将会被替换掉,所以点击还原就是还原成默认的数据。</p>
<h3>二.实现html与css</h3>
<p>ok,需求分析完成。接下来开始设计整个界面,没错为了让界面看起来比较简洁明了。布局很简单。就是排列一堆图片以及图片名,然后结果显示在下方,最后就是排列一堆按钮。如此一来基本用浮动布局也可以办到。所以html和css都没什么好说的:</p>
<p>html:</p>
<pre><code><div id="list-image"></div>
<div id="result">
点击开始选择吧!
</div>
<div id="list-button">
<button class="upload-btn btn" id="upload-btn" type="button">
上传
<input type="file" id="upload-input" class="upload-input" multiple>
</button>
<button class="start-btn btn" id="start-btn" type="button">开始</button>
<button class="stop-btn btn" id="stop-btn" type="button">停止</button>
<button class="reset-btn btn" id="reset-btn" type="button">重置</button>
<button class="origin-btn btn" id="origin-btn" type="button">还原</button>
</div></code></pre>
<p>css:</p>
<pre><code>* {
margin: 0;
padding: 0;
}
#list-image {
position: relative;
margin-top: 25px;
}
#list-image::after {
content: "";
clear: both;
height: 0;
display: block;
visibility: hidden;
}
#list-image .list-image-item {
width: 120px;
height: 150px;
float: left;
margin-left: 15px;
}
.list-image-item-active {
box-shadow: 0 0 15px #2396ef;
}
#list-image .list-image-item img {
width: 120px;
height: 120px;
display: block;
}
#list-image .list-image-item p {
text-align: center;
font-size: 18px;
}
#list-button {
margin: 15px;
}
.btn {
line-height: 1;
white-space: nowrap;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
transition: 0.1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 12px 14px;
font-size: 14px;
text-align: center;
display: inline-block;
cursor: pointer;
border-radius: 4px;
outline: none;
}
.start-btn,
.upload-btn {
background: #66b1ff;
border-color: #66b1ff;
color: #fff;
}
.start-btn:hover,
.upload-btn:hover,
.start-btn:active,
.upload-btn:active {
color: #fff;
background-color: #409eff;
border-color: #409eff;
}
.stop-btn:hover,
.stop-btn:active,
.reset-btn:hover,
.reset-btn:active,
.origin-btn:hover,
.origin-btn:active {
color: #57a3f3;
background-color: #fff;
border-color: #57a3f3;
}
#result {
color: #2396ef;
font-size: 25px;
margin: 2em;
}
@media screen and (max-width: 765px) {
#list-image {
padding: 20px;
margin-top: 0;
}
#list-image .list-image-item {
width: 50%;
margin-left: 0;
}
#list-image .list-image-item img {
width: 100px;
height: 100px;
margin: 10px auto;
}
#result {
margin: 0;
text-align: center;
margin-bottom: 1em;
}
#list-button {
margin: 0;
margin-bottom: 1em;
text-align: center;
}
}
input[type="file"] {
display: none !important;
}</code></pre>
<h3>三.分析js逻辑代码</h3>
<p>咱们来分析一下js代码,首先定义一堆默认数据:</p>
<pre><code>//默认数据
var data = [
{
url: "./image/1.jpg",
text: "1"
},
{
url: "./image/2.jpg",
text: "2"
},
{
url: "./image/3.jpg",
text: "3"
},
{
url: "./image/4.jpg",
text: "4"
},
{
url: "./image/5.jpg",
text: "5"
}
];</code></pre>
<p>然后获取变量:</p>
<pre><code>var startBtn = document.getElementById('start-btn');
var stopBtn = document.getElementById('stop-btn');
var originBtn = document.getElementById('origin-btn');
var listImage = document.getElementById("list-image");
var result = document.getElementById('result');
var resetBtn = document.getElementById('reset-btn');
var timer;//定时器
var count;//当前选择的是
var uploadInput = document.getElementById("upload-input");
var uploadBtn = document.getElementById("upload-btn");</code></pre>
<p>接着渲染默认的图片数据:</p>
<pre><code>function renderList(data) {
var str = "";
data.forEach(function (item) {
str += '<div class="list-image-item">' +
'<img src="' + item.url + '" alt="">' +
'<p>' + item.text + '</p>' +
'</div>';
});
listImage.innerHTML = str;
}
renderList(data);</code></pre>
<p>然后点击上传按钮会重新替换数据:</p>
<pre><code>uploadBtn.onclick = function () {
return uploadInput.click();
};
uploadInput.onchange = function (event) {
var file = event.target.files;
if (typeof file !== 'object') return;
var uploadData = [];
for (var i = 0; i < file.length; i++) {
if (/image\/\w+/.test(file[i].type)) {
uploadData.push({
url: window.URL.createObjectURL(file[i]),
text: (i + 1)
});
}
}
renderList(uploadData);
}</code></pre>
<p>重要说明:这里要随机选择图片所以要用到定时器和随机函数,咱们先把随机函数定义下来:</p>
<pre><code>function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}</code></pre>
<p>好,接下来完成随机选择的功能:</p>
<pre><code>function selectRandom() {
count = random(1, listImage.children.length);
for (var i = 0; i < listImage.children.length; i++) {
if (i === count - 1) {
listImage.children[i].classList.add('list-image-item-active');
} else {
listImage.children[i].classList.remove('list-image-item-active');
}
}
timer = setTimeout(function () {
selectRandom();
}, 50);
}</code></pre>
<p>点击开始按钮,开始随机选择:</p>
<pre><code> startBtn.onclick = function () {
if (!timer) {
selectRandom();
} else {
alert("请先停止再点击开始!")
}
}</code></pre>
<p>点击结束按钮,结束选择:</p>
<pre><code>stopBtn.onclick = function () {
if (timer && count > 0) {
clearTimeout(timer);
timer = null;
result.textContent = "最好看的是:" + count;
count = 0;
} else {
alert("请先点击开始再停止!");
}
}</code></pre>
<p>点击重置按钮,重置选择的效果:</p>
<pre><code> resetBtn.onclick = function () {
result.textContent = "点击开始选择吧!";
for (var i = 0; i < listImage.children.length; i++) {
listImage.children[i].classList.remove('list-image-item-active');
}
}</code></pre>
<p>点击还原按钮,还原默认数据:</p>
<pre><code>originBtn.onclick = function () {
renderList(data);
}</code></pre>
<p>到此为止,就算完了,以后你的女朋友要是再问你帮她选择,你实在选不出来,你可以理直气壮的说交给程序来选择吧。(ps:希望不要气到你的女朋友,哈哈哈!)。</p>
<p><a href="https://link.segmentfault.com/?enc=elHRhR2gpNXRbH2s4UEMDw%3D%3D.dv9SRPEFzpiucqRwA7ciJs0G0aPnVVT9ZKV5WBlOSE3oswLeFubGz3zkAnryZ00B" rel="nofollow">一个已经完成的demo</a>。</p>
<p>重要说明:(由于安卓手机的限制,input标签加multiple属性并不能多选,这也是这里的一个bug。)</p>
pixi.js学习总结
https://segmentfault.com/a/1190000020692071
2019-10-15T14:46:08+08:00
2019-10-15T14:46:08+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
18
<h2>一.pixi.js简介</h2>
<p>pixi.js是一个非常快速的2D精灵渲染引擎。它可以帮助我们显示,动画和管理交互式图形。如此一来,我们可以使用javascript和其它HTML5技术来轻松实现一个应用程序或者完成一款游戏。它有一个语义化的、简洁的API,包含了许多有用的功能。比如说支持纹理地图集,也提供一个通过动画精灵(交互式图像)所构建的精简的系统。它还为我们提供了一个完整的场景图,我们可以创建嵌套精灵的层次结构(也就是精灵当中嵌套精灵)。同样的也允许我们将鼠标以及触摸的事件添加到精灵上。而且,最重要的还是,pixi.js可以根据我们的实际需求来使用,很好的适应个人的编码风格,并且还能够和框架无缝集成。</p>
<p>pixi.js的API事实上是陈旧的Macromedia/Adobe Flash API的一个改进。熟练flash的开发人员将会有一种回到自己家里一样的熟悉。其它使用过类似API的精灵渲染框架有: CreateJS,Starling, Sparrow 和 Apple’s SpriteKit。pixi.js的优点在于它的通用性:它不是一个游戏引擎。这是极好的,因为它可以完全任由我们自由发挥,做自己的事情,甚至还可以用它写一个自己的游戏引擎。在学习pixi.js之前,我们需要先对HTML,CSS,Javascript有一些了解。因为这会对我们学习pixi.js有很大的帮助。</p>
<h2>二.pixi.js安装</h2>
<h3>1.前往github安装</h3>
<p>可以前往github上安装,在<a href="https://link.segmentfault.com/?enc=tTJ052lDrbePOfYGKsoNNw%3D%3D.OZp5RLTtpc6idYUB2CfNgf6NAD2Qbsg4KOdmrq2uu8U%2BX0t%2BmxlQ40K7Y%2BgG6ywj2qKiPE1lkxRT7pc%2Bx%2BbC7p6vY7bScnKAzaDirKdaVbI%3D" rel="nofollow">这里</a>。也可以在<a href="https://link.segmentfault.com/?enc=QTPB09GNftt1gX8QzdsvFQ%3D%3D.picRRwSpPWKDWfdZind8%2FAOduONV98T7v0BC4EFSkIs3t0hacC57DCj%2FgNsJui99" rel="nofollow">这里</a>下载安装。</p>
<h3>2.使用npm安装</h3>
<p>也可以使用npm来安装。首先需要安装<a href="https://link.segmentfault.com/?enc=Mhuc4AERykijqoQPbUBSLg%3D%3D.rlxqokrN8YgZIGpeo8SrtEu2zD6AKd6oKbGQpO4W2GA%3D" rel="nofollow">node.js</a>。<br>当安装node.js完成之后,会自动完成npm的安装。然后就可以通过npm将pixi.js安装在全局。命令如下:</p>
<pre><code>npm install pixi.js -g
//也可以简写为
npm i pixi.js -g</code></pre>
<p>或者也可以将pixi.js安装在当前项目的目录之下。命令如下:</p>
<pre><code>npm install pixi.js -D //或者
npm install pixi.js --save -dev
//也可以简写为
npm i pixi.js -D</code></pre>
<h2>三.开始使用pixi.js</h2>
<h3>1.引入pixi.js</h3>
<p>安装pixi.js完成之后,我们就可以使用了。首先在你的项目目录下创建一个基础的.html文件。 然后在你html文件中,使用script标签来,加载这个js文件。代码如下:</p>
<pre><code><script src="/pixi.min.js"></script></code></pre>
<p>或者,你也可以使用CDN来引入这个js文件。如下:</p>
<pre><code><script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.3/pixi.min.js"></script>
<!--或者使用bootstrap的cdn来引入-->
<script src="https://cdn.bootcss.com/pixi.js/5.1.3/pixi.min.js"></script></code></pre>
<p>如果使用es6的模块化加载pixi.js,那么就需要注意了,因为pixi.js没有默认导出。所以正确的引入方式应该如下:</p>
<pre><code>import * as PIXI from 'pixi.js'
</code></pre>
<h3>2.一个简单的示例</h3>
<p>好了,接下来就是写点js代码,看看pixi.js是否在工作中。代码如下:</p>
<pre><code>const type = "WebGL";
if (!PIXI.utils.isWebGLSupported()) {
type = "canvas";
}
PIXI.utils.sayHello(type);
</code></pre>
<p>如果能在浏览器控制台中,看到如下图所示的标志,那么就证明pixi.js加载成功。</p>
<p><img src="/img/remote/1460000020692074?w=1903&h=121" alt="1" title="1"></p>
<p>我们来分析一下以上的代码的意思,先定义一个变量,值是字符串"webGL",然后有一个if语句判断如果支持webGL,那么就改变这个变量的值为canvas。然后调用pixi.js所封装的在控制台打印这个值的方法,即sayHello方法。</p>
<p><a href="https://link.segmentfault.com/?enc=pU7kYZjT3AYZBDWaFk8QkA%3D%3D.aJnBScKOIRIObvxF2N6Ev%2F8eC4vqyieYDj4g2%2B2iVCucF6TLQoxV9MMyzHyhxqGg2MUQCVrCsYHmiJuxnNYxVUOtyPwyL7kxuO7JtEk7pDQ%3D" rel="nofollow">在线示例</a></p>
<h3>3.创建你自己的pixi应用和舞台</h3>
<p>现在,我们可以愉快的开始使用pixi.js呢。首先,我们可以创建一个可以显示图片的矩形区域,pixi.js有一个Application对象来帮助我们创建它。它会自动创建一个canvas的HTML标签。并且还能够自动计算出让你的图片能够在canvas元素中显示。然后,你需要创建一个特殊的pixi容器对象,它被叫做舞台。正如你所看到的,这个舞台元素会被当做根容器,然后你可以在这个根容器使用pixi来显示你想要显示的东西。以下是你需要创建一个pixi应用对象以及舞台对象所必要的代码。</p>
<pre><code>//创建一个pixi应用对象
let app = new PIXI.Application({width: 256, height: 256});
//将这个应用对象元素添加到dom文档中
document.body.appendChild(app.view);</code></pre>
<p>以上代码运行在浏览器中,效果如图所示:<br><img src="/img/remote/1460000020692075?w=758&h=470" alt="" title=""></p>
<p>很好,以上的代码所代表的意思就是,我们在HTML DOM中创建了背景颜色为黑色(默认颜色)的一个宽为256,高为256的canvas元素(单位默认是像素)。没错,就是一个黑色的矩形。我们可以看到PIXI.Application是一个构造函数,它会根据浏览器是支持canvas还是webGL来决定使用哪一个渲染图像。函数里面可以不传参数,也可以传一个对象当做参数。如果不传参数,那么会使用默认的参数。这个对象,我们可以称之为option对象。比如以上,我们就传了width和height属性。</p>
<p><a href="https://link.segmentfault.com/?enc=RRUUuv%2BOGlk2nQs87ILPNA%3D%3D.Bj7slfUpWLggJGM%2BIbPyAM84VRSd0nQBfiSwYO9XPFqmWXFdPPeuOtabtbQ2Dzpnu4ns3wZuCVJbNQiQoyg4bOeGaxISBGputGqyoL57cVA%3D" rel="nofollow">在线示例</a></p>
<h3>4.属性</h3>
<p>当然,我们还可以使用更多的属性,例如以下代码:</p>
<pre><code>let app = new PIXI.Application({
width: 256, //default:800
height: 256, //default:600
antialias: true, //default:false
transparent: false, //default:false
resolution: 1 //default:1
});</code></pre>
<p>这些属性到底代表什么意思呢?antialias属性使得字体的边界和图形更加平滑(webGL的anti-aliasing在所有平台都不可用,你需要在你的游戏平台中去做测试。)。transparent属性是设置整个canvas元素的透明度。resolution属性让不同分辨率和不同像素的平台运行起来更容易一些。只要将这个属性的值设置为1就可以应付大多数的工程项目呢。想要知道这个属性的所有细节,可以查看这个项目<a href="https://link.segmentfault.com/?enc=Y6KCnqOOR%2F%2BN5i4NYGF5DA%3D%3D.dim84ZoaE%2FYM7NAkSoQpCdy4zF3qrfNlukqzynBtEgmNWHkKAB26%2BVtWrFOfQGl0LDCGR3eE7SrcnAj4lVoaS%2BJqvmMP%2FpLpKJKNLQ7QBHM%3D" rel="nofollow">Mat Grove'sexplanation</a>的代码。</p>
<p>pixi.js默认是通过WebGL来渲染的。因为webGL的速度非常块,并且我们还可以使用一些将要学习的壮观的视觉效果。当然,如果需要强制性的使用Canvas来取代webGL渲染。我们可以将forceCanvas的值设置为true,即可。如下:</p>
<pre><code>forceCanvas:true</code></pre>
<p>在你创建了canvas元素之后,如果你想要改变它的背景颜色,你需要设置<code>app.renderer.backgroundColor</code>属性为任意的十六进制颜色值("0X"加上"0"~"f"之间的任意6个字符组成的8位字符。)。例如:</p>
<pre><code>app.renderer.backgroundColor = 0x061639;</code></pre>
<p>如果想要获取canvas的宽和高,可以使用app.renderer.view.width和app.renderer.view.height属性。</p>
<p>当然,我们也可以改变canvas的大小,只需要使用renderer.resize方法并且传入width和height属性的值即可。不过,为了确保大小能够正确的适应平台的分辨率,我们需要将autoResize的值设置为true。如下:</p>
<pre><code>app.renderer.autoResize = true;
app.renderer.resize(512, 512);//第一个512代表宽度,第二个512代表高度</code></pre>
<p>如果想要canvas填满整个浏览器的窗口,可以提供css样式,然后将canvas的大小设置为浏览器的窗口大小。如下:</p>
<pre><code>app.renderer.view.style.position = "absolute";
app.renderer.view.style.display = "block";
app.renderer.autoResize = true;
app.renderer.resize(window.innerWidth, window.innerHeight);</code></pre>
<p>但是,如果这样做了之后,确保使用如下的css代码来让所有HTML元素的margin和padding初始化为0。</p>
<pre><code> * {
margin:0;
padding:0;
}
</code></pre>
<p>上面代码中的星号*是CSS“通用选择器”,它的意思是“HTML文档中的所有标记”。</p>
<p>如果希望让canvas按比例缩放到任何浏览器窗口大小,则可以使用自定义的<a href="https://link.segmentfault.com/?enc=vfO9OC1jPXirCxQEbD8wjg%3D%3D.vCseRKOzJshadx%2B0BgLMu8iG1i%2BeMAFWKkau6KfdKMBrW7xGGxNwtE24lTsm9y%2F2" rel="nofollow">scaleToWindow</a>函数。可以点击<a href="https://link.segmentfault.com/?enc=0t2PnAB0eTA4GW1q7h4SLw%3D%3D.uoVCXVJj5NAK7uStcj%2FrXBJi7pG8N8Ryh9Ne08kuo73t2Ik1g04P9IlsVawNLixg7SvPOGGeT899%2F5a5%2FtCdJg%3D%3D" rel="nofollow">这里</a>查看更多应用对象配置属性。</p>
<p><a href="https://link.segmentfault.com/?enc=QHXpp%2FcGHM97CL2kHJ%2FIVQ%3D%3D.srX2ZfgKoXAQE7UqoX7rNmcT2qITsMfb%2FCPxzKxz60iiMVCXnS3Zw7ZxhzjAu%2F0EuWefIC9wplBVXHEf8oz6%2FhFA%2B%2Fgpc1BYYPuFqGQhyXY%3D" rel="nofollow">在线示例</a></p>
<h2>四.核心知识</h2>
<h3>1.pixi精灵</h3>
<p>现在,我们已经有了一个渲染器,或者我们可以称之为画布。我们可以开始往画布上面添加图片。我们希望显示的任何内容都必须被添加到一个叫做stage(舞台)的特殊pixi对象中。我们可以像如下那样使用这个舞台:</p>
<pre><code>//app为实例化的应用对象
let stage = app.stage;//现在这个stage变量就是舞台对象
</code></pre>
<p>stage是一个特殊的pixi容器对象。我们可以将它看作是一个空盒子,然后和我们添加进去的任何内容组合在一起,并且存储我们添加的任何内容。stage对象是场景中所有可见对象的根容器。无论我们在舞台中放入什么内容都会在画布中呈现。现在我们的这个盒子还是空的,没有什么内容,但是我们很快就会放点内容进去。可以点击<a href="https://link.segmentfault.com/?enc=Y%2FIjgrpSu6ZxnBeKoK2F%2Fw%3D%3D.sI6AgN1hivhXItb02LAE1%2Be3cc8xjNSfrEeOW8V4ABcAAoZNSzeTm1SAld4DMc9SMRzyF5ZW65eml5CMHGRKfw%3D%3D" rel="nofollow">这里</a>查看关于pixi容器对象的更多信息。</p>
<p>重要说明: 因为stage是一个pixi容器。所以它拥有有和其它任何容器一样的属性方法。虽然stage拥有width和height属性,但是它们并不指的是渲染窗口的大小。stage的width和height属性仅仅是为了告诉我们放在里面的东西所占据的区域——更多关于它的信息在前面。</p>
<p>那么我们应该在舞台上面放些什么东西呢?就是一些被称作为精灵的特殊图片对象。精灵基本上就是我们能用代码控制的图片。我们可以控制它们的位置,大小,以及其它用于动画和交互的有用的属性。学会如何制作与控制一个精灵真的是一件关于学习如何使用pixi.js的重要的事情。如果我们知道如何制作精灵并且能够把它们添加到舞台中,我们距离制作游戏仅剩一步之遥。</p>
<p>pixi有一个精灵类,它是一种制作游戏精灵的多功能的方式。有三种主要的方式来创建它们:</p>
<pre><code>1.从单个图片文件中创建。
2.用一个 雪碧图来创建。雪碧图是一个放入了你游戏所需的所有图像的大图。
3.从纹理图集(一个JSON文件,在雪碧图中定义图片大小和位置)。
</code></pre>
<p>我们将学习这三种方式。但在此之前,我们先了解一下我们需要了解的图片,然后使用pixi显示这些图片。</p>
<h3>2.将图像加载到纹理缓存中</h3>
<p>由于pixi使用webGL在GPU上渲染图片,所以图片需要采用GPU能够处理的格式。使用webGL渲染的图像就被叫做纹理。在你让精灵显示图片之前,需要将普通的图片转化成WebGL纹理。为了让所有东西在幕后快速有效地工作,Pixi使用纹理缓存来存储和引用精灵所需的所有纹理。纹理的名称是与它们引用的图像的文件位置匹配的字符串。 这意味着如果你有一个从"images/cat.png"加载的纹理,你可以在纹理缓存中找到它,如下所示:</p>
<pre><code>PIXI.utils.TextureCache["images/cat.png"];</code></pre>
<p>纹理以WebGL兼容格式存储,这对于Pixi的渲染器来说非常有效。然后,您可以使用Pixi的Sprite类使用纹理制作新的精灵。</p>
<pre><code>let texture = PIXI.utils.TextureCache["images/anySpriteImage.png"];
let sprite = new PIXI.Sprite(texture);</code></pre>
<p>但是如何加载图像文件并将其转换为纹理?使用Pixi的内置loader对象。Pixi强大的loader对象是加载任何类型图片所需的全部内容。以下是如何使用它来加载图像并在图像加载完成后调用一个名为setup的函数:</p>
<pre><code>PIXI.loader.add("images/anyImage.png").load(setup);
function setup() {
//此代码将在加载程序加载完图像时运行
}</code></pre>
<p>如果使用loader来加载图像,这是<a href="https://link.segmentfault.com/?enc=YZFI0AhUkxVBg9BLZEGmqw%3D%3D.boM7NhJuC7JHScqUbfJ2zRgTvvDgCNC%2BgxqsVbZa57gopW%2BjB89eh2FOQFmqPbdyESfAT%2FIN4TwQYKduivu1NbBVJvrnni66PcLWct4ncTQ%3D" rel="nofollow">pixi开发团队的建议</a>。我们应该通过引入loader's resources对象中的图片资源来创建精灵,就像如下这样:</p>
<pre><code>let sprite = new PIXI.Sprite(
//数组内是多张图片资源路径
PIXI.loader.resources["images/anyImage.png"].texture
);</code></pre>
<p>以下是一个完整的代码的例子,我们可以编写这些代码来加载图像,调用setup函数,然后从加载的图像中创建精灵。</p>
<pre><code>PIXI.loader.add("images/anyImage.png").load(setup);
function setup() {
let sprite = new PIXI.Sprite(
PIXI.loader.resources["images/anyImage.png"].texture
);
}
</code></pre>
<p>这是加载图像和创建图片的通用格式。我们还可以使用链式调用的add方法来同时加载多个图像,如下:</p>
<pre><code>PIXI.loader.add("images/imageOne.png").add("images/imageTwo.png").add("images/imageThree.png").load(setup);</code></pre>
<p>当然,更好的方式是,只需在单个add方法内将要加载的所有文件列出在数组中,如下所示:</p>
<pre><code>PIXI.loader.add(["images/imageOne.png","images/imageTwo.png","images/imageThree.png"]).load(setup);</code></pre>
<p>当然加载程序还允许我们加载JSON文件。这在后面,我们会学到。</p>
<h3>3.显示精灵</h3>
<p>在我们已经加载了图片,并且将图片制作成了精灵,我们需要使用stage.addChild方法来将精灵添加到pixi的stage(舞台)中。就像如下这样:</p>
<pre><code>//cat表示精灵变量
app.stage.addChild(cat);
</code></pre>
<p>注: 记住stage(舞台)是所有被添加的精灵的主容器元素。而且我们不会看到任何我们所构建的精灵,除非我们已经把它们添加到了stage(舞台)中。</p>
<p>好的,让我们来看一个示例关于如何使用代码来学会在舞台上显示一张单图图像。我们假定在目录examples/images下,你会发现有一张宽64px高64px的猫图。如下所示:</p>
<p><img src="/img/remote/1460000020692076?w=64&h=64" alt="" title=""></p>
<p>以下是一个需要加载图片,创建一个精灵,并且显示在pixi的stage上的所有JavaScript代码:</p>
<pre><code>//创建一个pixi应用对象
let app = new PIXI.Application({
width: 256,
height: 256,
antialias: true,
transparent: false,
resolution: 1
}
);
//将canvas元素添加到body元素中
document.body.appendChild(app.view);
//加载图像,然后使用setup方法运行
//"images/cat.png"这个路径需要根据自己情况所调整
PIXI.loader.add("images/cat.png").load(setup);
//当图片加载完成,setup方法会执行
function setup() {
//创建cat精灵,"images/cat.png"这个路径需要根据自己情况所调整
let cat = new PIXI.Sprite(PIXI.loader.resources["images/cat.png"].texture);
//将cat精灵添加到舞台中
app.stage.addChild(cat);
}</code></pre>
<p>当代码运行在浏览器上,你会看到如图所示:</p>
<p><img src="/img/remote/1460000020692077?w=822&h=588" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=bjTpNi7fgUhAyj%2BNvEJAKw%3D%3D.%2Fxy%2BouKsxDMLmDAXJU1of%2Bf9lRk4Mybk3nlso0oztkpl9Nrwt2vmLhCUhwspSc8psWrT7Tbm6eKR9Z1gsuLcy1U%2BUrtrgrhVO3y%2BqlaQZIc%3D" rel="nofollow">在线示例</a>。</p>
<p>如果我们需要从舞台上移除精灵,我们可以使用removeChild方法。如下:</p>
<pre><code>//参数为精灵图的路径
app.stage.removeChild(anySprite)</code></pre>
<p>但是通常将精灵的visible属性设置为false将是使精灵消失的更简单,更有效的方法。如下:</p>
<pre><code> //anySprite为精灵对象,例如前面示例的cat
anySprite.visible = false;</code></pre>
<h3>4.使用别名</h3>
<p>当然我们也可以对我们使用频繁的pixi对象和方法创建一些简略的可读性更好的别名。例如,你难道想给所有的pixi对象添加PIXI前缀吗?如果不这样想,那就给它一个简短的别名吧。例如:以下是为TextureCache对象所创建的一个别名。</p>
<pre><code> let TextureCache = PIXI.utils.TextureCache;
</code></pre>
<p>然后,使用该别名代替原始别名,如下所示:</p>
<pre><code>//"images/cat.png"这个路径需要根据自己情况所调整
let texture = TextureCache["images/cat.png"];</code></pre>
<p>使用别名给写出简洁的代码提供了额外的好处:他帮助你缓存了Pixi的常用API。如果Pixi的API在将来的版本里改变了-没准他真的会变!你将会需要在一个地方更新这些对象和方法,你只用在工程的开头而不是所有的实例那里!所以Pixi的开发团队想要改变它的时候,你只用一步即可完成这个操作! 来看看怎么将所有的Pixi对象和方法改成别名之后,来重写加载和显示图像的代码。</p>
<pre><code> //别名
let Application = PIXI.Application;
let loader = PIXI.loader;
let resources = PIXI.loader.resources;
let Sprite = PIXI.Sprite;
//创建一个应用对象
let app = new Application({
width: 256,
height: 256,
antialias: true,
transparent: false,
resolution: 1
}
);
//将Pixi自动为您创建的画布添加到HTML文档中
document.body.appendChild(app.view);
//加载图像并完成后运行“setup”函数
loader.add("images/cat.png").load(setup);
//该“setup”函数将在图像加载后运行
function setup() {
//创建一个cat精灵类
let cat = new Sprite(resources["images/cat.png"].texture);
//将cat精灵类添加到舞台中
app.stage.addChild(cat);
}</code></pre>
<p>大多数教程中的例子将会使用Pixi的别名来处理。除非另有说明,否则你可以假定下面所有的代码都使用了这些别名。这就是我们所需要知道的所有的关于加载图像和创建精灵的知识。</p>
<p><a href="https://link.segmentfault.com/?enc=0%2FaogdGiLNm1jN5KZYq0nA%3D%3D.4sVcY4vTp9Oqd2nrmk3N88KCmZYX%2BVGc7s4pPcsKgcMBDg6M0sSG7PO%2F%2F6DIGfRZwP3DjwafM7Dyh7x9lk3HiUm7HnY%2BOtCBV2eIOnw395U%3D" rel="nofollow">在线示例</a></p>
<h3>5.有关加载的更多信息</h3>
<p>前面所显示的格式是建议用作加载图像和显示图片的标准模板的格式。 因此,我们可以放心地忽略接下来的几段内容,而直接跳到下一部分“定位精灵”。 但是Pixi的加载程序对象非常复杂,即使您不定期使用它们,也要注意一些功能。 让我们看一些最有用的。</p>
<h4>(1).从普通的JavaScript Image对象或Canvas生成精灵</h4>
<p>为了优化和提高效率,始终最好从预先加载到Pixi的纹理缓存中的纹理制作精灵。 但是,如果由于某种原因需要从常规的JavaScript Image对象制作纹理,则可以使用Pixi的BaseTexture和Texture类来实现:</p>
<pre><code>//参数为任何JavaScriptImage对象
let base = new PIXI.BaseTexture(anyImageObject);
let texture = new PIXI.Texture(base);
let sprite = new PIXI.Sprite(texture);</code></pre>
<p>如果要从任何现有的canvas元素制作纹理,可以使用BaseTexture.fromCanvas:</p>
<pre><code>//参数为任何canvas元素
let base = new PIXI.BaseTexture.fromCanvas(anyCanvasElement);</code></pre>
<p>如果要更改精灵显示的纹理,请使用texture属性。 将其设置为任何texture对象,如下所示:</p>
<pre><code>anySprite.texture = PIXI.utils.TextureCache["anyTexture.png"];
</code></pre>
<p>如果游戏中发生重大变化,就可以使用此技术交互式地更改精灵的外观。</p>
<h4>(2).为加载文件分配名称</h4>
<p>可以为要加载的每个资源分配一个唯一的名称。只需提供名称(字符串)作为add方法中的第一个参数即可。 例如,以下是将cat的图像命名为catImage的方法。</p>
<pre><code>//第一个参数为分配的别名,第二个参数则是图像路径
PIXI.loader.add("catImage", "images/cat.png").load(setup);</code></pre>
<p>这将在loader.resources中创建一个名为catImage的对象。 这意味着可以通过引用catImage对象来创建一个精灵,如下所示:</p>
<pre><code>//catImage对象下的texture属性
let cat = new PIXI.Sprite(PIXI.loader.resources.catImage.texture);</code></pre>
<p>但是,建议不要使用此功能! 这是因为使用它就必须记住为每个已加载文件指定的所有名称,并确保不要意外地多次使用同一名称。正如在前面的示例中所做的那样,使用文件路径名更加简单,并且不容易出错。</p>
<h4>(3).监听加载进度</h4>
<p>Pixi的加载程序有一个特殊的progress事件,它将调用一个可自定义的函数,该函数将在每次文件加载时运行。进度事件由加载器的on方法调用,如下所示:</p>
<pre><code>//loadProgressHandler为处理进度的函数
PIXI.loader.on("progress", loadProgressHandler);
</code></pre>
<p>以下为在加载链中使用包括on方法的方式,并在每次文件加载时调用用户定义的函数loadProgressHandler。</p>
<pre><code> //使用on方法
PIXI.loader.add([
"images/one.png",
"images/two.png",
"images/three.png"
]).on("progress", loadProgressHandler).load(setup);
//loadProgressHandler函数
function loadProgressHandler() {
console.log("loading");
}
//setup函数
function setup() {
console.log("setup");
}</code></pre>
<p>每次加载其中一个文件时,进度事件都会调用loadProgressHandler以在控制台中显示“loading”。当所有三个文件都加载完毕后,setup函数将运行。 以下是上述代码在控制台中的输出:</p>
<pre><code>loading
loading
loading
setup</code></pre>
<p>这很不错了,但是会变得更好。我们还可以准确地找到已加载的文件以及当前已加载的文件总数的百分比。只需要通过向loadProgressHandler添加可选的loader和resource参数来做到这一点,如下所示:</p>
<pre><code>function loadProgressHandler(loader, resource) {
//从resouce中取得已加载的文件或者取得已加载文件的百分比
}</code></pre>
<p>然后,可以使用resource.url查找当前加载的文件。(如果要查找可能已分配给文件的可选名称,请使用resource.name作为add方法中的第一个参数。)然后,您可以使用loader.progress查找当前已加载的总资源百分比。以下是一些执行此操作的代码。</p>
<pre><code> PIXI.loader.add([
"images/one.png",
"images/two.png",
"images/three.png"
]).on("progress", loadProgressHandler).load(setup);
function loadProgressHandler(loader, resource) {
//显示当前加载的文件路径
console.log("loading: " + resource.url);
//显示当前文件加载的百分比
console.log("progress: " + loader.progress + "%");
//如果第一个参数提供的是文件的可选名称
//那么在add方法里就要像如下这样接收它们
//console.log("loading:"+resource.name);
}
function setup() {
console.log("All files loaded");
}</code></pre>
<p>以下是此代码在运行时将在控制台中显示的内容:</p>
<pre><code>loading: images/one.png
progress: 33.333333333333336%
loading: images/two.png
progress: 66.66666666666667%
loading: images/three.png
progress: 100%
All files loaded</code></pre>
<p>这确实好棒,因为我们可以以此为基础创建加载进度条。</p>
<p>注意: 我们也可以在资源对象上访问其他属性。resource.error会告诉您尝试加载文件时发生的任何可能的错误。resource.data允许您访问文件的原始二进制数据。</p>
<h3>6.有关loader的更多信息</h3>
<p>Pixi的loader具有丰富的功能和可配置性。让我们快速了解一下它的用法,好入门。 loader的可链接的add方法包含4个基本参数:</p>
<pre><code>add(name, url, optionObject, callbackFunction);</code></pre>
<p>以下是对这些参数做描述的简单文档:</p>
<pre><code>1.name (string):要加载的资源的名称。如果未被使用,则会自动使用url。
2.url (string):此资源的网址,相对于loader的baseUrl。
3.options (object literal):加载的选项。
4.options.crossOrigin (Boolean):请求是跨域的吗? 默认为自动确定。
5.options.loadType:资源应如何加载? 默认值为Resource.LOAD_TYPE.XHR。
6.options.xhrType:使用XHR时应如何执行正在加载的数据?默认值为Resource.XHR_RESPONSE_TYPE.DEFAULT。
7.callbackFunction:当资源完成加载时所要调用的函数(回调函数)。
</code></pre>
<p>这些参数中唯一需要传入的就是url(要加载的文件)。以下是一些可以使用add方法加载文件的方式的示例。 这是文档称为loader的“常规语法”:</p>
<pre><code>//第一个参数为加载资源的名称,第二个参数为资源路径,然后第三个参数可不传,也就是加载的选项,第四个参数就是回调函数
PIXI.loader.add('key', 'http://...', function () {});
PIXI.loader.add('http://...', function () {});
PIXI.loader.add('http://...');</code></pre>
<p>以下这些是loader的“对象语法”的示例:</p>
<pre><code>PIXI.loader.add({
name: 'key2',
url: 'http://...'
}, function () {})
PIXI.loader.add({
url: 'http://...'
}, function () {})
PIXI.loader.add({
name: 'key3',
url: 'http://...'
onComplete: function () {}
})
PIXI.loader.add({
url: 'https://...',
onComplete: function () {},
crossOrigin: true
})</code></pre>
<p>您还可以将对象或URL或两者的数组传递给add方法:</p>
<pre><code>PIXI.loader.add([
{name: 'key4', url: 'http://...', onComplete: function () {} },
{url: 'http://...', onComplete: function () {} },
'http://...'
]);</code></pre>
<p>注意: 如果需要重置loader以加载新一批文件,请调用loader的reset方法:PIXI.loader.reset()。</p>
<p>Pixi的loader具有许多更高级的功能,包括使我们可以加载和解析所有类型的二进制文件的选项。这不是我们日常要做的事情,并且超出了目前我们所学习的范围,因此可以<a href="https://link.segmentfault.com/?enc=emz3VrLYM8egl8TdzFqScA%3D%3D.LLGHuNAg4lgL9BxhMJ%2Fl509PEhESOPkMJ%2BHqx4QhW1%2F2%2FHBRDBDIWrNi3hpFLLRR" rel="nofollow">从GitHub项目中获取更多信息</a>。</p>
<h3>7.定位精灵</h3>
<p>现在我们知道了如何创建和显示精灵,让我们了解如何放置和调整精灵的大小。在前面的示例中,cat sprite已添加到舞台的左上角。cat的x位置为0,y位置为0。可以通过更改cat的x和y属性的值来更改cat的位置。通过将cat的x和y属性值设置为96的方法,可以使cat在舞台中居中。</p>
<pre><code>cat.x = 96;
cat.y = 96;</code></pre>
<p>创建精灵后,将以上两行代码添加到setup函数内的任何位置。</p>
<pre><code>function setup() {
//创建cat精灵
let cat = new Sprite(resources["images/cat.png"].texture);
//改变精灵的位置
cat.x = 96;
cat.y = 96;
//将cat精灵添加到舞台中如此便可以看到它
app.stage.addChild(cat);
}</code></pre>
<p>注意: 在这个例子中,Sprite是PIXI的别名。Sprite,TextureCache是PIXI.utils.TextureCache的别名,resources是PIXI.loader.resources的别名。后面都是使用别名,并且从现在开始,示例代码中所有Pixi对象和方法的格式都相同。</p>
<p>这两行代码会将cat右移96像素,向下移96像素。结果如下:</p>
<p><img src="/img/remote/1460000020692078?w=480&h=462" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=pK0qrEAF1TAnOv%2Fxt%2BYwjg%3D%3D.r1%2B9xxt9B88CcZCQ2Q0dFzL7DY%2BE751%2Fm3Zq8YqWLx9%2Fo9i7iLD0IKu1HbqTaqPQP6F6qhC6qAeD%2FhrtGPp8fDMyVqbUNllV5enF0rdVr4o%3D" rel="nofollow">在线示例</a>。</p>
<p>cat的左上角(左耳)代表其x和y锚点。要使cat向右移动,请增加其x属性的值。要使cat向下移动,请增加其y属性的值。如果cat的x值为0,则它将位于舞台的最左侧。如果y值为0,则它将位于该阶段的顶部。如下图所示:</p>
<p><img src="/img/remote/1460000020692080?w=398&h=398" alt="" title=""></p>
<p>其实可以不必单独设置精灵的x和y属性,而是可以在一行代码中将它们一起设置,如下所示:</p>
<pre><code>//也就是调用set方法即可,传入修改的x参数和y参数
sprite.position.set(x, y)</code></pre>
<p>让我们来看看以上的示例代码修改之后的结果:</p>
<p><img src="/img/remote/1460000020692078?w=480&h=462" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=%2FQtMqJRhzEiUsfQ%2Fc9osVg%3D%3D.87vswcJW5zsRmDjtPBZndq6olWZjv6zFjGD3JUK2CCMKH7NRNJf4wrzQipvNBrcmXU9S6qXJrO%2BT83EOyz8Mt53R6lBQDwJO6CdBcMNjBMQ%3D" rel="nofollow">在线示例</a>。</p>
<p>可以看出来,结果都是一样的。</p>
<h3>8.大小和比例</h3>
<p>我们可以通过设置精灵的width和height属性来更改其大小。以下便是一个示例,将cat设置为80像素的宽度和120像素的高度。</p>
<pre><code> cat.width = 80;
cat.height = 120;</code></pre>
<p>将这两行代码添加到setup函数中,就像如下:</p>
<pre><code> function setup() {
//创建cat精灵
let cat = new Sprite(resources["images/cat.png"].texture);
//改变精灵的位置
cat.x = 96;
cat.y = 96;
//改变精灵的大小
cat.width = 80;
cat.height = 120;
//将cat精灵添加到舞台中如此便可以看到它
app.stage.addChild(cat);
}</code></pre>
<p>效果如图所示:</p>
<p><img src="/img/remote/1460000020692081?w=393&h=379" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=7C0jJIuJZY3UAMlEveTZlQ%3D%3D.2Tl6mSSZC23bsLuzrhgHi6EgWU6Ao7WMMRseWTuqv6OIYF834nfWtr%2FXfTYzucBggvuMTtJ%2FJT7nVDPWD0EhbfmiqvQFvZUxJq%2Ft3H40mgY%3D" rel="nofollow">在线示例</a>。</p>
<p>我们会看到cat的位置(左上角)没有变化,只是宽度和高度有变化。如下图所示:</p>
<p><img src="/img/remote/1460000020692082?w=267&h=266" alt="" title=""></p>
<p>精灵还具有scale.x和scale.y属性,可按比例更改精灵的宽度和高度。以下是将cat的scale设置为一半尺寸的方法:</p>
<pre><code> cat.scale.x = 0.5;
cat.scale.y = 0.5;</code></pre>
<p>scale是介于0和1之间的数字,代表精灵大小的百分比。1表示100%(原尺寸),而0.5表示50%(半尺寸)。您可以通过将精灵的scale设置为2来使精灵大小增加一倍,如下所示:</p>
<pre><code> cat.scale.x = 2;
cat.scale.y = 2;</code></pre>
<p>Pixi提供了另一种简洁的方法,您可以使用scale.set方法在一行代码中设置精灵的缩放比例。</p>
<pre><code> //注意参数代表的意思
cat.scale.set(0.5, 0.5);</code></pre>
<p>如果喜欢这样使用,那就这样用吧!我们来看一个完整的示例:</p>
<pre><code> //别名
let Application = PIXI.Application;
let loader = PIXI.loader;
let resources = PIXI.loader.resources;
let Sprite = PIXI.Sprite;
//创建一个应用对象
let app = new Application({
width: 256,
height: 256,
antialias: true,
transparent: false,
resolution: 1
});
//将Pixi自动为您创建的画布添加到HTML文档中
document.body.appendChild(app.view);
//加载图像并完成后运行“setup”函数
loader.add("/static/page/PIXIJS/images/cat.png").load(setup);
//该“setup”函数将在图像加载后运行
function setup() {
//创建cat精灵
let cat = new Sprite(resources["/static/page/PIXIJS/images/cat.png"].texture);
//改变精灵的位置
cat.position.set(96, 96);
//改变精灵的大小
cat.scale.set(0.5,0.5);
//或者这样使用
//cat.scale.x=0.5
//cat.scale.y=0.5
//将cat精灵添加到舞台中如此便可以看到它
app.stage.addChild(cat);
}</code></pre>
<p>运行效果如图所示:</p>
<p><img src="/img/remote/1460000020692083?w=383&h=389" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=9orqT%2BS5ufB2d8v3tkIEZg%3D%3D.DsepKdiUn3XZaS0Zw7wDqVWJzi83Wu8epWXGBXH2Igi2w11Y19FZcSwV5xa%2FbqHaMJd%2B%2FYlrAYJROizng39ko0Ybn1K7nlzwL48wj4x8LIY%3D" rel="nofollow">在线示例</a>。</p>
<h3>9.旋转</h3>
<p>我们也可以通过将精灵的rotation属性设置为以弧度为单位的值来使其旋转。如下所示:</p>
<pre><code> cat.rotation = 0.5;</code></pre>
<p>但是旋转发生在哪一点附近?我们可以从下图中看到精灵的左上角代表其x和y位置。该点称为锚点。 如果将精灵的rotation属性设置为0.5,则旋转将围绕精灵的锚点进行。我们也会知道这将对我们的cat精灵产生什么影响。</p>
<p><img src="/img/remote/1460000020692084?w=188&h=139" alt="" title=""><br>我们会看到锚点,即cat的左耳,是cat围绕其旋转的假想圆的中心。 如果要让精灵围绕其中心旋转怎么办?更改精灵的锚点,使其居中,如下所示:</p>
<pre><code> //anchor就是锚点
cat.anchor.x = 0.5;
cat.anchor.y = 0.5;</code></pre>
<p>anchor.x和anchor.y值代表纹理尺寸的百分比,范围为0到1(0%到100%)。将其设置为0.5可使纹理在该点上居中。点本身的位置不会改变,只是纹理在其上定位的方式一样。下一张图显示了如果将居中的锚点居中,旋转的精灵会发生什么。</p>
<p><img src="/img/remote/1460000020692085?w=336&h=204" alt="" title=""></p>
<p>我们会看到精灵的纹理向上和向左移动。这是要记住的重要副作用!就像position和scale一样,我们也可以使用以下一行代码来设置锚点的x和y值:</p>
<pre><code> //注意参数即可
cat.anchor.set(x, y);</code></pre>
<p>精灵还具有pivot属性,其作用方式类似于anchor。 pivot设置精灵的x / y原点的位置。 如果更改轴心点然后旋转精灵,它将围绕该原点旋转。例如,下面的代码将把精灵的pivot.x指向32,将其pivot.y指向32。</p>
<pre><code> //注意参数的意义
cat.pivot.set(32, 32);</code></pre>
<p>假设精灵为64x64像素,则精灵现在将围绕其中心点旋转。但是请记住:如果更改了精灵的pivot,则还更改了它的x / y原点。那么,anchor和pivot点有什么区别?他们真的很相似!anchor使用0到1归一化的值移动精灵图像纹理的原点。pivot使用像素值移动精灵的x和y的原点。我们应该使用哪个?由我们自己决定。喜欢用哪个就用哪个即可。让我们来看看使用这两个属性的完整示例吧!</p>
<p><a href="https://link.segmentfault.com/?enc=CtcWR%2F62JF55mBWr0eW%2Fww%3D%3D.4W9LyM%2BrxJkjgwO1Fa3nlDoBzqD0ofztPyL2UIj6uExuWbQnLiLZ1YcXxd3dVdCRL1dPN%2FmUq3hb%2FzUq7Wq%2Fx1N07c9mKD1kMvjjleHV0WY%3D" rel="nofollow">第一个示例</a>。<a href="https://link.segmentfault.com/?enc=IZDfM7f7d%2BieFRcNlRulmw%3D%3D.DJiY%2FANWtPkl%2Bkc9TmTNHAVFSVAeXhUS7k5xliFemTSMkouTWo5CHb8Q2qgZQ0r5iK46dA7zqXlOFpCvTvs%2FxN8dfi3b%2BSWmyTZs58hUWyk%3D" rel="nofollow">第二个示例</a>。<a href="https://link.segmentfault.com/?enc=JUb4tieEvuwmlqAOor8b%2FA%3D%3D.3dpeia4TED0%2F4Boj2zfBW66jUSgy1pViioZ3suWf%2Blm2F2KUFTqWXwOeGf93hlWjTqN%2BQDcE7TN%2FRwQsCaCoJCjy1o0%2BGEpyUY%2BVKn3zR7U%3D" rel="nofollow">第三个示例</a>。</p>
<h3>10.从精灵雪碧图中制作精灵</h3>
<p>现在,我们也知道了如何从单个图像文件制作精灵。但是,作为游戏设计师,通常会使用雪碧图(也称为精灵图)来制作精灵。Pixi具有一些方便的内置方法来帮助我们完成此任务。所谓的雪碧图就是包含子图像的单个图像文件。子图像代表要在游戏中使用的所有图形。以下是图块图像的示例,其中包含游戏角色和游戏对象作为子图像。</p>
<p><img src="/img/remote/1460000020692086?w=192&h=192" alt="" title=""></p>
<p>整个雪碧图为192 x 192像素。每个图像都位于其自己的32 x 32像素网格单元中。在图块上存储和访问所有游戏图形是一种处理图形的非常高效的处理器和内存方式,Pixi为此进行了优化。 我们可以通过定义与我们要提取的子图像大小和位置相同的矩形区域,来从雪碧图中捕获子图像。以下是从雪碧图中提取的火箭子图像的示例。</p>
<p><img src="/img/remote/1460000020692087?w=536&h=306" alt="" title=""></p>
<p>让我们看看执行此操作的代码。首先,就像在前面的示例中所做的那样,使用Pixi的loader加载tileset.png图像。</p>
<pre><code> //注意这里的路径依据实际情况来修改调整
loader.add("images/tileset.png").load(setup);</code></pre>
<p>接下来,在加载图像后,使用雪碧图的矩形块来创建精灵的图像。以下是提取子图像,创建火箭精灵并将其定位并显示在画布上的代码。</p>
<pre><code> function setup() {
//从纹理创建“tileset”精灵
let texture = TextureCache["images/tileset.png"];
//创建一个定义位置矩形对象
//并且要从纹理中提取的子图像的大小
//`Rectangle`是`PIXI.Rectangle`的别名,注意这里的参数,后续会详解,参数值与实际情况有关
let rectangle = new Rectangle(192, 128, 64, 64);
//告诉纹理使用该矩形块
texture.frame = rectangle;
//从纹理中创建一个精灵
let rocket = new Sprite(texture);
//定位火箭精灵在canvas画布上
rocket.x = 32;
rocket.y = 32;
//将火箭精灵添加到舞台中
app.stage.addChild(rocket);
//重新渲染舞台
app.renderer.render(app.stage);
}</code></pre>
<p>这是如何工作的?Pixi具有内置的Rectangle对象(PIXI.Rectangle),它是用于定义矩形形状的通用对象。它有四个参数。前两个参数定义矩形的x和y位置。最后两个定义其宽度和高度。这是定义新Rectangle对象的格式。</p>
<pre><code> let rectangle = new PIXI.Rectangle(x, y, width, height);</code></pre>
<p>矩形对象只是一个数据对象。由我们自己来决定如何使用它。在我们的示例中,我们使用它来定义要提取的图块上的子图像的位置和区域。Pixi纹理具有一个有用的属性,称为frame,可以将其设置为任何Rectangle对象。frame将纹理裁剪为矩形的尺寸。以下是使用frame将纹理裁剪为火箭的大小和位置的方法。</p>
<pre><code> let rectangle = new Rectangle(192, 128, 64, 64);
texture.frame = rectangle;</code></pre>
<p>然后,我们就可以使用该裁剪的纹理来创建精灵:</p>
<pre><code> let rocket = new Sprite(texture);</code></pre>
<p>然后它就开始运行啦。由于我们会频繁地使用雪碧图制作精灵纹理,因此Pixi提供了一种更方便的方法来帮助我们完成此任务-让我们继续下一步。</p>
<p><a href="https://link.segmentfault.com/?enc=E8A1GlAaHgCWsuCuE%2B8ZEw%3D%3D.rw1wppx%2Fu2urMl6sdEdsOsyA2Vpr4fEooQ6YHH8JDn5HmlTf4frj6%2B46%2FAcahYr4B%2BZoBujt6xINpOYligOP9z%2BPtQ%2FanPf%2B3Bn%2FSeqB%2FIg%3D" rel="nofollow">在线示例</a>。</p>
<h3>11.使用纹理图集</h3>
<p>如果我们是开发大型复杂的游戏,则需要一种快速有效的方法来从雪碧图创建精灵。这是纹理图集真正有用的地方。纹理图集是JSON数据文件,其中包含匹配的图块PNG图像上子图像的位置和大小。如果使用纹理图集,那么关于要显示的子图像,我们所需要知道的就是它的名称。我们可以按任何顺序排列雪碧图图像,JSON文件将为我们跟踪其大小和位置。这真的很方便,因为这意味雪碧图图片的大小和位置不会硬编码到我们的游戏程序中。如果我们对雪碧图进行更改(例如添加图像,调整图像大小或将其删除),则只需重新发布JSON文件,我们的游戏就会使用该数据显示正确的图像。我们无需对游戏代码进行任何更改。</p>
<p>Pixi与一种流行的名为<a href="https://link.segmentfault.com/?enc=UAdC%2FpaSSmAbOB2hwSBg0Q%3D%3D.g4IcyGuXOlH97z%2BAcZnzhzxEODaI81I5Z1DHZ1K13eTy1bqDV3eTkuDqN24AVvLT" rel="nofollow">Texture Packer</a>的软件工具输出的标准JSON纹理图集格式兼容。Texture Packer的“基本”许可证是免费的。让我们了解如何使用它制作纹理图集,并将该图集加载到Pixi中。(我们也可以不必使用Texture Packer。类似的工具(例如<a href="https://link.segmentfault.com/?enc=6rONcbzJSoAJ6GvIlQLJ0w%3D%3D.vM7GigkOi8fxbab2o6yvlvx%2FZJZOFbiBWAKgW3dEk1A%3D" rel="nofollow">Shoebox</a>或<a href="https://link.segmentfault.com/?enc=0%2FdvWMiQn5BTzmYziHxPqw%3D%3D.kwlcfuyr9C0o%2BbeL66idtNyF2USNC6z7f30M6aQHD9q9vLRQsio%2FOqwgTQ%2BjnCJ9" rel="nofollow">spritesheet.js</a>)可以以与Pixi兼容的标准格式输出PNG和JSON文件。)</p>
<p>首先,要收集在游戏中使用的单个图像文件。</p>
<p><img src="/img/remote/1460000020692088?w=587&h=279" alt="" title=""></p>
<p>注: (本文中的所有图像均由Lanea Zimmerman创建。您可以在<a href="https://link.segmentfault.com/?enc=dF2bzDgTtmox1MugPj92FA%3D%3D.pi1CotYQ0WpipP2mYdynDpAMx9rXRoAlTiZhd9BqTH0YrnxYT6p2CFGb1f%2Bl%2FrrL" rel="nofollow">此处</a>找到她的更多作品。谢谢Lanea Zimmerman!)</p>
<p>接下来,打开Texture Packer,然后选择JSON Hash作为框架类型。将图像拖到Texture Packer的工作区中。(我们也可以将Texture Packer指向包含图像的任何文件夹。)它将自动将图像排列在单个雪碧图上,并为其提供与原始图像名称匹配的名称。</p>
<p><img src="/img/remote/1460000020692089?w=568&h=642" alt="" title=""></p>
<p>注:(如果使用的是免费版本的Texture Packer,则将Algorithm设置为Basic,将Trim mode模式设置为None,将Extrude设置为0,将Size constraints 设置为Any size,然后将PNG Opt Level一直滑到左边至0。这些是基本设置,可让免费版本的Texture Packer创建文件而没有任何警告或错误。)</p>
<p>完成后,点击Publish按钮。选择文件名和存储位置,然后保存发布的文件。 最终将获得2个文件:一个PNG文件和一个JSON文件。 在此示例中,文件名是treasureHunter.json和treasureHunter.png。为了简便点,只需将两个文件都保存在一个名为images的文件夹中。(可以将JSON文件视为图像文件的额外元数据,因此将两个文件都保留在同一文件夹中是很有意义的。)JSON文件描述了图集中每个子图像的名称,大小和位置。如以下摘录了一个文件内容,描述了Blob Monster(泡泡怪)子图像。</p>
<pre><code> "blob.png":
{
"frame": {"x":55,"y":2,"w":32,"h":24},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":24},
"sourceSize": {"w":32,"h":24},
"pivot": {"x":0.5,"y":0.5}
},</code></pre>
<p>treasureHunter.json文件还包含“dungeon.png”,“door.png”,“exit.png”和“explorer.png”属性,每个属性都具有相似的数据。这些子图像中的每一个都称为帧。拥有这些数据确实有帮助,因为现在无需知道纹理图集中每个子图像的大小和位置。只需要知道精灵的帧ID。帧ID只是原始图像文件的名称,例如“blob.png”或“explorer.png”。</p>
<p>使用纹理图集的众多优点之一是,可以轻松地在每个图像周围添加2个像素的填充(默认情况下,Texture Packer会这样做。)这对于防止纹理渗漏的可能性很重要。纹理出血(注:出血是排版和图片处理方面的专有名词,指在主要内容周围留空以便印刷或裁切)是当图块上相邻图像的边缘出现在精灵旁边时发生的一种效果。发生这种情况的原因是计算机的GPU(图形处理单元)决定如何舍入小数像素值的方式。它应该向上或向下取整?每个GPU都不同。在GPU上的图像周围留出1或2个像素的间距,可使所有图像始终显示一致。</p>
<p>注:(如果图形周围有两个像素填充,并且在Pixi的显示方式中仍然发现奇怪的“偏离一个像素”故障,请尝试更改纹理的缩放模式算法。方法如下:</p>
<pre><code> texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;</code></pre>
<p>。由于GPU浮点舍入错误,有时会发生这些故障。)</p>
<p>现在我们已经知道如何创建纹理图集,让我们了解如何将其加载到游戏代码中。</p>
<p>ps:关于以上的示例所涉及到的图片资源可点击<a href="https://link.segmentfault.com/?enc=Jm5KNuFPIAHqaNxiZNrOnQ%3D%3D.pDjCSq8D765NuzL3AzmNeulabQk7O8e7EiFa2s1OQUQkTSYGrUt55n8TY6E1qCmD1B%2Ba1TCGGPARASn02LxYdbz6QtxXWVVtABuEiwQMDt8%3D" rel="nofollow">此处</a>下载。</p>
<p>下图为本人使用Texture Packer创建的纹理图集的一个展示:</p>
<p><img src="/img/remote/1460000020692090?w=1316&h=788" alt="" title=""></p>
<p><img src="/img/remote/1460000020692091?w=512&h=544" alt="" title=""></p>
<p>可点击<a href="https://link.segmentfault.com/?enc=a%2B3ZkZerhzTt2FF%2FxPiBLw%3D%3D.JCwVuqY0Ft4xtiqvZpgJkjfD0MfkQQTXxWbFC2A7v4uywTDgempmNF0hBV0ZPpbfkneZSZz6wqJ86TqSwBwIgEDfBiA%2FZtVqAPBC8GckqUg%3D" rel="nofollow">此处(JSON)</a>, <a href="https://link.segmentfault.com/?enc=sAezrykQ6VY52yturwtldQ%3D%3D.5Xdty4%2F7QZ3nvyHk7ur1BAD3E7cKtaG%2FaQSP4XliS2N9OlXvwH%2Fow%2F9kflwk7Xp3sBWtGELPrcE63hY0msL6Ork6gPl3qu7heS%2F%2BLRGFIOM%3D" rel="nofollow">此处(png)</a>下载已经创建的纹理图集JSON文件和PNG文件。</p>
<h3>12.加载纹理图集</h3>
<p>可以使用Pixi的loader来加载纹理贴图集。如果是用Texture Packer生成的JSON,loader会自动读取数据,并对每一个帧创建纹理。下面就是怎么用loader来加载treasureHunter.json。当它成功加载,setup方法将会执行。</p>
<pre><code> //路径与实际项目有关
loader.add("images/treasureHunter.json").load(setup);</code></pre>
<p>现在,纹理图集上的每个图像都是Pixi缓存中的单个纹理。我们可以使用与Texture Packer中相同的名称(“ blob.png”,“ dungeon.png”,“ explorer.png”等)访问缓存中的每个纹理。</p>
<h3>13.从加载的纹理图集创建精灵。</h3>
<p>Pixi提供了三种从纹理图集创建精灵的方式:</p>
<p>1.使用TextureCache:</p>
<pre><code> let texture = TextureCache["frameId.png"],
sprite = new Sprite(texture);</code></pre>
<p>2.如果使用的是pixi的loader来加载纹理贴图集,则使用loader的 resources属性。</p>
<pre><code> let sprite = new Sprite(resources["images/treasureHunter.json"].textures["frameId.png"]);</code></pre>
<p>3.要创建一个精灵需要写太多东西了!所以建议给纹理贴图集的textures对象创建一个叫做id的别名,就像是这样:</p>
<pre><code> let id = PIXI.loader.resources["images/treasureHunter.json"].textures;</code></pre>
<p>现在就可以像这样实例化一个精灵了:</p>
<pre><code> let sprite = new Sprite(id["frameId.png"]);</code></pre>
<p>这真的太棒了!</p>
<p>以下为在setup函数中如何使用这三种不同的方法来创建和显示dungeon,explorer,和treasure精灵。</p>
<pre><code>//定义这三个变量,方便之后的使用
let textureId;
//地牢
let dungeon;
//探险者
let explorer;
//宝藏
let treasure;
//setup函数
function setup(){
//有3种不同的方式来创建和显示精灵
//第一种,使用纹理别名,TextureCache为PIXI.utils.TextureCache的别名
let dungeonTexture = TextureCache['dungeon.png'];
//Sprite为PIXI.Sprite的别名
dungeon = new Sprite(dungeonTexture);
//调用addChild方法将精灵添加到舞台中
app.stage.addChild(dungeon);
//第二种,使用resources来创建,也要注意参数根据实际情况来写
explorer = new Sprite(resources["images/treasureHunter.json"].textures['explorer.png']);
//将探险者的坐标设置一下,也就是设置探险者的位置,探险者在舞台中间,x方向距离随便设置
explorer.x = 68;
explorer.y = app.stage.height / 2 - explorer.height / 2;
app.stage.addChild(explorer);
//为所有的纹理图集创建一个别名
textureId = PIXI.loader.resources['images/treasureHunter.json'].textures;
treasure = new Sprite(textureId["treasure.png"]);
//将宝藏的坐标设置一下
treasure.x = app.stage.width - treasure.width - 48;
treasure.y = app.stage.height / 2 - treasure.height / 2;
//将宝藏精灵添加到舞台中去
app.stage.addChild(treasure);
}
</code></pre>
<p>下图为以上代码所展现的结果:</p>
<p><img src="/img/remote/1460000020692092?w=523&h=522" alt="" title=""></p>
<p>舞台尺寸为512 x 512像素,您可以在上面的代码中看到app.stage.height和app.stage.width属性用于对齐精灵。 以下是浏览器的y位置垂直居中的方式:</p>
<pre><code> explorer.y = app.stage.height / 2 - explorer.height / 2;</code></pre>
<p>学习使用纹理图集创建和显示精灵是一个重要的基本操作。因此,在继续之前,我们再来编写用于添加其余精灵的代码:blobs和exit,这样您便可以生成如下所示的场景:</p>
<p><img src="/img/remote/1460000020692093?w=523&h=522" alt="" title=""></p>
<p>以下是完成所有这些操作的全部代码。还包括了HTML代码,因此可以在适当的上下文中查看所有内容。(可以在<a href="https://link.segmentfault.com/?enc=nq0ELJEwDYghD9NNBEcrPg%3D%3D.apFNL4%2F2CjSePvmMyT804CrQVD%2BSC%2FoW%2Fo8HDfaobI717lG0QIAShBrFCaju6zXRUwn4VWLmR0RkYvtjmU%2BZhi3CQ2tXUIIFc%2BlOV3TToJU%3D" rel="nofollow">此处</a>下载代码。)请注意,已创建了blobs精灵,并将其添加到循环中的舞台上,并分配了随机位置。</p>
<pre><code> <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>从纹理图中创建精灵</title>
</head>
<body>
<script src="https://www.eveningwater.com/static/data/web/PixiJS/source/dist/pixi.min.js"></script>
<script>
//别名
let Application = PIXI.Application,
Container = PIXI.Container,
loader = PIXI.loader,
resources = PIXI.loader.resources,
TextureCache = PIXI.utils.TextureCache,
Sprite = PIXI.Sprite,
Rectangle = PIXI.Rectangle;
//创建pixi应用对象
let app = new Application({
width: 512,
height: 512,
antialiasing: true,
transparent: false,
resolution: 1
});
//将应用对象添加到dom中
document.body.appendChild(app.view);
//加载json文件,并在加载完成之后执行setup函数,注意这里的json文件路径,后面的也是
loader.add("./texture.json").load(setup);
//定义一些需要用到的变量
let dungeon, explorer, treasure, door, textureId;
function setup() {
//以下分别使用三种不同的方式来创建精灵
//第一种
let dungeonTexture = TextureCache["dungeon.png"];
dungeon = new Sprite(dungeonTexture);
app.stage.addChild(dungeon);
//第二种
explorer = new Sprite(
resources["./texture.json"].textures["explorer.png"]
);
explorer.x = 68;
//设置探险者的位置
explorer.y = app.stage.height / 2 - explorer.height / 2;
app.stage.addChild(explorer);
//第三种
textureId = PIXI.loader.resources["./texture.json"].textures;
treasure = new Sprite(textureId["treasure.png"]);
//设置宝藏的位置
treasure.x = app.stage.width - treasure.width - 48;
treasure.y = app.stage.height / 2 - treasure.height / 2;
app.stage.addChild(treasure);
//创建出口的精灵
door = new Sprite(textureId["door.png"]);
door.position.set(32, 0);
app.stage.addChild(door);
//制作泡泡怪精灵
let numberOfBlobs = 6,//数量
spacing = 48,//位置
xOffset = 150;//偏移距离
//根据泡泡怪精灵的数量来制作精灵
for (let i = 0; i < numberOfBlobs; i++) {
let blob = new Sprite(textureId["blob.png"]);
let x = spacing * i + xOffset;
//随机生成泡泡怪的位置
let y = randomInt(0, app.stage.height - blob.height);
// 设置泡泡怪的位置
blob.x = x;
blob.y = y;
//将泡泡怪添加到舞台中
app.stage.addChild(blob);
}
}
//随机生成的函数
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
</script>
</body>
</html></code></pre>
<p><a href="https://link.segmentfault.com/?enc=O06xyQIUfZqs9ac%2B1nlhaQ%3D%3D.Zc%2BFENn57%2BOfCNiCVUY%2Fylh7NuPCMU9mITUBHFCxHX4o70K92wbJ32m3pIRTyYSGGXLGnme1nqAH7Wv0J2DaKwN9oC2OKrbE2E%2FtcJnjBXU%3D" rel="nofollow">在线示例</a>。</p>
<p>我们可以在上面的代码中看到所有的blob都是使用for循环创建的。每个blobs沿x轴均匀分布,如下所示:</p>
<pre><code> let x = spacing * i + xOffset;
blob.x = x;</code></pre>
<p>spacing的值为48,xOffset的值为150。这意味着第一个Blob的x位置为150。这会将其从舞台的左侧偏移150个像素。每个后续的Blob的x值将比循环的上一次迭代中创建的Blob大48个像素。这样沿着地牢地板从左到右创建了一条均匀分布的怪物线。</p>
<p>每个blob也被赋予一个随机的y位置。以下为执行此操作的代码:</p>
<pre><code> let y = randomInt(0, stage.height - blob.height);
blob.y = y;</code></pre>
<p>可以为blob的y位置分配介于0到512之间的任何随机数,512是stage.height的值。这在名为randomInt的自定义函数的帮助下起作用。randomInt返回一个随机数,该随机数在您提供的任何两个数字之间的范围内。</p>
<pre><code> //注意参数代表的意思
randomInt(lowestNumber, highestNumber);</code></pre>
<p>这意味着,如果您想要一个介于1到10之间的随机数,则可以这样获得:</p>
<pre><code> let randomNumber = randomInt(1, 10);</code></pre>
<p>以下是完成所有这些操作的randomInt函数定义:</p>
<pre><code> function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}</code></pre>
<p>randomInt是一个很好的用来做游戏的工具函数,在写游戏的时候会经常用到它。</p>
<h3>14.移动精灵</h3>
<p>我们现在知道了如何显示精灵,但是如何使它们移动呢?这很容易:使用Pixi的代码创建循环功能,这称为游戏循环。放入游戏循环中的任何代码都会每秒更新60次。可以编写以下代码来使cat精灵以每帧1个像素的速率向右移动。</p>
<pre><code> function setup() {
//开始游戏循环,创建一个这样的函数
//Pixi的`ticker`提供了一个delta参数
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta){
//是cat精灵移动1px
cat.x += 1;
}</code></pre>
<p>如果运行这段代码,我们将看到精灵逐渐移到舞台的右侧。</p>
<p><img src="/img/remote/1460000020692094?w=267&h=267" alt="" title=""></p>
<p>这是因为每次gameLoop运行时,它都会将cat的x位置加1。</p>
<pre><code> cat.x += 1;</code></pre>
<p>每一个你放进Pixi的ticker的函数都会每秒被执行60次。你可以看见函数里面提供了一个delta的内容,他是什么呢?delta的值代表帧的部分的延迟。可以把它添加到cat的位置,让cat的速度和帧率无关。下面是代码:</p>
<pre><code> cat.x += 1 + delta;</code></pre>
<p>是否选择添加此增量值在很大程度上是美学选择。而且只有当您的动画努力保持每秒60帧的一致显示速率时,效果才会真正显着(例如,如果它在慢速设备上运行,则可能会发生这种情况)。本文中的其余示例将不使用此增量值,但可以根据需要在自己的工作中随意使用它。 可以不必使用Pixi的代码来创建游戏循环。如果愿意的话,可以使用requestAnimationFrame,如下所示:</p>
<pre><code>function gameLoop() {
//每60秒执行一次游戏循环函数
requestAnimationFrame(gameLoop);
//移动cat精灵
cat.x += 1;
}
//开始游戏循环
gameLoop();
</code></pre>
<p>采用哪种方式随我们自己的喜好。这就是移动精灵所有的操作!只需在循环内以较小的增量更改任何sprite属性,它们就会随着时间推移进行动画处理。如果要让精灵沿相反的方向(向左)设置动画,只需给它指定一个负值即可,例如-1。</p>
<p>以下是以上示例的完整代码:</p>
<pre><code> //别名
let Application = PIXI.Application,
Container = PIXI.Container,
loader = PIXI.loader,
resources = PIXI.loader.resources,
TextureCache = PIXI.utils.TextureCache,
Sprite = PIXI.Sprite,
Rectangle = PIXI.Rectangle;
//创建应用对象
let app = new Application({
width: 256,
height: 256,
antialias: true,
transparent: false,
resolution: 1
});
//将应用对象添加到dom中
document.body.appendChild(app.view);
// 加载图像资源
loader.add("images/cat.png").load(setup);
//定义cat精灵
let cat;
function setup() {
//创建cat精灵
cat = new Sprite(resources["images/cat.png"].texture);
cat.y = 96;
app.stage.addChild(cat);
//开始游戏循环
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta) {
// 移动1像素
cat.x += 1;
//也可以使用增量
//cat.x += 1 + delta;
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=wpFHvlZwz%2BMiV56ew8ExEg%3D%3D.IQjy5tuqPKsVHS3emOLqzdbKDKd%2BHJYGkOs7ANMq39nM5T0mRA0em7Ezq5F4O4qKdDLvcNlktygpeJ1WRWHsOgMzB8YhFmoGemMaHSJ8iPg%3D" rel="nofollow">在线示例</a>。</p>
<p>注意: (cat变量需要在setup和gameLoop函数之外定义,以便可以在两个函数中进行访问。)</p>
<p>我们还可以为精灵的比例,旋转或大小设置动画-无论如何!我们将看到更多有关如何为精灵设置动画的示例。</p>
<h3>15.使用速度属性</h3>
<p>为了方便自己,给游戏增加点灵活性,最好使用两个速度属性(vx和vy)控制精灵的移动速度。vx用于设置精灵在x轴上(水平)的速度和方向。vy用于在y轴上(垂直)设置精灵的速度和方向。与其直接更改精灵的x和y值,不如先更新速度变量,然后将这些速度值分配给精灵。这是交互式游戏动画所需的额外的一个模块。</p>
<p>第一步是在精灵上创建vx和vy属性,并为其赋予初始值。</p>
<pre><code> cat.vx = 0;
cat.vy = 0;</code></pre>
<p>设置vx和vy的值为0意味着精灵并没有被移动(即静止)。接着,在游戏循环中,更新我们想要移动的vx和vy的速度值,然后把它们赋值给精灵的x和y属性。以下是使用这种方式来使得精灵每帧往右下方移动1像素的示例代码:</p>
<pre><code> function setup() {
//创建cat精灵
cat = new Sprite(resources["images/cat.png"].texture);
cat.y = 96;
cat.vx = 0;
cat.vy = 0;
app.stage.addChild(cat);
//开始游戏循环
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta){
//更新cat精灵的速度
cat.vx = 1;
cat.vy = 1;
//将速度属性值赋值给cat精灵的位置,即x和y坐标
cat.x += cat.vx;
cat.y += cat.vy;
}</code></pre>
<p>当运行以上的代码,cat精灵就会每帧往右下方移动1像素。如下图所示:</p>
<p><img src="/img/remote/1460000020692095?w=267&h=267" alt="" title=""></p>
<p>想要让cat精灵往不同方向移动吗?让cat精灵往左移动,那么就将vx 的值设置为负数,例如-1。想要让它往上移动,那么就将vy的值设置为负数,例如-1。想要让cat精灵移动的更快,只需要将vx和vy的值设置大一点,就像3,5,-2,-4。(负号代表方向)。</p>
<p>我们会看到如何通过利用vx和vy的速度值来模块化精灵的速度,它对游戏的键盘和鼠标控制系统很有帮助,而且更容易实现物理模拟。</p>
<p><a href="https://link.segmentfault.com/?enc=LGpCbl6J9wYfyBzD9qFhgA%3D%3D.UHSAgdRW86wpjdFlwLuYJ1PEEMvwT3fGNBB3qMyVanbEHpv20juFrJQJLAl1mDj%2FlQjdw6G1qAvBF2BSyLOW7tNxWSd7EnEOwYUXdfU8nVk%3D" rel="nofollow">在线示例</a>。</p>
<h3>16.游戏状态</h3>
<p>考虑到样式,也为了帮助模块化代码,个人建议还是像下面这样构造游戏循环:</p>
<pre><code> //定义一个变量设置游戏开始状态
let state = play;
app.ticker.add((delta) => { gameLoop(delta)});
//开始游戏循环
function gameLoop(delta){
//更改游戏状态
state(delta);
}
function play(delta){
cat.vx = 1;
cat.x += cat.vx;
}</code></pre>
<p>我们会看到gameLoop每秒60次调用了state函数。state函数是什么呢?它被赋值为play。意味着play函数会每秒运行60次。以下的示例展示了如何用一个新模式来重构上一个例子的代码:</p>
<pre><code> //为了方便其它函数使用变量,将定义全局变量
let cat;
let state;
function setup() {
//创建cat精灵
cat = new Sprite(resources["images/cat.png"].texture);
cat.y = 96;
cat.vx = 0;
cat.vy = 0;
app.stage.addChild(cat);
//开始设置游戏状态
state = play;
//开始游戏循环
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta){
//更新当前的游戏状态
state(delta);
}
function play(delta) {
//每帧让cat移动1像素
cat.vx = 1;
cat.x += cat.vx;
}</code></pre>
<p>是的,也许这有点让人不快<a href="https://link.segmentfault.com/?enc=IptDpx%2FYgkBQI6fRrEUtdw%3D%3D.ox7BYCO9j8t848%2FLsfiB4QqdcpCPL%2BqJXEZAsjvOXVXUX63NbMSe6Ey82se9Y67vg1zM%2Bv02lc8FoQonntfCey8msES4G8OxmmPH81SRTYI%3D" rel="nofollow">(head-swirler)</a>!但是,不要让它吓到我们,而是花一两分钟来思考这些功能是如何连接的。正如我们将要看到的那样,像这样构造游戏循环将使进行切换游戏场景和关卡之类的事情变得非常容易。</p>
<p><a href="https://link.segmentfault.com/?enc=W2ucnmEcb%2FllM1qEi2WKlA%3D%3D.SzM78i6TzW8sUrUk5uiHkeVooT5sAOxyzUsZuEbQP5%2FyHJ3hB0p0N%2BJa%2FWlnDRwtmlL4ztHKpsu%2ByOAXh4iST%2FdSnKP9u6xDIFfNLHWZGtw%3D" rel="nofollow">在线示例</a>。</p>
<h3>17.键盘控制运动</h3>
<p>只需多做一些工作,我们就可以构建一个简单的系统来使用键盘控制精灵。为了简化我们的代码,建议使用称为keyboard的自定义函数来侦听和捕获键盘事件。如下所示:</p>
<pre><code> function keyboard(value) {
let key = {};
key.value = value;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
//键盘按下开始操作
key.downHandler = event => {
if (event.keyCode === key.value) {
if (key.isUp && key.press)key.press();
key.isDown = true;
key.isUp = false;
event.preventDefault();
}
};
//键盘按下结束
key.upHandler = event => {
if (event.keyCode === key.value) {
if (key.isDown && key.release)key.release();
key.isDown = false;
key.isUp = true;
event.preventDefault();
}
};
//绑定监听的事件
const downListener = key.downHandler.bind(key);
const upListener = key.upHandler.bind(key);
window.addEventListener("keydown", downListener, false);
window.addEventListener("keyup", upListener, false);
//解绑事件的监听
key.unsubscribe = () => {
window.removeEventListener("keydown", downListener);
window.removeEventListener("keyup", upListener);
};
return key;
}</code></pre>
<p>keyboard函数用起来很容易,可以像这样创建一个新的键盘对象:</p>
<pre><code> let keyObject = keyboard(keyValue);</code></pre>
<p>它的一个参数是您要收听的键值。可以点击此处查看键盘键值列表。然后将press和release方法分配给键盘对象,如下所示:</p>
<pre><code> keyObject.press = () => {
//key object pressed
};
keyObject.release = () => {
//key object released
};</code></pre>
<p>键盘对象也有isDown和isUp的布尔值属性,用它们来检查每个按键的状态。但是 不要忘记使用unsubscribe方法删除事件侦听器:</p>
<pre><code> keyObject.unsubscribe();</code></pre>
<p>在examples文件夹里看一下keyboardMovement.html文件是怎么用keyboard函数的,利用键盘的方向键去控制精灵图。运行它,然后用上下左右按键去让猫在舞台上移动。</p>
<p><img src="/img/remote/1460000020692096?w=267&h=267" alt="" title=""></p>
<p>以下是所有的代码:</p>
<pre><code> let cat;
let state;
function setup(){
//创建cat精灵
cat = new Sprite(resource["./images/cat.png"].texture);
//设置cat精灵的坐标
cat.y = 96;
cat.vx = 0;
cat.vy = 0;
//添加到舞台中
app.stage.addChild(cat);
//键盘按键事件注意参数为实际对应的键盘key,是整数数字
let left = keyboard("arrowLeft");
let right = keyboard("arrowRight");
let up = keyboard("arrowUp");
let down = keyboard("arrowDown");
//当按下左方向键往左改变速度,即改变vx为负值,在这里是5,vy不变
left.press = () => {
cat.vx = -5;
cat.vy = 0;
}
//当释放左方向键时初始化速度
left.release = () => {
//如果右键没有被按下,并且cat的vy速度为0
if(!right.isDown && cat.vy === 0){
cat.vx = 0;
}
}
//当按下右方向键
right.press = () => {
cat.vx = 5;
cat.vy = 0;
}
//当释放右方向键
right.release = () => {
if(!left.isDown && cat.vy === 0){
cat.vx = 0;
}
}
//当按下上方向键
up.press = () => {
cat.vy = -5;
cat.vx = 0;
}
//当释放上方向键
up.release = () => {
if(!down.isDown && cat.vx === 0){
cat.vy = 0;
}
}
//当按下下方向键
down.press = () => {
cat.vy = 5;
cat.vx = 0;
}
//当释放下方向键
down.release = () => {
if(!up.isDown && cat.vx === 0){
cat.vy = 0;
}
}
state = play;
//开始游戏循环
app.ticker.add((delta) => {
gameLoop(delta);
})
}
function gameLoop(delta){
//更新游戏状态
state(delta);
}
function play(delta){
cat.x += cat.vx;
cat.y += cat.vy;
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=8acewnYRY28aqlyr5NppAA%3D%3D.s5xxrNQ7m4e3I5vHNFt%2FzMz3YH4UIMbQNGZMvyL0fCLzykDOUjZuEpJkrMDNqOxj7RvDxd3Fa8MH9XDOe8tifnPMfN3VP%2B98PkthnffG%2BvE%3D" rel="nofollow">在线示例</a>。</p>
<h3>18.精灵分组</h3>
<h4>(1).精灵分组含义</h4>
<p>精灵分组使我们可以创建游戏场景,并将相似的精灵作为一个单元一起管理。Pixi有一个名为Container的对象,我们可以使用它来完成一些操作。让我们看看它是如何工作的。 假设您要显示三种精灵:猫,刺猬和老虎。创建它们并设置它们的位置-但是不要将它们添加到舞台上。</p>
<pre><code> //The cat
let cat = new Sprite(id["cat.png"]);
cat.position.set(16, 16);
//The hedgehog
let hedgehog = new Sprite(id["hedgehog.png"]);
hedgehog.position.set(32, 32);
//The tiger
let tiger = new Sprite(id["tiger.png"]);
tiger.position.set(64, 64);</code></pre>
<p>接下来,创建一个animals容器以将它们全部分组,如下所示:</p>
<pre><code> let animals = new Container();</code></pre>
<p>然后使用addChild方法将这些精灵添加到分组容器中</p>
<pre><code> animals.addChild(cat);
animals.addChild(hedgehog);
animals.addChild(tiger);</code></pre>
<p>最后,把分组添加到舞台中</p>
<pre><code> app.stage.addChild(animals);</code></pre>
<p>注: (stage对象也是一个Container。它是所有Pixi精灵的根容器。)</p>
<p>以上的代码效果如下图所示:</p>
<p><img src="/img/remote/1460000020692097?w=267&h=267" alt="" title=""></p>
<p>我们是看不到这个包含精灵图的animals分组的。它仅仅是个容器而已。</p>
<p><img src="/img/remote/1460000020692098?w=267&h=267" alt="" title=""></p>
<p>现在,我们可以将这个animals分组视为一个单元。我们也可以将Container视为一种没有纹理的特殊精灵。如果需要获取animals包含的所有子精灵的列表,可以使用children数组来获取。</p>
<pre><code> console.log(animals.children)
//Displays: Array [Object, Object, Object]</code></pre>
<p>它告诉我们animals有三个子精灵。因为animals与任何其他Sprite一样,因此可以更改其x和y值,alpha,scale和所有其他sprite属性。我们在父容器上更改的任何属性值都将以相对方式影响子精灵。因此,如果设置了animals的x和y位置,则所有子精灵将相对于animals的左上角重新定位。如果将animals的x和y位置设置为64,会发生什么?</p>
<pre><code> animals.position.set(64, 64);</code></pre>
<p>整个精灵组将向右下方移动64个像素。如下图所示:</p>
<p><img src="/img/remote/1460000020692099?w=267&h=267" alt="" title=""></p>
<p>animals也有其自己的尺寸,该尺寸基于包含的精灵所占据的面积。我们可以找到它的宽度和高度值,如下所示:</p>
<pre><code> console.log(animals.width);
//Displays: 112
console.log(animals.height);
//Displays: 112
</code></pre>
<p><img src="/img/remote/1460000020692100?w=267&h=267" alt="" title=""></p>
<p>如果更改animals的宽度或高度会怎样?</p>
<pre><code> animals.width = 200;
animals.height = 200;</code></pre>
<p>所有子精灵将缩放以匹配该更改。如下图所示:</p>
<p><img src="/img/remote/1460000020692101?w=267&h=267" alt="" title=""></p>
<p>我们可以根据需要在其他容器中嵌套尽可能多的容器,以根据需要创建深层次结构。但是,DisplayObject(如Sprite或另一个Container)一次只能属于一个父级。如果使用addChild将精灵作为另一个对象的子级,则Pixi会自动将其从当前父级中删除。这是我们无需担心的有用的管理。</p>
<h4>(2).局部和全局位置</h4>
<p>当把精灵添加到一个容器中时,它的x和y是相对于精灵分组的左上角来定位的,这就是精灵的局部位置。例如:你认为cat精灵在以下图中所处的位置是?</p>
<p><img src="/img/remote/1460000020692102?w=267&h=267" alt="" title=""></p>
<p>让我们来获取它的值:</p>
<pre><code> console.log(cat.x);
//Displays:16</code></pre>
<p>16?是的!这是因为cat相对于分组的左上角只仅仅偏移了16像素而已。 16就是cat的局部位置。</p>
<p>精灵当然也有全局位置。全局位置就是舞台左上角到精灵的锚点(通常值得是精灵的左上角的距离)的距离。我们可以通过toGlobal方法来获取精灵的全局位置。如下:</p>
<pre><code> //父精灵上的方法,传入子精灵位置参数
parentSprite.toGlobal(childSprite.position)</code></pre>
<p>以上代码的意思就是如果我们要在animals分组中找到cat精灵的全局位置,那么就要像如下这样写代码:</p>
<pre><code> console.log(animals.toGlobal(cat.position));
//Displays: Object {x: 80, y: 80...};</code></pre>
<p>然后它就会给我们一个x和y的位置值,即80。更确切的说,cat的全局位置就是相对于舞台左上角的位置。</p>
<p>如果不知道精灵的父容器是什么?我们如何找到精灵的全局位置呢?每个精灵都会有一个parent属性来告诉我们它的父容器(或者说叫父精灵分组)是什么。如果将一个精灵正确的添加到了舞台中,那么舞台就是精灵的父容器。在以上的示例中,cat精灵的父容器就是 animals精灵分组。那也就意味着可以通过编写如下的代码来获取cat的全局位置。</p>
<pre><code> cat.parent.toGlobal(cat.position);</code></pre>
<p>即使我们不知道cat精灵的父容器是谁,它一样会执行。当然还有一种更好的方式来计算出精灵的全局位置,并且它通常也是一种最佳方式,所以听好啦!如果我们想要知道精灵到canvas左上角的距离,并且不知道或者不关心精灵的父容器是谁,可以使用getGlobalPosition方法。以下展示了如何获取tiger精灵的全局位置:</p>
<pre><code> tiger.getGlobalPosition().x
tiger.getGlobalPosition().y</code></pre>
<p>在我们已经写好的示例中,它会返回我们x和y的值是128。更特别的是, getGlobalPosition返回的值非常精确:当精灵的局部位置改变的同时,也会返回给我们准确的全局位置。</p>
<p>如果想要将全局位置转为局部位置应该怎么办?我们可以使用toLocal方法。它的工作方式很类似,通常是以下的格式:</p>
<pre><code> sprite.toLocal(sprite.position, anyOtherSprite)</code></pre>
<p>使用toLocal方法可以找到一个精灵与另一个任意精灵之间的距离。以下代码展示了如何找到tiger相对于 hedgehog的位置。</p>
<pre><code> tiger.toLocal(tiger.position, hedgehog).x
tiger.toLocal(tiger.position, hedgehog).y</code></pre>
<p>上面的代码会返回一个32的x值和一个32的y值。我们可以在示例图中看到tiger的左上角和hedgehog的左上角距离32像素。</p>
<h4>(3).使用ParticleContainer分组精灵</h4>
<p>Pixi有一个额外的,高性能的方式去分组精灵的方法称作:ParticleContainer(PIXI.particles.ParticleContainer)。任何在ParticleContainer里的精灵都会比在一个普通的Container的渲染速度快2到5倍。这是用于提升游戏性能的一个很棒的方法。 可以像这样创建ParticleContainer:</p>
<pre><code> let superFastSprites = new PIXI.particles.ParticleContainer();</code></pre>
<p>然后用addChild方法去往里添加精灵,就像往普通的Container添加一样。</p>
<p>如果使用ParticleContainer,我们就不得不做出一些妥协。在一个ParticleContainer里的精灵仅仅只有一些基本的属性: x,y,width,height,scale,pivot,alpha, visible——就这么多。而且,它包含的精灵不能拥有自己的嵌套子级ParticleContainer也无法使用Pixi的高级视觉效果,例如滤镜和混合模式。每个ParticleContainer只能用一个纹理(因此,如果您想要具有不同外观的精灵,则必须使用雪碧图)。但是对于获得的巨大性能提升,这些妥协通常是值得的。而且,还可以在同一项目中同时使用Containers和ParticleContainers,因此可以微调优化。</p>
<p>为什么在Particle Container的精灵会如此快呢?因为精灵的位置是直接在GPU上计算的。Pixi开发团队正在努力让尽可能多的雪碧图在GPU上处理,所以很有可能用的最新版的Pixi的 ParticleContainer的特性一定比现在在这儿描述的特性多得多。查看当前<a href="https://link.segmentfault.com/?enc=AFn9kSVdbjAMg9PMD%2BWGqQ%3D%3D.HzpGSQdQmvjo97pPq661f9YAHhc5N%2B8TrA1Yr7xeSqnt4Mnu9JbOFVPfuq16SPWaQym8K6Hkjyf27nLiBN14MssZg2kDtxQ49Sto%2FcWal2Y%3D" rel="nofollow">ParticleContainer文档</a>以获取更多信息。</p>
<p>无论在哪里创建一个ParticleContainer,都会有四个属性参数需要提供: size,properties,batchSize,autoResize。</p>
<pre><code> let superFastSprites = new ParticleContainer(maxSize, properties, batchSize, autoResize);</code></pre>
<p>maxSize的默认值是1500。所以,如果需要包含更多的精灵,那就把这个值设为更大的数字。 properties参数是一个对象,对象包含5个需要设置的布尔值:scale, position,rotation,uvs,alphaAndTint。position的默认值是true,其它4个参数的默认值是false。这意味着在ParticleContainer中,想要改变 scale,rotation,uvs,alphaAndTint,就不得不把这些属性设置为true,就像如下这样:</p>
<pre><code> let superFastSprites = new ParticleContainer(size,
{
rotation: true,
alphaAndtint: true,
scale: true,
uvs: true
}
);</code></pre>
<p>但是,如果认为不需要使用这些属性,请将它们设置为false可以最大限度地发挥性能。什么是uvs属性?仅当具有在动画时更改其纹理的粒子时,才将其设置为true。(所有精灵的纹理也都必须在同一雪碧图上才能起作用。) (注意:UV映射是3D图形显示术语,指的是被映射到3D表面上的纹理(图像)的x和y坐标。U是x轴,V是y轴。WebGL已经使用x,y和z用于3D空间定位,因此选择U和V表示2D图像纹理的x和y。)</p>
<p><a href="https://link.segmentfault.com/?enc=zt7c5gQ5n%2BKCNU01ARk%2BXg%3D%3D.ixmjSNEX5NFQPylhomuqp3SzmucWol44b7mMA45TlCEZj2o4t7wKDIgialDw0TLrXEynXj1HxhpjIuIzLXS41cG%2B8GgAo0gD0bq5SK%2FaWyo%3D" rel="nofollow">在线示例</a>。</p>
<h3>19.pixi画几何图形</h3>
<h4>(1).描述</h4>
<p>使用图像纹理是制作精灵最有用的方法之一,但是也有其自己的低级绘制工具。可以使用它们制作矩形,形状,线,复杂的多边形和文本。而且,幸运的是,它使用了与Canvas Drawing API几乎相同的API,因此,如果已经熟悉canvas,就没有什么真正的新知识了。但是最大的好处是,与Canvas Drawing API不同,使用Pixi绘制的形状由WebGL在GPU上渲染。Pixi可以利用所有未开发的性能。让我们快速浏览一下如何制作一些基本形状。下面是我们将要使用的代码来创造的图形。</p>
<p><img src="/img/remote/1460000020692103?w=267&h=267" alt="" title=""></p>
<h4>(2).矩形</h4>
<p>所有的形状的初始化都是先创造一个Pixi的Graphics的类 (PIXI.Graphics)的实例。</p>
<pre><code> let rectangle = new Graphics();</code></pre>
<p>然后使用参数为十六进制颜色代码值的beginFill方法来设置矩形的填充颜色。以下是将其设置为浅蓝色的方法。</p>
<pre><code> rectangle.beginFill(0x66CCFF);</code></pre>
<p>如果想要给形状设置一个轮廓,使用方法。以下为给矩形设置一个4像素宽alpha值为1的红色轮廓的示例:</p>
<pre><code> //第一个参数为轮廓线宽度,第二个参数为轮廓线颜色值,第三个参数为alpha值
rectangle.lineStyle(4, 0xFF3300, 1);</code></pre>
<p>使用drawRect方法来画一个矩形,它的四个参数分别是x,y,width,height。</p>
<pre><code> rectangle.drawRect(x, y, width, height);</code></pre>
<p>使用endFill方法来结束绘制。就像<a href="https://link.segmentfault.com/?enc=zh9vAqSxBeDvDzLAxSK9Pg%3D%3D.%2BDf%2BfSWHsFUKGCWwq3mOOHWafqXF0TCOKfsxZcmQbzeg1V7RsHLE%2B62UQPwYS7vJXks%2BxsLSfBsdraZsOONLLSXyFVQPgaSXMVrQsl3f%2FyjSqN77UwQ4iKX28pwUpRwi" rel="nofollow">Canvas Drawing API</a>一样!以下是绘制矩形,更改其位置并将其添加到舞台所需的全部代码。</p>
<pre><code> let rectangle = new Graphics();
rectangle.lineStyle(4, 0xFF3300, 1);
rectangle.beginFill(0x66CCFF);
rectangle.drawRect(0, 0, 64, 64);
rectangle.endFill();
rectangle.x = 170;
rectangle.y = 170;
app.stage.addChild(rectangle);</code></pre>
<p>以上代码可以在(170,170)这个位置创造一个宽高都为64的蓝色的红框矩形。</p>
<h4>(3).圆形</h4>
<p>使用drawCircle方法来创造一个圆。它的三个参数是x, y和radius。</p>
<pre><code> drawCircle(x, y, radius)</code></pre>
<p>与矩形和精灵不同,圆的x和y位置也是其中心点(圆点)。以下是制作半径为32像素的紫色圆圈的代码。</p>
<pre><code> let circle = new Graphics();
circle.beginFill(0x9966FF);
circle.drawCircle(0, 0, 32);
circle.endFill();
circle.x = 64;
circle.y = 130;
app.stage.addChild(circle);</code></pre>
<h4>(4).椭圆形</h4>
<p>作为Canvas Drawing API的一个方面,Pixi允许您使用drawEllipse方法绘制椭圆。</p>
<pre><code> drawEllipse(x, y, width, height);</code></pre>
<p>x / y位置定义了椭圆的左上角(假设椭圆被一个不可见的矩形边界框包围-该框的左上角将代表椭圆的x / y锚点位置)。以下代码绘制了一个黄色的椭圆,宽50像素,高20像素。</p>
<pre><code> let ellipse = new Graphics();
ellipse.beginFill(0xFFFF00);
ellipse.drawEllipse(0, 0, 50, 20);
ellipse.endFill();
ellipse.x = 180;
ellipse.y = 130;
app.stage.addChild(ellipse);</code></pre>
<h4>(5).圆角矩形</h4>
<p>Pixi还允许您使用drawRoundedRect方法制作圆角矩形。最后一个参数cornerRadius是一个数字(以像素为单位),该数字确定应将圆角设置为多少。</p>
<pre><code> drawRoundedRect(x, y, width, height, cornerRadius)</code></pre>
<p>以下是绘制一个圆角为10像素的矩形的代码。</p>
<pre><code> let roundBox = new Graphics();
roundBox.lineStyle(4, 0x99CCFF, 1);
roundBox.beginFill(0xFF9933);
roundBox.drawRoundedRect(0, 0, 84, 36, 10)
roundBox.endFill();
roundBox.x = 48;
roundBox.y = 190;
app.stage.addChild(roundBox);</code></pre>
<h4>(6).线段</h4>
<p>从前面的例子我们已经知道使用lineStyle方法来绘制一条线段了。与Canvas Drawing API一样,我们可以使用moveTo和lineTo方法来画线段的开始和结束点。以下代码画了一条4像素宽,白色的对角线。</p>
<pre><code> let line = new Graphics();
line.lineStyle(4, 0xFFFFFF, 1);
line.moveTo(0, 0);
line.lineTo(80, 50);
line.x = 32;
line.y = 32;
app.stage.addChild(line);</code></pre>
<p>PIXI.Graphics对象(如线条)具有x和y值,就像sprites一样,因此绘制它们之后,可以将它们放置在舞台上的任何位置。</p>
<h4>(7).多边形</h4>
<p>我们还可以使用drawPolygon方法将线连接在一起并用颜色填充它们,以制作复杂的形状。drawPolygon的参数是x / y点的路径数组,这些点定义形状上每个点的位置。</p>
<pre><code> let path = [
point1X, point1Y,
point2X, point2Y,
point3X, point3Y
];
graphicsObject.drawPolygon(path);</code></pre>
<p>drawPolygon将这三个点连接在一起以形成形状。以下是使用drawPolygon将三条线连接在一起以形成带有蓝色边框的红色三角形的方法。在位置(0,0)处绘制三角形,然后使用其x和y属性将其移动到舞台上的位置。</p>
<pre><code> let triangle = new Graphics();
triangle.beginFill(0x66FF33);
triangle.drawPolygon([
-32, 64,
32, 64,
0, 0
]);
triangle.endFill();
triangle.x = 180;
triangle.y = 22;
app.stage.addChild(triangle);</code></pre>
<p><a href="https://link.segmentfault.com/?enc=kNSKOrLsaUYrD9GuqJmDMA%3D%3D.FBBWCiE0a4uTzJJHJg569Pn5SNrjLODWt4x6hTMVd3bIjy%2BX7lQ%2F2otYjBonY2xIvjji0KpZb4WffHClEs0iQDmlJsBvXG4x9neqd9hKn7M%3D" rel="nofollow">在线示例</a>。</p>
<h3>20.显示文本</h3>
<p>使用Text对象(PIXI.Text)在舞台上显示文本。在最简单的形式中,可以这样操作:</p>
<pre><code> let message = new Text("Hello Pixi!");
app.stage.addChild(message);</code></pre>
<p>这将在画布上显示单词“Hello,Pixi”。Pixi的Text对象继承自Sprite类,因此它们包含所有相同的属性,例如x,y,width,height,alpha和rotation。 就像在其他精灵上一样,在舞台上放置文本并调整其大小。例如,可以使用position.set来设置消息的x和y位置,如下所示:</p>
<pre><code> message.position.set(54, 96);</code></pre>
<p><img src="/img/remote/1460000020692104?w=251&h=254" alt="" title=""></p>
<p>这将为我们提供基本的,无样式的文本。但是,如果想变得更时髦,请使用Pixi的TextStyle(PIXI.TextStyle)函数来定义自定义文本样式。以下为示例代码:</p>
<pre><code> let style = new TextStyle({
fontFamily: "Arial",
fontSize: 36,
fill: "white",
stroke: '#ff3300',
strokeThickness: 4,
dropShadow: true,
dropShadowColor: "#000000",
dropShadowBlur: 4,
dropShadowAngle: Math.PI / 6,
dropShadowDistance: 6,
});</code></pre>
<p>这将创建一个新的样式对象,其中包含要使用的所有文本样式。有关可以使用的所有样式属性的完整列表,请参见此处。 要将样式应用于文本,请添加样式对象作为Text函数的第二个参数,如下所示:</p>
<pre><code> let message = new Text("Hello Pixi!", style);</code></pre>
<p><img src="/img/remote/1460000020692105?w=251&h=249" alt="" title=""></p>
<p>如果要在创建文本对象后更改其内容,可以使用text属性。</p>
<pre><code> message.text = "Text changed!";</code></pre>
<p>如果要重新定义样式属性,可以使用style属性。</p>
<pre><code> message.style = {fill: "black", font: "16px PetMe64"};</code></pre>
<p>Pixi通过使用Canvas Drawing API将文本呈现为不可见的临时画布元素来制作文本对象。然后,它将画布变成WebGL纹理,以便可以将其映射到精灵。这就是需要将文本的颜色包裹在字符串中的原因:这是Canvas Drawing API的颜色值。与任何画布颜色值一样,可以使用用于常见的颜色单词,例如"red"或"green",也可以使用rgba,hsla或hex颜色模式。Pixi还可以包装长行文本。将文本的wordWrap样式属性设置为true,然后将wordWrapWidth设置为文本行应达到的最大长度(以像素为单位)。使用align属性设置多行文本的对齐方式。如下例:</p>
<pre><code> message.style = {wordWrap: true, wordWrapWidth: 100, align: center};</code></pre>
<p>注: align不会影响单行文字。</p>
<p>如果要使用自定义字体文件,可以使用CSS@font-face规则将字体文件链接到运行Pixi应用程序的HTML页面。</p>
<pre><code> @font-face {
font-family: "fontFamilyName";
src: url("fonts/fontFile.ttf");
}</code></pre>
<p>将此@font-face规则添加到HTML页面的CSS样式表中。</p>
<p><a href="https://link.segmentfault.com/?enc=6vGGOFnxc%2BC1erX9DvEYIg%3D%3D.g7AR0rzhV6v137NkgQeJOzXv6JA6RmceVqDSUEnDuFbMQUXmN5mIXv%2FsDK0kHPVK6vU2skl7R37d2epX3owggw%3D%3D" rel="nofollow">Pixi还支持位图字体</a>。还可以使用Pixi的loader来加载位图字体XML文件,就像加载JSON或图像文件一样。</p>
<p><a href="https://link.segmentfault.com/?enc=vQmaMeJjWMkfx%2B4dU284og%3D%3D.0hf6fFt0ePC9AormEfaK2pe2WWKA5TLkVSOW0Pc3HPiOKsybxiY5BuZzQDFfT%2B8gbZNAhWWMicsL%2FroZDZvpmVQmizV0peNDvhumre5JHsc%3D" rel="nofollow">在线示例</a>。</p>
<h3>21.碰撞检测</h3>
<h4>(1).碰撞检测介绍</h4>
<p>我们现在知道了如何制作各种图形对象,但是可以使用它们做什么呢?一个有趣的事情是构建一个简单的碰撞检测系统。可以使用一个名为hitTestRectangle的自定义函数,该函数检查是否有两个矩形Pixi精灵正在接触。</p>
<pre><code> hitTestRectangle(spriteOne, spriteTwo)</code></pre>
<p>如果它们重叠(即碰撞),则hitTestRectangle将返回true。我们可以将hitTestRectangle与if语句一起使用,以检查两个精灵之间的碰撞,如下所示:</p>
<pre><code> if (hitTestRectangle(cat, box)) {
//There's a collision
} else {
//There's no collision
}</code></pre>
<p>如我们所见,hitTestRectangle是游戏设计广阔领域的门槛。 运行examples文件夹中的collisionDetection.html文件以获取有关如何使用hitTestRectangle的工作示例。使用键盘上的方向键移动猫。如果猫碰到盒子,盒子会变成红色,然后"hit!"将由文本对象显示。</p>
<p><img src="/img/remote/1460000020692106?w=567&h=286" alt="" title=""></p>
<p>我们已经看到了创建所有这些元素的所有代码,以及使猫移动的键盘控制系统。唯一的新东西就是play函数内部使用hitTestRectangle来检查碰撞的函数。</p>
<pre><code> function play(delta) {
//使用cat精灵的速度属性来移动
cat.x += cat.vx;
cat.y += cat.vy;
//检查cat精灵与box精灵是否碰撞
if (hitTestRectangle(cat, box)) {
//如果碰撞则改变文本
//盒子颜色变成红色
message.text = "hit!";
box.tint = 0xff3300;
} else {
//如果没有碰撞重置文本与盒子颜色
message.text = "No collision...";
box.tint = 0xccff99;
}
}</code></pre>
<p>由于play函数每秒被游戏循环调用60次,因此该if语句会不断检查猫和盒子之间的碰撞。 如果hitTestRectangle返回的是true,则文本消息对象使用文本显示"hit!":</p>
<pre><code> message.text = "Hit!";</code></pre>
<p>然后,通过将盒子的tint属性设置为十六进制的红色值,将盒子的颜色从绿色更改为红色。</p>
<pre><code> box.tint = 0xff3300;</code></pre>
<p>如果没有碰撞,则文本和盒子将保持其原始状态:</p>
<pre><code> message.text = "No collision...";
box.tint = 0xccff99;</code></pre>
<p>这段代码非常简单,但是突然之间创建了一个似乎完全活跃的交互式世界。几乎就像魔术!而且,也许令人惊讶的是,我们现在拥有开始使用Pixi制作游戏所需的全部技能!</p>
<h4>(2).碰撞检测函数</h4>
<p>但是hitTestRectangle函数呢?它是做什么的,它是如何工作的?这样的碰撞检测算法如何工作的细节不在本文的讨论范围之内。(如果真的想知道,可以了解这本书的用法。)最重要的是,知道如何使用它。但是,仅供参考,以防万一,也可以参考完整的hitTestRectangle函数定义。我们能从注释中弄清楚它在做什么?</p>
<pre><code> function hitTestRectangle(r1, r2) {
//Define the variables we'll need to calculate
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
//hit will determine whether there's a collision
hit = false;
//Find the center points of each sprite
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;
//Find the half-widths and half-heights of each sprite
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2;
//Calculate the distance vector between the sprites
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//Check for a collision on the x axis
if (Math.abs(vx) < combinedHalfWidths) {
//A collision might be occurring. Check for a collision on the y axis
if (Math.abs(vy) < combinedHalfHeights) {
//There's definitely a collision happening
hit = true;
} else {
//There's no collision on the y axis
hit = false;
}
} else {
//There's no collision on the x axis
hit = false;
}
//`hit` will be either `true` or `false`
return hit;
};</code></pre>
<p><a href="https://link.segmentfault.com/?enc=DBMV%2B9OLWI5CdGz48Id09A%3D%3D.19MupBxK2YjPt78gFNieA7uhsv7Rd1rAXtb%2Be%2BicV1%2B6MEmtNbGAPpRO9ugwQsSpWN0kgl6VAcHFDcCIB6IM6mY%2F62jEAlPZW0qIhxE7zxQ%3D" rel="nofollow">在线示例</a>。</p>
<h3>22.实例学习:寻宝猎人小游戏</h3>
<p>到此为止,我们现在已经拥有开始制作游戏所需的所有技能。 什么? 你不相信我吗 让我向你证明! 让我们来看看如何制作一个简单的对象收集和避免敌人的游戏,称为《寻宝猎人》。</p>
<p><img src="/img/remote/1460000020692107?w=700&h=308" alt="" title=""></p>
<p>《寻宝猎人》是可以使用到目前为止学到的工具制作的最简单的完整游戏之一的一个很好的例子。 使用键盘上的箭头键可帮助探险家找到宝藏并将其带到出口。六个Blob怪物在地牢壁之间上下移动,如果碰到了探索者,他将变成半透明, 并且右上角的血量进度条会缩小。如果所有血量都用光了,舞台上会显示“ You Lost!”; 如果探险家带着宝藏到达出口,则显示“ You Won!”。 尽管它是一个基本的原型,但《寻宝猎人》包含了您在大型游戏中发现的大多数元素:纹理图集图形,交互性,碰撞以及多个游戏场景。 让我们浏览一下游戏的组合方式,以便可以将其用作自己的一款游戏的起点。</p>
<h4>(1).代码结构</h4>
<p>打开treasureHunter.html文件,你将会看到所有的代码都在一个大的文件里。 下面是一个关于如何组织所有代码的概览:</p>
<pre><code> //创建pixi应用以及加载所有的纹理图集的函数,就叫setup
function setup() {
//游戏精灵的创建,开始游戏状态,开始游戏循环
}
function gameLoop(delta) {
//运行游戏循环
}
function play(delta) {
//所有的游戏魔法都在这里
}
function end() {
//游戏最后所运行的代码
}
//游戏需要用到的工具函数:
//`keyboard`, `hitTestRectangle`, `contain`and `randomInt`</code></pre>
<p>把这个当作你游戏代码的蓝图,让我们看看每一部分是如何工作的。</p>
<h4>(2).用setup函数初始化游戏</h4>
<p>加载纹理图集图像后,setup函数即会运行。它仅运行一次,并允许您为游戏执行一次性设置任务。 在这里创建和初始化对象,精灵,游戏场景,填充数据数组或解析加载的JSON游戏数据的好地方。 以下是Treasure Hunter中setup函数及其执行任务的简要视图。</p>
<pre><code> function setup() {
//创建游戏开始场景分组
//创建门精灵
//创建玩家也就是探险者精灵
//创建宝箱精灵
//创造敌人
//创建血量进度条
//添加一些游戏所需要的文本显示
//创建游戏结束场景分组
//分配玩家的键盘控制器
//设置游戏状态
state = play;
//开始游戏循环
app.ticker.add(delta => gameLoop(delta));
}</code></pre>
<p>代码的最后两行,state = play;和gameLoop可能是最重要的。 将gameLoop添加到Pixi的切换开关中可以打开游戏引擎, 并在连续循环中调用play函数。但是,在研究其工作原理之前,让我们先看看设置函数中的特定代码是做什么的。</p>
<h5>a.创建游戏场景</h5>
<p>setup函数将创建两个容器组,分别称为gameScene和gameOverScene。这些都添加到舞台中。</p>
<pre><code> gameScene = new Container();
app.stage.addChild(gameScene);
gameOverScene = new Container();
app.stage.addChild(gameOverScene);</code></pre>
<p>属于主游戏的所有精灵都添加到gameScene组中。游戏结束时应显示在游戏上方的文本将添加到gameOverScene组。</p>
<p><img src="/img/remote/1460000020692108?w=600&h=387" alt="" title=""></p>
<p>尽管它是在setup函数中创建的,但当游戏首次启动时gameOverScene不应该可见,因此其visible属性被初始化为false。</p>
<pre><code> gameOverScene.visible = false;</code></pre>
<p>我们将看到,当游戏结束时,gameOverScene的visible属性将设置为true,以显示出现在游戏结束时的文本。</p>
<h5>b.制作地牢,门,探险者与宝藏精灵</h5>
<p>玩家,出口,宝箱以及地牢都是从纹理图集中制作而来的精灵。十分重要的是,它们都被作为gameScene的子精灵而添加。</p>
<pre><code> //从纹理图集中创建精灵
id = resources["images/treasureHunter.json"].textures;
//Dungeon
dungeon = new Sprite(id["dungeon.png"]);
gameScene.addChild(dungeon);
//Door
door = new Sprite(id["door.png"]);
door.position.set(32, 0);
gameScene.addChild(door);
//Explorer
explorer = new Sprite(id["explorer.png"]);
explorer.x = 68;
explorer.y = gameScene.height / 2 - explorer.height / 2;
explorer.vx = 0;
explorer.vy = 0;
gameScene.addChild(explorer);
//Treasure
treasure = new Sprite(id["treasure.png"]);
treasure.x = gameScene.width - treasure.width - 48;
treasure.y = gameScene.height / 2 - treasure.height / 2;
gameScene.addChild(treasure);</code></pre>
<p>把它们都放在gameScene分组会使我们在游戏结束的时候去隐藏gameScene和显示gameOverScene操作起来更简单。</p>
<h5>c.制作泡泡怪精灵</h5>
<p>6个blob怪物是循环创建的。每个blob都被赋予一个随机的初始位置和速度。每个blob的垂直速度交替乘以1或-1,这就是导致每个blob沿与其相邻方向相反的方向移动的原因。每个创建的blob怪物都会被推入称为blob的数组。</p>
<pre><code> //泡泡怪数量
let numberOfBlobs = 6;
//泡泡怪水平位置值
let spacing = 48;
//泡泡怪偏移量
let xOffset = 150;
//泡泡怪速度
let speed = 2;
//泡泡怪移动方向
let direction = 1;
//一个数组存储所有的泡泡怪
let blobs = [];
//开始创建泡泡怪
for (let i = 0; i < numberOfBlobs; i++) {
//创建一个泡泡怪
let blob = new Sprite(id["blob.png"]);
//根据`spacing`值将每个Blob水平隔开
//xOffset确定屏幕左侧的点
//应在其中添加第一个Blob
let x = spacing * i + xOffset;
//给泡泡怪一个随机的垂直方向上的位置
let y = randomInt(0, stage.height - blob.height);
//设置泡泡怪的位置
blob.x = x;
blob.y = y;
//设置泡泡怪的垂直速度。 方向将为1或
//`-1。“ 1”表示敌人将向下移动,“-1”表示泡泡怪将
//提升。将“方向”与“速度”相乘即可确定泡泡怪
//垂直方向
blob.vy = speed * direction;
//下一个泡泡怪方向相反
direction *= -1;
//将泡泡怪添加到数组中
blobs.push(blob);
//将泡泡怪添加到gameScene分组中
gameScene.addChild(blob);
}</code></pre>
<h5>d.制作血量进度条</h5>
<p>当我们在玩寻宝猎人的时候,我想应该会发现,当我们的探险者触碰到任何一个敌人的时候,屏幕右上角的血量进度条的宽度都会减少。那么这个血量进度条是如何制作的呢?它仅仅只是两个相同位置重叠的矩形:一个黑色的矩形在后面,一个红色的矩形在前面。它们都被分在healthBar分组中。healthBar被添加到gameScene分组中,然后在舞台上被定位。</p>
<pre><code> //创建血量进度条
healthBar = new PIXI.Container();
healthBar.position.set(stage.width - 170, 4)
gameScene.addChild(healthBar);
//创建黑色的矩形
let innerBar = new PIXI.Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);
//创建红色的矩形
let outerBar = new PIXI.Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);
healthBar.outer = outerBar;</code></pre>
<p>我们已经看到一个被叫做outer的属性被添加到healthBar中。它仅仅引用outerBar(红色矩形),以便以后访问时很方便。</p>
<pre><code> healthBar.outer = outerBar;</code></pre>
<p>我们不必这样做;但是,为什么不呢!这意味着,如果我们想控制红色outerBar的宽度,则可以编写一些如下所示的平滑代码:</p>
<pre><code> healthBar.outer.width = 30;</code></pre>
<p>那很整洁而且可读,所以我们会保留它!</p>
<h5>e.制作文本提示</h5>
<p>游戏结束时,根据游戏的结果,一些文本显示“You won!”或“You lost!”。这是通过使用文本精灵并将其添加到gameOverScene来实现的。由于游戏开始时gameOverScene的visible属性设置为false,因此我们看不到此文本。这是setup函数的代码,该函数创建消息文本并将其添加到gameOverScene。</p>
<pre><code> let style = new TextStyle({
//字体类型
fontFamily: "Futura",
//字体大小
fontSize: 64,
//字体颜色
fill: "white"
});
message = new Text("The End!", style);
message.x = 120;
message.y = app.stage.height / 2 - 32;
gameOverScene.addChild(message);</code></pre>
<h4>(3).开始游戏</h4>
<p>所有使精灵移动的游戏逻辑和代码都发生在play函数内部,该函数连续循环运行。这是play函数的概述:</p>
<pre><code> function play(delta) {
//移动探险者并将其包含在地牢中
//移动泡泡怪
//检测泡泡怪与探险者的碰撞
//检测探险者与宝箱的碰撞
//检测宝箱与出口的碰撞
//决定游戏是赢还是输
//游戏结束时,改变游戏的状态为end
}</code></pre>
<p>让我们找出所有这些功能的工作方式。</p>
<h4>(4).移动探险者</h4>
<p>探险者是使用键盘控制的,执行该操作的代码与先前学习的键盘控制代码非常相似。键盘对象会修改探险者的速度,并将该速度添加到play函数中探险者的位置。</p>
<pre><code> explorer.x += explorer.vx;
explorer.y += explorer.vy;</code></pre>
<h5>a.运动范围</h5>
<p>但是,新功能是探险者的动作被包含在地牢的墙壁内。绿色轮廓线显示了探险者运动的极限。</p>
<p><img src="/img/remote/1460000020692109?w=526&h=525" alt="" title=""></p>
<p>这是在名为contain的自定义函数的帮助下完成的。</p>
<pre><code> contain(explorer, {x: 28, y: 10, width: 488, height: 480});</code></pre>
<p>contain包含2个参数。第一个参数是你想要被包含的精灵,第二个参数则是任意的一个对象,包含x,y,width,height属性,为了定义一个矩形区域。在这个例子中,contain对象定义了一个仅比舞台稍微偏移且小于舞台的区域,它与地牢墙的尺寸所匹配。</p>
<p>以下是完成这些工作的contain函数。该函数检查精灵是否已超出contain对象的边界。 如果超出了,则代码将精灵移回该边界。contain函数还会返回碰撞变量,其值取决于"top","right","bottom","left",具体取决于击中边界的哪一侧。(如果精灵没有碰到任何边界,则碰撞将是不确定的。)</p>
<pre><code> function contain(sprite, container) {
let collision = undefined;
//左
if (sprite.x < container.x) {
sprite.x = container.x;
collision = "left";
}
//上
if (sprite.y < container.y) {
sprite.y = container.y;
collision = "top";
}
//右
if (sprite.x + sprite.width > container.width) {
sprite.x = container.width - sprite.width;
collision = "right";
}
//下
if (sprite.y + sprite.height > container.height) {
sprite.y = container.height - sprite.height;
collision = "bottom";
}
//返回collision的值
return collision;
}</code></pre>
<p>你将看到如何在前面的代码中使用碰撞返回值,以使Blob怪物在上层和下层地下城墙之间来回反弹。</p>
<h4>(4).移动怪物</h4>
<p>play函数还可以移动Blob怪物,将它们保留在地牢壁中,并检查每个怪物是否与玩家发生碰撞。如果Blob撞到地牢的顶壁或底壁,则其方向会相反。所有这些都是在forEach循环的帮助下完成的,该循环遍历每帧Blobs数组中的每个Blob精灵。</p>
<pre><code> blobs.forEach(function(blob) {
//移动泡泡怪
blob.y += blob.vy;
//检查泡泡怪的屏幕边界
let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});
//如果泡泡怪撞到舞台的顶部或者底部,则方向反转
//它的方向
if (blobHitsWall === "top" || blobHitsWall === "bottom") {
blob.vy *= -1;
}
//碰撞检测如果任意敌人触碰到探险者
//将探险者的explorerHit值设置为true
if(hitTestRectangle(explorer, blob)) {
explorerHit = true;
}
});</code></pre>
<p>我们可以在上面的代码中看到contain函数的返回值如何用于使blob从墙反弹。名为blobHitsWall的变量用于捕获返回值:</p>
<pre><code> let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});</code></pre>
<p>blobHitsWall通常是undefined(未定义)的。但是,如果blob碰到了顶壁,则blobHitsWall的值将为“top”。 如果blob碰到底壁,则blobHitsWall的值将为“bottom”。如果以上两种情况均成立,则可以通过反转blob的速度来反转blob的方向。以下是执行此操作的代码:</p>
<pre><code> if (blobHitsWall === "top" || blobHitsWall === "bottom") {
//通过改变速度为负值来反转方向
blob.vy *= -1;
}</code></pre>
<p>将blob的vy(垂直速度)值乘以-1将翻转其运动方向。</p>
<h4>(5).检测碰撞</h4>
<p>前面循环中的代码使用hitTestRectangle函数来确定是否有任何敌人触摸了探险者。</p>
<pre><code> if(hitTestRectangle(explorer, blob)) {
explorerHit = true;
}</code></pre>
<p>如果hitTestRectangle返回的是true。也就意味着会发生一次碰撞并且explorerHit变量的值会是true。如果explorerHit的值是true,play函数将会使探险者变成半透明,并且血量进度条的宽度减少1像素。(具体减少多少依据每个人自己定义。)。</p>
<pre><code> if(explorerHit) {
//使探险者变成半透明
explorer.alpha = 0.5;
//减少血量进度条的宽度
healthBar.outer.width -= 1;
} else {
//使探险者完全透明,如果不能再被撞击
explorer.alpha = 1;
}</code></pre>
<p>如果explorerHit为false,则将explorer的alpha属性保持为1,这使其完全不透明。play函数还检查宝箱和探险者之间是否发生碰撞。如果有发生碰撞,宝藏将设置到探险者的位置,并稍有偏移。这使其看起来像探险家正在携带宝藏。</p>
<p><img src="/img/remote/1460000020692110?w=1194&h=261" alt="" title=""></p>
<p>以下是完成这个工作的代码:</p>
<pre><code> if (hitTestRectangle(explorer, treasure)) {
//8的数字还可以再大一点点
treasure.x = explorer.x + 8;
treasure.y = explorer.y + 8;
}</code></pre>
<h4>(6).到达出口并结束游戏</h4>
<p>有两种方式会让游戏结束:探险者携带宝箱并到达了出口就表示你赢了,或者就是你的血量进度条没有了那就表示你失败了。为了赢得游戏探险者仅仅只需要触碰到出口,如果发生了这种情况。那么将游戏的状态state设置为结束end,然后message也就是文本消息提示显示"You won!"</p>
<pre><code> if (hitTestRectangle(treasure, door)) {
state = end;
message.text = "You won!";
}</code></pre>
<p>如果血量进度条没有了,你也就游戏失败了。也将游戏的状态state设置为结束end,然后message也就是文本消息提示显示"You lost!"</p>
<pre><code> if (healthBar.outer.width < 0) {
state = end;
message.text = "You lost!";
}</code></pre>
<p>那么以下代码到底是什么意思呢?</p>
<pre><code> state = end;</code></pre>
<p>我们从前面的示例中记住,gameLoop会以每秒60次的速度不断更新称为状态的函数。这是执行此操作的gameLoop:</p>
<pre><code> function gameLoop(delta){
//更新当前的游戏状态
state(delta);
}</code></pre>
<p>我们还将记住,我们最初将状态值设置为play,这就是为什么play函数循环运行的原因。通过将状态设置为end,我们告诉代码我们想要另一个函数,称为end的循环运行。在更大的游戏中,还可以具有tileScene状态,以及每个游戏级别的状态,例如leveOne,levelTwo和levelThree。</p>
<p>end函数是什么?以下便是:</p>
<pre><code> function end() {
//游戏场景不显示,游戏结束场景显示
gameScene.visible = false;
gameOverScene.visible = true;
}</code></pre>
<p>它只是翻转游戏场景的可见性。这是隐藏gameScene并在游戏结束时显示gameOverScene的内容。 这是一个非常简单的示例,说明了如何切换游戏状态,但是可以在游戏中拥有任意数量的游戏状态,并根据需要填充尽可能多的代码。只需将state的值更改为要在循环中运行的任何函数。 而这正是寻宝猎人的全部!只需多做一些工作,就可以将这个简单的原型变成完整的游戏-试试吧!</p>
<p><a href="https://link.segmentfault.com/?enc=D3R40yUMy8nyPEvnBYF3IA%3D%3D.TsE3fH%2Bq2XGd84CwoK08hOv0UgDE5fm5dJiVSqV28zlpI8ENEEgHpw2LlHauv2OuiNU3N3fTzIpFdbHTbojNxuke0wwD0IVkbmw4FarMFAk%3D" rel="nofollow">在线示例</a>。</p>
<h3>23.更多关于精灵的知识</h3>
<p>到目前为止,我们已经学习了如何使用许多有用的精灵属性,例如x,y,visible和rotation,这些属性使我们可以大量控制精灵的位置和外观。但是Pixi Sprites还具有许多更有趣的有用属性。这是<a href="https://link.segmentfault.com/?enc=KwxfQE%2FLcgDf%2Fg%2BU73f5lw%3D%3D.wWtGxt2bFiXw5n2AHanTP9jmy43KUKek3RJ1WN86o%2BG%2BV9FoQh%2B7u46a13%2BFXwv%2BCpxQIQKRj0lKxymkv2b3jw%3D%3D" rel="nofollow">完整列表</a>。 Pixi的类继承系统如何运作?(什么是类,什么是继承?单击此<a href="https://link.segmentfault.com/?enc=PisDK6yx6y%2B%2F282G9m3utA%3D%3D.ZG7NOHLmNiJNwrnJ8AK%2FWrf%2FGbhSzO3aYN7yUHebeeFHyZfWU1P0ZHpowqa97BudEq7Z7bWEpDDInVU662YjDE2Kl1L9uEDQX%2FHT9m7zrOxM1Lmr4DQH36xq%2Byju8Zno9EqJWTq2lT0sosj9nhvLPg%3D%3D" rel="nofollow">链接</a>以查找。)Pixi的sprites建立在遵循此链的继承模型上:</p>
<pre><code> DisplayObject > Container > Sprite</code></pre>
<p>继承只是意味着链中后面的类使用链中前面的类的属性和方法。这意味着,即使Sprite是链中的最后一个类,它除了具有自己的独特属性外,还具有与DisplayObject和Container相同的所有属性。 最基本的类是DisplayObject。任何DisplayObject都可以在舞台上呈现。容器是继承链中的下一个类。它允许DisplayObject充当其他DisplayObject的容器。排名第三的是Sprite类。精灵既可以显示在舞台上,也可以作为其他精灵的容器。</p>
<p>(PS:本文基于官方教程而翻译并加入了个人的理解以及示例,不喜勿喷。本文总结在本人的<a href="https://link.segmentfault.com/?enc=1Ojh%2BbhVhn6LeAMXpntb0g%3D%3D.wxRSjVk40CewSbESgxCCzYaIxJNL%2FyqCIJih8GZqmeM%3D" rel="nofollow">个人网站</a>上)。</p>
一个奇葩问题引发的"吐血"
https://segmentfault.com/a/1190000019177206
2019-05-14T15:48:14+08:00
2019-05-14T15:48:14+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
10
<p>某天,和某同事交流技术,他提到了一个问题:<code>在js中,如果一个变量赋值给另一个变量,那么他们一定相等吗?为什么?</code>。然后,我脱口而出,是相等,这无毛病。变量赋值本身就只是一个定义好的变量的副本,他们相等是没问题的,即便是引用类型的数据对象,在赋值的时候保持着内存的同一引用,它们当然也相等啊。当然注意这里是相等,不是全等。即<code>"=="</code>而非<code>"==="</code>,所以没有什么类型转换的讨论。不信,那我们来两个代表示例看看:</p>
<pre><code>//基本类型
var a;
var b = a;
b == a;//true
//引用类型
var a = {};
var b = a;
b == a;//true,它们的引用地址相同
</code></pre>
<p>看起来好像是没什么问题的。然而事实真的是这样吗?</p>
<p><img src="/img/bVbsCYs?w=103&h=120" alt="clipboard.png" title="clipboard.png"></p>
<p>我转念一想,不对啊,有这么简单的问题?同事再给我一个例子,狠狠的打击了我,卧槽,还可以这样?</p>
<p><img src="/img/bVbsCZg?w=300&h=300" alt="clipboard.png" title="clipboard.png"></p>
<p><code>js</code>数据当中还有一个特殊的值,那就是<code>NaN</code>。</p>
<pre><code>var a = NaN;
var b = a;
b == a;//猜猜这里是啥
</code></pre>
<p>答案真的是出乎你的意料,当然是<code>false</code>,也就是不相等啦。 </p>
<p><img src="/img/bVbsCZK?w=200&h=183" alt="clipboard.png" title="clipboard.png"></p>
<p>不带这么玩的吧?</p>
<p><img src="/img/bVbsC0p?w=300&h=300" alt="clipboard.png" title="clipboard.png"></p>
<p>后面脑袋一闪,原来如此,这么低级的错误都犯,<code>NaN</code>虽然是一个数值型的值,但是它并不是一个确切的值,所以<code>NaN !== NaN</code>。也就是说,变量的赋值也不会改变它们的不等性呢。</p>
<p><img src="/img/bVbsC09?w=240&h=240" alt="clipboard.png" title="clipboard.png"></p>
<p>还可以这么玩。</p>
<p><code>ps:</code>总结:任何时候任何一个问题都不要小看它,拘束于表面,因为它随时都会是一个坑,让你跳进去,认真点总不会有坏处的。</p>
<p><img src="/img/bVbsC1t?w=350&h=350" alt="clipboard.png" title="clipboard.png"></p>
<p>鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:</p>
<p><img src="/img/bVbslX3?w=256&h=256" alt="clipboard.png" title="clipboard.png"></p>
装逼的最高境界---一行js代码完成一个简易版的贪吃蛇游戏
https://segmentfault.com/a/1190000019137088
2019-05-10T14:40:01+08:00
2019-05-10T14:40:01+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
27
<p>有些奇淫技巧玩好的话,就能提升自己的逼格,这不,一行<code>js</code>代码实现一个贪吃蛇小游戏就成了装逼到了最高境界嘛!代码如下:</p>
<pre><code>(function(){var s = [41,40],d = 1,f = 43,x,c = document.createElement('canvas');c.width=400;c.height=400;c.style.background="#535353";c.textContent="当前浏览器不支持canvas标签";b=c.getContext('2d');function w(s,c){b.fillStyle = c;b.fillRect(s % 20 * 20, ~~(s / 20) * 20 , 18 , 18);};document.onkeydown=function(e){d = s[1] - s[0] == (x = [-1,-20,1,20][(e || event).keyCode - 37] || d ) ? d : x;};!(function(){s.unshift(x = s[0] + d);if(s.indexOf(x,1) > 0 || x < 0 || x > 399 || d == 1 && x % 20 == 0 || d == -1 && x % 20 == 19)return alert('游戏结束!');w(x,'#2396ef');x === f ? (()=>{while (s.indexOf(f = ~~(Math.random() * 399)) > 0);w(f,'#35e3dc');})() : w(s.pop(),'#535353');setTimeout(arguments.callee,300);})();document.body.appendChild(c);})();
</code></pre>
<p><code>ps</code>:我不是来装逼的。!</p>
<p><img src="/img/bVbssf2?w=287&h=264" alt="clipboard.png" title="clipboard.png"></p>
<p>好了,让我们来运行一下这行代码,看一下效果:</p>
<p><img src="/img/bVbssgz?w=1158&h=692" alt="clipboard.png" title="clipboard.png"></p>
<p>看动图看着不过瘾?,好,你自己去线上看看<code>demo</code>可以撒,<a href="https://link.segmentfault.com/?enc=W9soJsKD%2Bm5Q88GRxFsdHA%3D%3D.E6RYJTvt3Xee4zo%2FYBqRaP5zX0gMkDVF0uRVyV%2FQ1Whoh8lWTTBdZisMY6Q6G80V" rel="nofollow">具体示例</a>。</p>
<p><img src="/img/bVbssgV?w=259&h=300" alt="clipboard.png" title="clipboard.png"></p>
<p>装逼完成,<br><img src="/img/bVbssg4?w=224&h=126" alt="clipboard.png" title="clipboard.png"><br>。</p>
<p>好了,言归正传,我怎么可能是来装逼的,我要来分析一下,这个是怎么玩的,这才是我的目的。</p>
<p>让我们拆分代码来看:</p>
<p>首先,最外层包裹了一个自调用函数,如下:</p>
<pre><code>(function(){
//具体内容
})();
//自调用函数当然不止这样的写法
</code></pre>
<p>然后第二步,要画一个场景,那很明显,要用<code>HTML5</code>的<code>canvas</code>标签,我们可以采用<code>document.createElement()</code>这个方法来创建一个元素,继续:</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
})();
</code></pre>
<p>蛇运动的场景肯定是固定大小的,也就是说,我们要给<code>canvas</code>设置宽和高,在这里,我就是设置的<code>400X400</code>,然后给场景加一个背景。就这样:</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
})();
</code></pre>
<p>创建了<code>canvas</code>元素,我们要添加到<code>DOM</code>网页中去,所以用<code>appendChild()</code>来添加。如下:</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
document.body.appendChild(c);
})();
</code></pre>
<p>哦,有些浏览器可能不支持<code>canvas</code>标签,所以我们不能忘了给一个优雅的提示:</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
c.textContent="当前浏览器不支持canvas标签";
document.body.appendChild(c);
})();
</code></pre>
<p>场景画好了,接下来,我们要在场景上画蛇,所以<code>canvas.getContext('2d')</code>这个方法是必不可少的。</p>
<pre><code> (function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
document.body.appendChild(c);
})();
</code></pre>
<p>接下来分析蛇的构成,实际上这里的场景,我们可以看成是<code>400</code>个<code>20*20</code>的块组成,那么蛇也就可以看成一个块一个块的组成,用技术术语来说就是一个队列,也就是一个数组,<code>[20]</code>。在这里初始化蛇为<code>2</code>个块,也就是<code>[20,20]</code>,那么蛇初始化位置还是稍微调整一下嘛,所以也就改成<code>[40,40]</code>,其实在这里第一次出现的时候,是隐藏了一个食物的,默认就在蛇初始化位置的下一格,然后蛇会立即吃掉,然后就随机出现下一个食物的位置了,因此默认食物的位置是<code>40</code>。为了一个方便细小的微差,就稍微调大一点,为<code>43</code>。好了,现在我们要知道蛇运行的方向,蛇可以上下左右活动,那么,我们定义为<code>s[1] - s[0]</code>,这是根据位置来计算的。咱们先定义蛇再说:</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的
d = 1,//定义蛇活动方向,默认向右
f = 42;//默认食物的位置
document.body.appendChild(c);
})();
</code></pre>
<p>接下来,开始绘制蛇,蛇活动的轨迹与食物。定义一个函数,利用<code>canvas.fillStyle()</code>填充背景,在这里蛇与食物还有蛇活动的轨迹都是一个小方块矩形,这样我们就使用<code>canvas.fillRect()</code>来绘制一个矩形,这个方法有四个参数,如下图所示:</p>
<p><img src="/img/bVbsslP?w=866&h=237" alt="clipboard.png" title="clipboard.png"></p>
<p>蛇活动的轨迹坐标,也就是定义的蛇数组的坐标,因此第一个参数就是<code>s % 20 * 20</code>,第二个参数就是<code>~~(s / 20 * 20)</code>。关于<code>~~</code>这个操作符,如果不理解的话,可以看我的文章<a href="https://segmentfault.com/a/1190000018241410">浅谈JavaScript位操作符</a>。这里感觉有些抽象,实际上,需要靠自己的想象力来想象理解。</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42;//默认食物的位置
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
document.body.appendChild(c);
})();
</code></pre>
<p>蛇每吃掉一个食物,也就是往数组中添加一个<code>20*20</code>的小块,当然为了区分方向,这里的小块宽高应该与绘制的矩形宽高有关。因此,我们还要定义一个变量来代表蛇吃掉食物后,随机出现的下一个食物出现的位置。</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
document.body.appendChild(c);
})();
</code></pre>
<p>接下来就是用户按键盘的方向键,当然也可以是<code>wsad</code>字母键来控制蛇活动的方向。不过我们需要知道键盘键的<code>keyCode</code>,方向键的<code>keyCode</code>分别是:<code>向左:37,向上:38,,向右:39,向下:40</code>。键盘事件为<code>onkeydown</code>。</p>
<pre><code>
(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
//按方向键控制蛇运动方向,这里根据食物的位置来控制方向,防止用户随便更改方向,然后游戏崩溃
document.onkeydown = function(e){
d = s[1] - s[0] === (x = [-1,-20,1,20][e || event].keyCode - 37 ] || d) ? d : x;
}
document.body.appendChild(c);
})();
</code></pre>
<p>然后蛇吃掉一个食物就应该添加下一个食物,在这里用<code>unshift()</code>方法来添加数组。</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
//按方向键控制蛇运动方向,这里根据食物的位置来控制方向,防止用户随便更改方向,然后游戏崩溃
document.onkeydown = function(e){
//方向由蛇头来确定,初始化蛇只有2个小方块组成,因此蛇的方向就是s[1] - s[0]
d = s[1] - s[0] === (x = [-1,-20,1,20][e || event].keyCode - 37 ] || d) ? d : x;
}
!(function(){
s.unshift(x = s[0] + d);
})();//这也是一种自调用函数写法
document.body.appendChild(c);
})();
</code></pre>
<p>接下来判断蛇如果撞墙或者撞到了自身,则游戏结束。</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
//按方向键控制蛇运动方向,这里根据食物的位置来控制方向,防止用户随便更改方向,然后游戏崩溃
document.onkeydown = function(e){
//方向由蛇头来确定,初始化蛇只有2个小方块组成,因此蛇的方向就是s[1] - s[0]
d = s[1] - s[0] === (x = [-1,-20,1,20][e || event].keyCode - 37 ] || d) ? d : x;
}
!(function(){
s.unshift(x = s[0] + d);
//判断蛇如果撞墙或者撞到了自身,则游戏结束
if(s.indexOf(x,1) > 0 || x < 0 || x > 399 || d == 1 && x % 20 == 0 || d == -1 && x % 20 == 19)return alert('游戏结束');
})();//这也是一种自调用函数写法
document.body.appendChild(c);
})();
</code></pre>
<p>然后开始画食物,以及判断食物是否被蛇吃掉,如果吃掉了,则随机生成下一个食物。</p>
<pre><code>(function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
//按方向键控制蛇运动方向,这里根据食物的位置来控制方向,防止用户随便更改方向,然后游戏崩溃
document.onkeydown = function(e){
//方向由蛇头来确定,初始化蛇只有2个小方块组成,因此蛇的方向就是s[1] - s[0]
d = s[1] - s[0] === (x = [-1,-20,1,20][e || event].keyCode - 37 ] || d) ? d : x;
}
!(function(){
s.unshift(x = s[0] + d);
//判断蛇如果撞墙或者撞到了自身,则游戏结束
if(s.indexOf(x,1) > 0 || x < 0 || x > 399 || d == 1 && x % 20 == 0 || d == -1 && x % 20 == 19)return alert('游戏结束');
//然后开始画蛇节点的颜色
w(x,'#e641d3');
//判断蛇是不是吃到食物,如果吃到则重新随机生成一个节点也就是新食物的坐标,Math.random()方法表示取随机数,因为方向有可能是负的,所以用到了~~符号表示取绝对值到正数.~就是先取反再减一的意思.
if(x == f){
while (s.indexOf(f = ~~(Math.random() * 399)) > 0);
//重新画食物颜色
w(f,'#35e3dc');
}else{
//蛇吃到食物,蛇身会变长,所以不会改变蛇的运动轨迹
w(s.pop(),'#535353');
}
})();//这也是一种自调用函数写法
document.body.appendChild(c);
})();
</code></pre>
<p>最后,让蛇按一定的时间运行,如下:</p>
<pre><code> (function(){
//创建canvas标签
var c = document.createElement('canvas');
c.width = 400;
c.height = 400;
c.style.background = '#535353';
var b = c.getContext('2d');
var s = [41,40],//这里41也是有讲究的,代表默认向右方向
d = 1,//定义蛇活动方向,默认向右,蛇运动方向为s[1] - s[0]
f = 42,//默认食物的位置
x;
//这个函数既是绘制蛇方块,也是绘制食物与蛇活动轨迹的定义
function w(s,c){
b.fillStyle = c;
b.fillRect(s % 20 * 20,~~(s / 20 * 20),18,18);
}
//按方向键控制蛇运动方向,这里根据食物的位置来控制方向,防止用户随便更改方向,然后游戏崩溃
document.onkeydown = function(e){
//方向由蛇头来确定,初始化蛇只有2个小方块组成,因此蛇的方向就是s[1] - s[0]
d = s[1] - s[0] === (x = [-1,-20,1,20][e || event].keyCode - 37 ] || d) ? d : x;
}
!(function(){
s.unshift(x = s[0] + d);
//判断蛇如果撞墙或者撞到了自身,则游戏结束
if(s.indexOf(x,1) > 0 || x < 0 || x > 399 || d == 1 && x % 20 == 0 || d == -1 && x % 20 == 19)return alert('游戏结束');
//然后开始画蛇节点的颜色
w(x,'#e641d3');
//判断蛇是不是吃到食物,如果吃到则重新随机生成一个节点也就是新食物的坐标,Math.random()方法表示取随机数,因为方向有可能是负的,所以用到了~~符号表示取绝对值到正数.~就是先取反再减一的意思.
if(x == f){
while (s.indexOf(f = ~~(Math.random() * 399)) > 0);
//重新画食物颜色
w(f,'#35e3dc');
}else{
//蛇吃到食物,蛇身会变长,所以不会改变蛇的运动轨迹
w(s.pop(),'#535353');
}
//这是一种递归的写法
setTimeout(arguments.callee,300);
})();//这也是一种自调用函数写法
document.body.appendChild(c);
})();
</code></pre>
<p>到此为止,就拆分完了,其实这里的逻辑不算难,难的是计算蛇与蛇运动轨迹还有食物的坐标。能够理解透,就看个人的数学知识了,哈哈。最后,将这么多代码整合成一行代码,就可以好好的装逼了!</p>
<p><code>ps</code>:本文代码借鉴国外某大神20多行js代码实现贪吃蛇,在源代码基础上进行分析并加以修改封装而成,不喜勿喷。</p>
<p><img src="/img/bVbssAt?w=400&h=400" alt="clipboard.png" title="clipboard.png"></p>
<p>鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:</p>
<p><img src="/img/bVbslX3?w=256&h=256" alt="clipboard.png" title="clipboard.png"></p>
js验证身份证号码记录
https://segmentfault.com/a/1190000018993609
2019-04-26T10:03:53+08:00
2019-04-26T10:03:53+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
1
<p>在一些需要填写身份证的表单网页中,需要对身份证的输入做一个验证,于是,我记录下了自己写的验证。在写验证之前,我们需要理解身份证的一些常识规则。中华人民共和国居民身份证验证规则如下:</p>
<h2>1.号码的结构:</h2>
<ul>
<li>公民身份号码是特征组合码,由十七位数字本体码和一位校验码组成。</li>
<li>排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,</li>
<li>三位数字顺序码和一位数字校验码。</li>
</ul>
<h2>2.地址码</h2>
<p>* 表示编码对象常住户口所在县(市、旗、区)的行政区划代码,按GB/T2260的规定执行。</p>
<h2>3.出生日期码</h2>
<p>* 表示编码对象出生的年、月、日,按GB/T7408的规定执行,年、月、日代码之间不用分隔符。</p>
<h2>4.顺序码</h2>
<p>* 表示在同一地址码所标识的区域范围内,对同年、同月、同日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配给女性。</p>
<h2>5.校验码</h2>
<p>* 根据前面十七位数字码,按照ISO 7064:1983.MOD 11-2校验码计算出来的检验码。</p>
<p>而计算方法则为:</p>
<ul>
<li>1、将前面的身份证号码17位数分别乘以不同的系数。</li>
<li>从第一位到第十七位的系数分别为:<code>7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2</code>。</li>
<li>2、将这17位数字和系数相乘的结果相加。</li>
<li>3、用加出来和除以11,看余数是多少?</li>
<li>4、余数只可能有<code>0-1-2-3-4-5-6-7-8-9-10</code>这11个数字。</li>
<li>其分别对应的最后一位身份证的号码为<code>1-0-X -9-8-7-6-5-4-3-2</code>。</li>
<li>5、通过上面得知如果余数是3,就会在身份证的第18位数字上出现的是9。如果对应的数字是2,身份证的最后一位号码就是罗马数字x。</li>
</ul>
<p>例如:某男性的身份证号码为<code>53010219200508011x</code>, 我们看看这个身份证是不是合法的身份证。</p>
<ul>
<li>首先我们得出前17位的乘积和<code>[(5*7)+(3*9)+(0*10)+(1*5)+(0*8)+(2*4)+(1*2)+(9*1)+(2*6)+(0*3)+(0*7)+(5*9)+(0*10)+(8*5)+(0*8)+(1*4)+(1*2)]</code>是189,</li>
<li>然后用189除以11得出的结果是189/11=17----2,也就是说其余数是2。</li>
<li>最后通过对应规则就可以知道余数2对应的检验码是X。所以,可以判定这是一个正确的身份证号码。</li>
</ul>
<p>根据以上的规则和计算方法,我们就可以封装一个验证身份证的函数,如下:</p>
<pre><code> const validateIdCard = function (idcard) {
// 判断如果传入的不是一个字符串,则转换成字符串
idcard = typeof idcard === 'string' ? idcard : String(idcard);
//正则表达式验证号码的结构
let regx = /^[\d]{17}[0-9|X|x]{1}$/;
if (regx.test(idcard)) {
// 验证前面17位数字,首先定义前面17位系数
let sevenTeenIndex = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
// 截取参数前17位
let front_seventeen = idcard.slice(0, 17);
// 截取第18位
let eighteen = idcard.slice(17, 18);
// 这里如果是X要转换成小写,如果是数字在这里是字符串类型,则转换成数字类型,好做判断
eighteen = isNaN(parseInt(eighteen)) ? eighteen.toLowerCase() : parseInt(eighteen);
// 定义一个变量计算系数乘积之和余数
let remainder = 0;
//利用循环计算前17位数与系数乘积并添加到一个数组中
// charAt()类似数组的访问下标一样,访问单个字符串的元素,返回的是一个字符串因此要转换成数字
for (let i = 0; i < 17; i++) {
remainder = (remainder += parseInt(front_seventeen.charAt(i)) * sevenTeenIndex[i]) % 11;
}
//余数对应数字数组
let remainderKeyArr = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2];
// 取得余数对应的值
let remainderKey = remainderKeyArr[remainder] === 'X' ? remainderKeyArr[remainder].toLowerCase() : remainderKeyArr[remainder];
console.log(remainderKey);
console.log(eighteen)
// 如果最后一位数字对应上了余数所对应的值,则验证合格,否则不合格,
// 由于不确定最后一个数字是否是大小写的X,所以还是都转换成小写进行判断
if (eighteen === remainderKey) {
return idcard;
} else {
console.log('你输入的身份证号码格式不对!')
}
} else {
console.log('你输入的身份证号码格式不对,请重新输入!')
}
}
//函数调用
validateIdCard('53010219200508011x');//验证合格
</code></pre>
<p>鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:</p>
<p><img src="/img/bVbslX3?w=256&h=256" alt="clipboard.png" title="clipboard.png"></p>
vue.js实现一个会动的简历(包含底部导航功能,编辑功能,添加了用户自定义写字速度功能)
https://segmentfault.com/a/1190000018779991
2019-04-07T11:26:28+08:00
2019-04-07T11:26:28+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
12
<p>在网上看到一个这样的网站,<a href="https://link.segmentfault.com/?enc=a8zDh7BfDRPUTiYuttFpEg%3D%3D.FDPGibuM44sXopRQLQk1uWtH4ypZzQI3Uni53cK8P5Q%3D" rel="nofollow">STRML</a>它的效果看着十分有趣,如下图所示:<br><img src="/img/bVbqXwx" alt="图片描述" title="图片描述"></p><p>这个网站是用<code>react.js</code>来写的,于是,我就想着用<code>vue.js</code>也来写一版,开始撸代码。</p><p>首先要分析打字的原理实现,假设我们定义一个字符串<code>str</code>,它等于一长串注释加<code>CSS</code>代码,并且我们看到,当<code>css</code>代码写完一个分号的时候,它写的样式就会生效。我们知道要想让一段<code>CSS</code>代码在页面生效,只需要将其放在一对<code><style></code>标签对中即可。比如:</p><pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
红色字体
<style>
body{
color:#f00;
}
</style>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=0XFyK1%2BtdiYKT6i7h33NEg%3D%3D.x8P4yoqpOQ9g3IopkCOg1%2Bni%2B6w11xAI3phFjRwcXpq15I6NeqPb46M3bbRes%2BOc" rel="nofollow">具体示例</a>查看效果。 </p><p>当看到打字效果的时候,我们不难想到,这是要使用<code>间歇调用(定时函数:setInterval())</code>或<code>超时调用(延迟函数:setTimeout())</code>加<code>递归</code>去模拟实现<code>间歇调用</code>。一个包含一长串代码的字符串,它是一个个截取出来,然后分别写入页面中,在这里,我们需要用到字符串的截取方法,如<code>slice(),substr(),substring()</code>等,选择用哪个截取看个人,不过需要注意它们之间的区别。好了,让我们来实现一个简单的这样打字的效果,如下:</p><pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="result"></div>
<script>
var r = document.getElementById('result');
var c = 0;
var code = 'body{background-color:#f00;color:#fff};'
var timer = setInterval(function(){
c++;
r.innerHTML = code.substr(0,c);
if(c >= code.length){
clearTimeout(timer);
}
},50)
</script>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=jZOTFQ76SLi298aX5EZNgw%3D%3D.D%2BmFHFqAbPNiF91I%2FJsVRXKS11wUEA2zCFTW%2F%2BV5gQURmKXaf1prohyse6p6ge5IYpVGM4YYgt6AL2xGSYa5Jw%3D%3D" rel="nofollow">具体示例</a>查看效果。好的,让我们来分析一下以上代码的原理,首先放一个用于包含代码显示的标签,然后定义一个包含代码的字符串,接着定义一个初始值为<code>0</code>的变量,为什么要定义这样一个变量呢?我们从实际效果中看到,它是一个字一个字的写入到页面中的。初始值是没有一个字符的,所以,我们就从第<code>0</code>个开始写入,<code>c</code>一个字一个字的加,然后不停的截取字符串,最后渲染到标签的内容当中去,当<code>c</code>的值大于等于了字符串的长度之后,我们需要清除定时器。定时函数看着有些不太好,让我们用超时调用结合递归来实现。</p><pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="result"></div>
<script>
var r = document.getElementById('result');
var c = 0;
var code = 'body{background-color:#f00;color:#fff};';
var timer;
function write(){
c++;
r.innerHTML = code.substr(0,c);
if(c >= code.length && timer){
clearTimeout(timer)
}else{
setTimeout(write,50);
}
}
write();
</script>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=i6ROY3okE8zITLt%2FmFVqpg%3D%3D.48pXcrjOCLqAZ%2F2vtoj%2FJ4jh9sbi3uSatJ25FcwgJs0iAY7TPd3vgknoEA3YgCLeoDa2dUepiN6fRbJFGK0E0Q%3D%3D" rel="nofollow">具体示例</a>查看效果。</p><p>好了,到此为止,算是实现了第一步,让我们继续,接下来,我们要让代码保持空白和缩进,这可以使用<code><pre></code>标签来实现,但其实我们还可以使用css代码的<code>white-space</code>属性来让一个普通的<code>div</code>标签保持这样的效果,为什么要这样做呢,因为我们还要实现一个功能,就是编辑它里面的代码,可以让它生效。更改一下代码,如下:</p><pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
#result{
white-space:pre-wrap;
oveflow:auto;
}
</style>
</head>
<body>
<div id="result"></div>
<script>
var r = document.getElementById('result');
var c = 0;
var code = `
body{
background-color:#f00;
color:#fff;
}
`
var timer;
function write(){
c++;
r.innerHTML = code.substr(0,c);
if(c >= code.length && timer){
clearTimeout(timer)
}else{
setTimeout(write,50);
}
}
write();
</script>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=OcwomX5VIGzq6wqDXT5rlg%3D%3D.oLbndIEz%2FhUq28D%2BvrijbmPVKtyBt1JKT4qq%2BMKCY6CXtyaxwt8ByIAvKc7t2rkB%2FjcoFk6MMnnQFHFY8IFO6w%3D%3D" rel="nofollow">具体示例</a>查看效果。</p><p>接下来,我们还要让样式生效,这很简单,将代码在<code>style</code>标签中写一次即可,请看:</p><pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
#result{
white-space:pre-wrap;
overflow:auto;
}
</style>
</head>
<body>
<div id="result"></div>
<style id="myStyle"></style>
<script>
var r = document.getElementById('result'),
t = document.getElementById('myStyle');
var c = 0;
var code = `
body{
background-color:#f00;
color:#fff;
}
`;
var timer;
function write(){
c++;
r.innerHTML = code.substr(0,c);
t.innerHTML = code.substr(0,c);
if(c >= code.length){
clearTimeout(timer);
}else{
setTimeout(write,50);
}
}
write();
</script>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=wGmZJYJTByJC6qlLATFTTA%3D%3D.gkrC5UUphwokH35ShzbWwQnGOCJzNWpXwJk53qnbt0KVZi4C8QP2B31i6Hz07mPg0Pz4Cu7AT5Bq2SAZd7FXAQ%3D%3D" rel="nofollow">具体示例</a>查看效果。</p><p>我们看到代码还会有高亮效果,这可以用正则表达式来实现,比如以下一个<code>demo</code>:</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>代码编辑器</title>
<style>
* {
margin: 0;
padding: 0;
}
.ew-code {
tab-size: 4;
-moz-tab-size: 4;
-o-tab-size: 4;
margin-left: .6em;
background-color: #345;
white-space: pre-wrap;
color: #f2f2f2;
text-indent: 0;
margin-right: 1em;
display: block;
overflow: auto;
font-size: 20px;
border-radius: 5px;
font-style: normal;
font-weight: 400;
line-height: 1.4;
font-family: Consolas, Monaco, "宋体";
margin-top: 1em;
}
.ew-code span {
font-weight: bold;
}
</style>
</head>
<body>
<code class="ew-code">
&lt;div id="app"&gt;
&lt;p&gt;{{ greeting }} world!&lt;/p&gt;
&lt;/div&gt;
</code>
<code class="ew-code">
//定义一个javascript对象
var obj = {
greeting: "Hello,"
};
//创建一个实例
var vm = new Vue({
data: obj
});
/*将实例挂载到根元素上*/
vm.$mount(document.getElementById('app'));
</code>
<script>
var lightColorCode = {
importantObj: ['JSON', 'window', 'document', 'function', 'navigator', 'console', 'screen', 'location'],
keywords: ['if', 'else if', 'var', 'this', 'alert', 'return', 'typeof', 'default', 'with', 'class', 'export', 'import', 'new'],
method: ['Vue', 'React', 'html', 'css', 'js', 'webpack', 'babel', 'angular', 'bootstap', 'jquery', 'gulp','dom'],
// special: ["*", ".", "?", "+", "$", "^", "[", "]", "{", "}", "|", "\\", "(", ")", "/", "%", ":", "=", ';']
}
function setHighLight(el) {
var htmlStr = el.innerHTML;
//匹配单行和多行注释
var regxSpace = /(\/\/\s?[^\s]+\s?)|(\/\*(.|\s)*?\*\/)/gm,
matchStrSpace = htmlStr.match(regxSpace),
spaceLen;
//匹配特殊字符
var regxSpecial = /[`~!@#$%^&.{}()_\-+?|]/gim,
matchStrSpecial = htmlStr.match(regxSpecial),
specialLen;
var flag = false;
if(!!matchStrSpecial){
specialLen = matchStrSpecial.length;
}else{
specialLen = 0;
return;
}
for(var k = 0;k < specialLen;k++){
htmlStr = htmlStr.replace(matchStrSpecial[k],'<span style="color:#b9ff01;">' + matchStrSpecial[k] + '</span>');
}
for (var key in lightColorCode) {
if (key === 'keywords') {
lightColorCode[key].forEach(function (imp) {
htmlStr = htmlStr.replace(new RegExp(imp, 'gim'), '<span style="color:#00ff78;">' + imp + '</span>')
})
flag = true;
} else if (key === 'importantObj') {
lightColorCode[key].forEach(function (kw) {
htmlStr = htmlStr.replace(new RegExp(kw, 'gim'), '<span style="color:#ec1277;">' + kw + '</span>')
})
flag = true;
} else if (key === 'method') {
lightColorCode[key].forEach(function (mt) {
htmlStr = htmlStr.replace(new RegExp(mt, 'gim'), '<span style="color:#52eeff;">' + mt + '</span>')
})
flag = true;
}
}
if (flag) {
if (!!matchStrSpace) {
spaceLen = matchStrSpace.length;
} else {
spaceLen = 0;
return;
}
for(var i = 0;i < spaceLen;i++){
var curFont;
if(window.innerWidth <= 1200){
curFont = '12px';
}else{
curFont = '14px';
}
htmlStr = htmlStr.replace(matchStrSpace[i],'<span style="color:#899;font-size:'+curFont+';">' + matchStrSpace[i] + '</span>');
}
el.innerHTML = htmlStr;
}
}
var codes = document.querySelectorAll('.ew-code');
for (var i = 0, len = codes.length; i < len; i++) {
setHighLight(codes[i])
}
</script>
</body>
</html>
</code></pre><p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=ws8CMEeQwcmbN7Zqa0Dc0Q%3D%3D.LX%2BJlG64%2Bla%2BsqDhpqvFxISqn2khlEWYeOrA4SviBUIMlehoyvI9klsLsqMjqoeZ98C%2BPuL6nLAJj7Jhyg6f8Q%3D%3D" rel="nofollow">具体示例</a>查看效果。</p><p>不过这里为了方便,我还是使用插件<code>Prism.js</code>,另外在这里,我们还要用到将一个普通文本打造成<code>HTML</code>网页的插件<code>marked.js</code>。</p><p>接下来分析如何暂停动画和继续动画,很简单,就是清除定时器,然后重新调用即可。如何让编辑的代码生效呢,这就需要用到自定义事件<code>.sync</code>事件修饰符,自行查看官网<code>vue.js</code>。</p><p>虽然这里用原生<code>js</code>也可以实现,但我们用<code>vue-cli</code>结合组件的方式来实现,这样更简单一些。好了,让我们开始吧:</p><p>新建一个<code>vue-cli</code>工程(步骤自行百度):</p><p>新建一个<code>styleEditor.vue</code>组件,代码如下:</p><pre><code><template>
<div class="container">
<div class="code" v-html="codeInstyleTag"></div>
<div class="styleEditor" ref="container" contenteditable="true" @blur="updateCode($event)" v-html="highlightedCode"></div>
</div>
</template>
<script>
import Prism from 'prismjs'
export default {
name:'Editor',
props:['code'],
computed:{
highlightedCode:function(){
//代码高亮
return Prism.highlight(this.code,Prism.languages.css);
},
// 让代码生效
codeInstyleTag:function(){
return `<style>${this.code}</style>`
}
},
methods:{
//每次打字到最底部,就要滚动
goBottom(){
this.$refs.container.scrollTop = 10000;
},
//代码修改之后,可以重新生效
updateCode(e){
this.$emit('update:code',e.target.textContent);
}
}
}
</script>
<style scoped>
.code{
display:none;
}
</style>
</code></pre><p>新建一个<code>resumeEditor.vue</code>组件,代码如下:</p><pre><code><template>
<div class = "resumeEditor" :class="{htmlMode:enableHtml}" ref = "container">
<div v-if="enableHtml" v-html="result"></div>
<pre v-else>{{result}}</pre>
</div>
</template>
<script>
import marked from 'marked'
export default {
props:['markdown','enableHtml'],
name:'ResumeEditor',
computed:{
result:function(){
return this.enableHtml ? marked(this.markdown) : this.markdown
}
},
methods:{
goBottom:function(){
this.$refs.container.scrollTop = 10000
}
}
}
</script>
<style scoped>
.htmlMode{
anmation:flip 3s;
}
@keyframes flip{
0%{
opactiy:0;
}
100%{
opactiy:1;
}
}
</style>
</code></pre><p>新建一个底部导航菜单组件<code>bottomNav.vue</code>,代码如下:</p><pre><code><template>
<div id="bottom">
<a id="pause" @click="pauseFun">{{ !paused ? '暂停动画' : '继续动画 ||' }}</a>
<a id="skipAnimation" @click="skipAnimationFun">跳过动画</a>
<p>
<span v-for="(url,index) in demourl" :key="index">
<a :href="url.url">{{ url.title }}</a>
</span>
</p>
<div id="music" @click="musicPause" :class="playing ? 'rotate' : ''" ref="music"></div>
</div>
</template>
<script>
export default{
name:'bottom',
data(){
return{
demourl:[
{url:'http://eveningwater.com/',title:'个人网站'},
{url:'https://github.com/eveningwater',title:'github'}
],
paused:false,//暂停
playing:false,//播放图标动画
autoPlaying:false,//播放音频
audio:''
}
},
mounted(){
},
methods:{
// 播放音乐
playMusic(){
this.playing = true;
this.autoPlaying = true;
// 创建audio标签
this.audio = new Audio();
this.audio.loop = 'loop';
this.audio.autoplay = 'autoplay';
this.audio.src = "http://eveningwater.com/project/newReact-music-player/audio/%E9%BB%84%E5%9B%BD%E4%BF%8A%20-%20%E7%9C%9F%E7%88%B1%E4%BD%A0%E7%9A%84%E4%BA%91.mp3";
this.$refs.music.appendChild(this.audio);
},
// 跳过动画
skipAnimationFun(e){
e.preventDefault();
this.$emit('on-skip');
},
// 暂停动画
pauseFun(e){
e.preventDefault();
this.paused = !this.paused;
this.$emit('on-pause',this.paused);
},
// 暂停音乐
musicPause(){
this.playing = !this.playing;
if(!this.playing){
this.audio.pause();
}else{
this.audio.play();
}
}
}
}
</script>
<style scoped>
#bottom{
position:fixed;
bottom:5px;
left:0;
right:0;
}
#bottom p{
float:right;
}
#bottom a{
text-decoration: none;
color: #999;
cursor:pointer;
margin-left:5px;
}
#bottom a:hover,#bottom a:active{
color: #010a11;
}
</style>
</code></pre><p>接下来是核心<code>APP.vue</code>组件代码:</p><pre><code><template>
<div id="app">
<div class="main">
<StyleEditor ref="styleEditor" v-bind.sync="currentStyle"></StyleEditor>
<ResumeEditor ref="resumeEditor" :markdown = "currentMarkdown" :enableHtml="enableHtml"></ResumeEditor>
</div>
<BottomNav ref ="bottomNav" @on-pause="pauseAnimation" @on-skip="skipAnimation"></BottomNav>
</div>
</template>
<script>
import ResumeEditor from './components/resumeEditor'
import StyleEditor from './components/styleEditor'
import BottomNav from './components/bottomNav'
import './assets/common.css'
import fullStyle from './style.js'
import my from './my.js'
export default {
name: 'app',
components: {
ResumeEditor,
StyleEditor,
BottomNav
},
data() {
return {
interval: 40,//写入字的速度
currentStyle: {
code: ''
},
enableHtml: false,//是否打造成HTML网页
fullStyle: fullStyle,
currentMarkdown: '',
fullMarkdown: my,
timer: null
}
},
created() {
this.makeResume();
},
methods: {
// 暂停动画
pauseAnimation(bool) {
if(bool && this.timer){
clearTimeout(this.timer);
}else{
this.makeResume();
}
},
// 快速跳过动画
skipAnimation(){
if(this.timer){
clearTimeout(this.timer);
}
let str = '';
this.fullStyle.map((f) => {
str += f;
})
setTimeout(() => {
this.$set(this.currentStyle,'code',str);
},100)
this.currentMarkdown = my;
this.enableHtml = true;
this.$refs.bottomNav.playMusic();
},
// 加载动画
makeResume: async function() {
await this.writeShowStyle(0)
await this.writeShowResume()
await this.writeShowStyle(1)
await this.writeShowHtml()
await this.writeShowStyle(2)
await this.$nextTick(() => {this.$refs.bottomNav.playMusic()});
},
// 打造成HTML网页
writeShowHtml: function() {
return new Promise((resolve, reject) => {
this.enableHtml = true;
resolve();
})
},
// 写入css代码
writeShowStyle(n) {
return new Promise((resolve, reject) => {
let showStyle = (async function() {
let style = this.fullStyle[n];
if (!style) return;
//计算出数组每一项的长度
let length = this.fullStyle.filter((f, i) => i <= n).map((it) => it.length).reduce((t, c) => t + c, 0);
//当前要写入的长度等于数组每一项的长度减去当前正在写的字符串的长度
let prefixLength = length - style.length;
if (this.currentStyle.code.length < length) {
let l = this.currentStyle.code.length - prefixLength;
let char = style.substring(l, l + 1) || ' ';
this.currentStyle.code += char;
if (style.substring(l - 1, l) === '\n' && this.$refs.styleEditor) {
this.$nextTick(() => {
this.$refs.styleEditor.goBottom();
})
}
this.timer = setTimeout(showStyle, this.interval);
} else {
resolve();
}
}).bind(this)
showStyle();
})
},
// 写入简历
writeShowResume() {
return new Promise((resolve, reject) => {
let length = this.fullMarkdown.length;
let showResume = () => {
if (this.currentMarkdown.length < length) {
this.currentMarkdown = this.fullMarkdown.substring(0, this.currentMarkdown.length + 1);
let lastChar = this.currentMarkdown[this.currentMarkdown.length - 1];
let prevChar = this.currentMarkdown[this.currentMarkdown.length - 2];
if (prevChar === '\n' && this.$refs.resumeEditor) {
this.$nextTick(() => {
this.$refs.resumeEditor.goBottom()
});
}
this.timer = setTimeout(showResume, this.interval);
} else {
resolve()
}
}
showResume();
})
}
}
}
</script>
<style scoped>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.main {
position: relative;
}
html {
min-height: 100vh;
}
* {
transition: all 1.3s;
}
</style>
</code></pre><p>到此为止,一个可以快速跳过动画,可以暂停动画,还有音乐播放,还能自由编辑代码的会动的简历已经完成,还添加了用户来控制写字速度快慢的功能。代码已上传至<a href="https://link.segmentfault.com/?enc=qaRYTkx%2F84OTt156r3YBRA%3D%3D.p0y9mxgGq4HoBhK2g6PZ8uXgwA45XpnUQAWLHamQHKaPbnI3KmL7q1Vntx1fTp8k" rel="nofollow">git源码</a>,欢迎<code>fork</code>,也望不吝啬<code>star</code>。</p><p><a href="https://link.segmentfault.com/?enc=5Nm1B7a1HqqqGAgjkaT8Gw%3D%3D.%2FB28BH9oUwY3KQVI7JUjj%2B4Eo6mZRGiT7ShEoxBBEjcLyG0oAQ1WJpkAaNc3a61o" rel="nofollow">在线预览</a>。</p>
es6块级作用域
https://segmentfault.com/a/1190000018632501
2019-03-24T15:19:16+08:00
2019-03-24T15:19:16+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
0
<h2>一.var 声明与变量提升机制</h2>
<p>在<code>JavaScript</code>中使用<code>var</code>定义一个变量,无论是定义在全局作用域函数函数的局部作用域中,都会被提升到其作用域的顶部,这也是<code>JavaScript</code>定义变量的一个令人困惑的地方。由于<code>es5</code>没有像其它<code>类C</code>语言一样的块级作用域,因此<code>es6</code>增加了<code>let</code>定义变量,用来创建块级作用域。</p>
<p>我们来看一个var定义变量的示例:</p>
<pre><code>function setName(){
if(condition){
var name = 'loho';
console.log(name);
}else{
console.log(name);
}
}
var student = 'eveningwater';
setName();
</code></pre>
<p>以上代码可以理解成如下:</p>
<pre><code>var student;
function setName(){
var name;
if(condition){
name = 'loho';
console.log(name);//loho
}else{
console.log(name);//undefined
}
}
student = 'eveningwater';
setName();
</code></pre>
<h2>二.块级声明</h2>
<p>块级声明意在指定一个块级作用域,使得块级作用域中所定义的变量无法再全局被访问到,块级作用域也被称为词法作用域。</p>
<p>块级作用域存在于两个地方:</p>
<ol>
<li>函数内部。</li>
<li>指定代码块中。(即"{"和"}"之间的区域)</li>
</ol>
<h3>1.let 声明</h3>
<p><code>let</code>声明同<code>var</code>声明用法一致,唯一的区别在于,<code>let</code>声明将变量限制在一个块内,这样就形成了一个块级作用域,因此也就不会存在变量的提升了。</p>
<p>例如前面的示例,我们可以写成如下:</p>
<pre><code>let stundent = 'eveningwater';
function setName(){
if(condition){
let name = 'loho';
console.log(name);//loho
}else{
//如果条件为false执行到这里
console.log(name);//不返回值
}
}
setName();
</code></pre>
<h3>2.禁止重声明</h3>
<p>在使用<code>let</code>定义变量之前如果已经声明了相同的变量,就会报错。因此不能重复声明变量。如以下示例:</p>
<pre><code>var name = 'eveningwater';
//报错,重复声明
let name = 'loho';
</code></pre>
<p>当然这两个变量必须是在同一个作用域中,如果是不同作用域中,则不会报错。但有可能会遮蔽第一次声明的变量。如以下示例:</p>
<pre><code>var name = 'eveningwater';
if(condition){
//不会报错
let name = 'loho';
}
</code></pre>
<h3>3.const声明</h3>
<p>使用<code>const</code>标识符所声明的变量必须要初始化,因此这个声明的就是一个常量。如下例:</p>
<pre><code>const name='eveningwater';//正确
const name;//错误,未初始化
</code></pre>
<p><code>const</code>声明同<code>let</code>声明一样,也是创建了一个块级作用域,在这个块级作用域之外是无法访问到所声明的变量的。换句话说,就是<code>const</code>所声明的变量不会有变量提升机制。如下例:</p>
<pre><code>if(condition){
const name = 'eveningwater';
console.log(name);//'eveningwater'
}
//错误
console.log(name);
</code></pre>
<p>同样的<code>const</code>也不能重复声明,如下例:</p>
<pre><code>var name = 'eveningwater';
//错误,不能重复声明
const name = 'loho';
</code></pre>
<p>但也可以在不同作用域中重复声明,如下例:</p>
<pre><code>var name = 'eveningwater';
if(condition){
const name = 'loho';
console.log(name);//loho,屏蔽全局定义的变量
}
</code></pre>
<p>尽管<code>const</code>声明与<code>let</code>声明有太多相似的地方,但<code>const</code>声明也有一处与<code>let</code>声明不同,那就是<code>const</code>声明的变量不能被赋值,无论是在非严格模式下还是在严格模式下,都不能对<code>const</code>声明的变量进行赋值。如下例:</p>
<pre><code>const name = 'eveningwater';
//错误
name = 'loho';
</code></pre>
<p>不过,如果定义的是一个对象,可以对对象的值进行修改,如下例:</p>
<pre><code>const student = {
name:'eveningwater'
}
student.name = 'loho';//没问题
//错误,相当于赋值修改对象
student = {
name:'loho'
}
</code></pre>
<h3>4.临时死区</h3>
<p>前面提到<code>let</code>和<code>const</code>声明的变量都不会提升到作用域的顶部,因此在使用这两个标识符声明之前访问会报错,即使是<code>typeof</code>操作符也会触发引用错误。如下例:</p>
<pre><code>console.log(typeof name);//报错
const name = 'eveningwater';
</code></pre>
<p>由于第一行代码就报错了,因此后续的声明变量语句不会执行,此时就出现了<code>JavaScript</code>社区所谓的<code>"临时死区"(temporal dead zone)</code>.虽然这里示例是<code>const</code>声明,但<code>let</code>声明也是一样的。</p>
<p>但如果在<code>const</code>或<code>let</code>声明的变量的作用域之外使用<code>typeof</code>操作符监测却不会报错,只不过会返回<code>undefined</code>。如下例:</p>
<pre><code>console.log(typeof name);//undefined
if(condition){
let name = 'eveningwater';
}
</code></pre>
<h3>5.循环中的块级作用域绑定</h3>
<p>我们在使用<code>var</code>声明变量的时候,总会遇到这样的情况,如下:</p>
<pre><code>for(var i = 0;i < 100;i++){
//执行某些操作
}
//这里也能访问到变量i
console.log(i);//100
</code></pre>
<p>我们可以使用<code>let</code>声明将变量<code>i</code>限制在循环中,此时再在循环作用域之外访问变量<code>i</code>就会报错了,因为<code>let</code>声明已经为循环创建了一个块级作用域。如下:</p>
<pre><code>for(let i = 0;i < 100;i++){
//执行某些操作
}
//报错
console.log(i);
</code></pre>
<h3>6.循环中的创建函数</h3>
<p>在使用<code>var</code>声明变量的循环中,创建一个函数非常的困难,如下例:</p>
<pre><code>var func = [];
for(var i = 0;i < 5;i++){
func.push(function(){
console.log(i);
})
}
func.forEach(function(func){
func();
});
</code></pre>
<p>你可能预期想的是打印从<code>0到5</code>之间,即<code>0,1,2,3,4</code>的数字,但实际上答案并不是如此。由于函数有自己的作用域,因此在向数组中添加函数的时候,实际上循环已经运行完成,因此每次打印变量<code>i</code>的值都相当于是在全局中访问变量<code>i</code>的值,即<code>i = 5</code>这个值,因此实际上答案最终会返回<code>5次5</code>.</p>
<p>在<code>es5</code>中,我们可以使用<code>函数表达式(IIFE)</code>来解决这个问题,因为函数表达式会创建一个自己的块级作用域。如下:</p>
<pre><code>var func = [];
for(var i = 0;i < 5;i++){
(function(i){
func.push(function(){
console.log(i);
})
})(i)
}
func.forEach(function(func){
func();//这就是你想要的答案,输出0,1,2,3,4
})
</code></pre>
<p>;</p>
<p>但事实上有了<code>es6</code>的<code>let</code>声明,我们不必如此麻烦,只需要将<code>var</code>变成<code>let</code>声明就可以了,如下:</p>
<pre><code>var func = [];
for(let i = 0;i < 5;i++){
func.push(function(){
console.log(i);
})
}
func.forEach(function(func){
func();//输出0,1,2,3,4
})
</code></pre>
<p>但是这里不能使用<code>const</code>声明,因为前面提到过,<code>const</code>声明并初始化了一个常量之后是不能被修改的,只能在对象中被修改值。如以下示例就会报错:</p>
<pre><code>//在执行循环i++条件的时候就会报错
for(const i = 0;i < len;i++){
console.log(i);
}
</code></pre>
<p>因为<code>i++</code>这个语句就是在尝试修改常量<code>i</code>的值,因此不能将<code>const</code>声明用在<code>for</code>循环中,但可以将<code>const</code>声明用在<code>for-in</code>或者<code>for-of</code>循环中。如下:</p>
<pre><code>var func = [];
var obj = {
name:'eveningwater',
age:22
}
for(let key in obj){
func.push(function(){
console.log(key)
})
}
func.forEach(function(func){
func();//name,age
});
//以下也没问题
var func = [];
var obj = {
name:'eveningwater',
age:22
}
for(const key in obj){
func.push(function(){
console.log(key)
})
}
func.forEach(function(func){
func();//name,age
});
</code></pre>
<p>这里并没有修改<code>key</code>的值,因此使用<code>const</code>和<code>let</code>声明都可以,同理<code>for-of</code>循环也是一样的道理。<code>for-of</code>循环是<code>es6</code>的新增的循坏。。</p>
<h3>7.全局作用域绑定</h3>
<p><code>let,const</code>声明与<code>var</code>声明还有一个区别就是三者在全局作用域中的行为。当使用<code>var</code>声明一个变量时,会在全局作用域(通常情况下是浏览器window对象)中创建一个全局属性,这也就意味着可能会覆盖<code>window</code>对象中已经存在的一个全局变量。如下例:</p>
<pre><code>console.log(window.Array);//应该返回创建数组的构造函数,即f Array(){}
var Array = '这是数组';
console.log(window.Array);//返回'这是数组';
</code></pre>
<p>从上例,我们可以知道即使全局作用域中已经定义了<code>Array</code>变量或者已经存在了<code>Array</code>属性,但我们之后定义的<code>Array</code>变量则会覆盖之前已经定义好的或者已经存在的<code>Array</code>变量(属性)。</p>
<p>但是<code>es6</code>的<code>let</code>和<code>const</code>声明则不会出现这种情况,<code>let</code>和<code>const</code>声明会创建一个新的绑定,也就是说不会成为<code>window</code>对象的属性。换句话说,就是所声明的变量不会覆盖全局变量,而只会遮蔽它。如下例:</p>
<pre><code>let Array = '这是数组';
console.log(Array);//'这是数组‘;
console.log(window.Array);//应该返回创建数组的构造函数,即f Array(){}
</code></pre>
<p>这也就是说<code>window.Array !== Array</code>这个等式返回布尔值<code>true</code>。</p>
<h3>8.块级绑定的最佳实践</h3>
<p>在使用<code>es6</code>块级声明变量中,最佳实践是如果确定后续不会改变这个变量的值,用<code>const</code>声明,如果确定要改变这个变量的值,则用<code>let</code>声明。因为预料外的变量值的改变时很多<code>bug</code>出现的源头。如下示例:</p>
<pre><code>function eveningWater(){};
eveningWater.prototype.name = 'eveningwater';
let ew = new eveningWater();
//定义不能被修改的变量,也就是用于判断实例类型的属性
const _constructor = ew.constructor;
//可以改变自定义的名字属性
let name = ew.name;
if(_constructor === eveningWater || _constuctor === Object){
console.log(_constructor);
}else{
name = 'loho';
console.log(name)
}
</code></pre>
<p>鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:</p>
<p><img src="/img/bVbslX3?w=256&h=256" alt="clipboard.png" title="clipboard.png"></p>
使用es6实现iview的选项卡切换
https://segmentfault.com/a/1190000018372881
2019-03-03T21:45:09+08:00
2019-03-03T21:45:09+08:00
夕水
https://segmentfault.com/u/xishui_5ac9a340a5484
7
<p>代码如下:</p>
<pre><code>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>选项卡切换</title>
</head>
<body>
<my-tabs
index="0"
titles="['首页','核心产品','项目案例']"
contents="['<h1>公司地址:</h1><article>公司位于四川省成都市武侯区XX街道</article>','<h2>核心的产品1</h2><p>XXAPP是为XXX公司服务二产生的</p>','<a href=https://www.iviewui.com/ target=_blank>项目地址</a>']">
</my-tabs>
<script>
function ewEval(str) {
return new Function('return ' + str)();
}
class MyTabs extends HTMLElement {
constructor() {
super();
const shadom = this.attachShadow({
mode: "open"
});
let count = ewEval(this.getAttribute('index')) || 0,
tabTitles = ewEval(this.getAttribute('titles')) || ['标签一', '标签二', '标签三'],
tabContent = ewEval(this.getAttribute('contents')) || ['标签一的内容', '标签二的内容', '标签三的内容'],
title_len = tabTitles.length,
content_len = tabContent.length;
let header = document.createElement('header');
for (let i = 0; i < title_len; i++) {
let buttons = document.createElement('button');
buttons.innerHTML = tabTitles[i];
if (count === i) buttons.classList.add('active');
buttons.onclick = function () {
let curIndex = tabTitles.indexOf(this.textContent);
let _buttons = shadom.querySelectorAll('button'),
_divs = shadom.querySelectorAll('div');
for (let j = 0,len = _buttons.length; j < len; j++) {
if (curIndex !== j) {
_buttons[j].classList.remove('active');
_divs[j].classList.remove('active');
}
_buttons[curIndex].classList.add('active');
_divs[curIndex].classList.add('active');
}
}
header.appendChild(buttons);
}
shadom.appendChild(header);
for (let _i = 0; _i < content_len; _i++) {
let divs = document.createElement('div');
divs.innerHTML = tabContent[_i];
if (count === _i) divs.classList.add('active');
shadom.appendChild(divs);
}
let style = document.createElement('style');
style.textContent = `
header{
border-bottom:1px solid #dcdee2;
height:35px;
}
button{
outline:none;
cursor:pointer;
color:#515a6e;
margin-left:15px;
font-size:14px;
transition:color .2s linear,border-bottom .2s linear;
background-color:transparent;
padding:0 13px;
height:35px;
display:inline-block;
border:none;
}
a{
text-decoration:none;
color:#515a6e;
}
a:hover,a:active{
color:#2196ff;
}
button.active{
color:#2196ff;
border-bottom:1px solid #2192ff;
}
div{
display:none;
padding:13px 0;
}
div.active{
display:block;
}`;
shadom.appendChild(style);
}
}
customElements.define('my-tabs', MyTabs);
</script>
</body>
</html>
</code></pre>
<p>你可以狠狠点击此处<a href="https://link.segmentfault.com/?enc=5LjxJIWSaS8fziCiwlnW6Q%3D%3D.CpcbZdIu5t0VcCdHERkyObr9%2BhPMaC7Qf8zf3BEvp6ihgK2zS1nueWf9dGKyTm2f" rel="nofollow">具体示例</a>查看效果。这里还可以进行优化。</p>
<p>鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:</p>
<p><img src="/img/bVbslX3?w=256&h=256" alt="clipboard.png" title="clipboard.png"></p>