先睹为快
如下图,代码在自己一行一行写程序,逐渐画出一个喜气灯笼的模样(PC移动端都支持噢),想不想知道是它怎么实现的呢?和胖头鱼一起来探究一番吧O(∩_∩)O~
你也可以直接点击用程序自动画了一个灯笼体验一番,胖头鱼的掘金活动仓库查看源码
原理探究
这个效果就好像一个打字员在不断地录入文字,页面呈现动态效果。又好像一个早已经录制好影片,而我们只是坐在放映机前观看。
原理本身也非常简单,只要你会一点点前端知识,就可以马上亲手做一个出来
1. 滚动的代码
定时器字符累加: 相信聪明的你早已经猜到屏幕中滚动的html、css代码就是通过启动一个定时器,然后将预先准备好的字符,不断累加到一个pre标签中。
2. 灯笼的布局
动态添加html片段和css片段:,一张静态网页由html和css组成,灯笼能不断地发生变化,背后自然是组成灯笼的html和css不断变化的结果。
3. 例子解释
想象一下你要往一张网页每间隔0.1秒增加一个啊字,是不是开个定时器,间断地往body里面塞啊,就可以啊!没错,做到这一步就完成了原理的第一部分
再想象一下,在往页面里面塞啊的时候,我还想改变啊字的字体颜色以及网页背景颜色,那应该怎么做呢,是不是执行下面的代码就可以呢?
.xxx{ color: blue; background: red; } 复制代码
没错,只不过更改字体和背景色不是突然改变的,而是开个定时器,间断地往style标签中塞入以下代码,这样就完成了原理的第二步,是不是好简单 , 接下来让我们一步步完成它。
简要解析
1.编辑器布局
工欲善其事,必先利其器。在实现代码自己画画的前提是有个类似编辑器地方给他show,所以会有编辑html、css和预览三个区域。
移动端布局
上下结构布局,上面是html、css的编辑区域,下面的灯笼的展示区域
PC端布局
左右结构布局,左边是html、css的编辑区域,右边是灯笼的展示区域
模板
<template><div :class="containerClasses"><div class="edit"><div class="html-edit" ref="htmlEditRef"><!-- 这是html代码编辑区域 --><pre v-html="htmlEditPre" ref="htmlEditPreRef"></pre></div><div class="css-edit" ref="cssEditRef"><!-- 这是css代码编辑区域 --><pre v-html="styleEditPre"></pre></div></div><div class="preview"><!-- 这是预览区域,灯笼最终会被画到这里噢 --><div class="preview-html" v-html="previewHtmls"></div><!-- 这里是样式真正起作用的地方,密码就隐藏... --><div v-html="previewStyles"></div></div></div></template>
端控制
简单的做一下移动端和PC端的适配,然后通过样式去控制布局即可
computed: {containerClasses () {// 做一个简单的适配return ['container',isMobile() ? 'container-mobile' : '']}}
2.代码高亮
示例中的代码高亮是借助prismjs和pre进行转化处理的,只需要填充你想要高亮的代码,以及选择高亮的语言就可以实现上述效果。
// 核心代码,只有一行this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)
通过preview-html``承载html片段,通过previewStyles承载由style标签包裹的css样式
// 容器<div class="preview"><!-- 这是预览区域,灯笼最终会被画到这里噢 --><div class="preview-html" v-html="previewHtmls"></div><!-- 这里是样式真正起作用的地方 --><div v-html="previewStyles"></div></div>
逻辑代码
// 样式控制核心代码this.previewStyles = `<style>${previewStylesSource}</style>`// html控制核心代码this.previewHtmls = previewHtmls
4. 代码配置预览
我们通过一个个步骤将代码按阶段去执行,而代码本身是通过两个文件进行配置的,一个是控制html的文件,一个是控制css的文件。每一个步骤都是数组的一项
4.1 html配置
注意下面的代码格式是故意弄成这种格式的,并非是没有对齐
export default [// 开头寒暄`<!--XDM好,我是前端胖头鱼~~~听说掘金又在搞活动了,奖品还很丰厚...我能要那个美腻的小姐姐吗?-->`,// 说明主旨`<!--以前都是用“手”写代码,今天想尝试一下“代码写代码”,自动画一个喜庆的灯笼-->`,// 创建编辑器`<!--第①步,先创建一个编辑器-->`,// 创建编辑器html结构`<div class="container"><div class="edit"><div class="html-edit"><!-- 这是html代码编辑区域 --><pre v-html="htmlEditPre"><!-- htmlStep0 --></pre></div><div class="css-edit"><!-- 这是css代码编辑区域 --><pre v-html="cssEditPre"></pre></div></div><div class="preview"><!-- 这是预览区域,灯笼最终会被画到这里噢 --><div class="preview-html"></div><!-- 这里是样式真正起作用的地方,密码就隐藏... --><div v-html="cssEditPre"></div></div></div>`,// 开始画样式`<!--第②步,给编辑器来点样式,我要开始画了喔~~-->`,// 画灯笼的大肚子`<!-- 第③步,先画灯笼的大肚子结构 --><div class="lantern-container"><!-- htmlStep1 --><!-- 大红灯笼区域 --><div class="lantern-light"><!-- htmlStep2 --></div></div>`,// 提着灯笼的线`<!-- 第④步,灯笼顶部是有根线的 --><div class="lantern-top-line"></div>`,`<!-- 第⑤步,给灯笼加两个盖子 --><div class="lantern-hat-top"></div><div class="lantern-hat-bottom"></div><!-- htmlStep3 -->`,`<!-- 第⑥步,感觉灯笼快要成了,再给他加上四根线吧 --><div class="lantern-line-out"><div class="lantern-line-innner"><!-- htmlStep5 --></div></div><!-- htmlStep4 -->`,`<!-- 第⑦步,灯笼是不是还有底部的小尾巴呀 --><div class="lantern-rope-top"><div class="lantern-rope-middle"></div><div class="lantern-rope-bottom"></div></div>`,`<!-- 第⑧步,最后当然少不了送给大家的福啦 --><div class="lantern-fu">福</div>`]
4.2 css配置
export default [// 0. 添加基本样式`/* 首先给所有元素加上过渡效果 */* {transition: all .3s;-webkit-transition: all .3s;}/* 白色背景太单调了,我们来点背景 */html {color: rgb(222,222,222);background: rgb(0,43,54);}/* 代码高亮 */.token.selector{color: rgb(133,153,0);}.token.property{color: rgb(187,137,0);}.token.punctuation{color: yellow;}.token.function{color: rgb(42,161,152);}`,// 1.创建编辑器本身的样式`/* 我们需要做一个铺满全屏的容器 */.container{width: 100%;height: 100vh;display: flex;justify-content: space-between;align-items: center;}/* 代码编辑区域50%宽度,留一些空间给预览区域 */.edit{width: 50%;height: 100%;background-color: #1d1f20;display: flex;flex-direction: column;justify-content: space-between;}.html-edit, .css-edit{flex: 1;overflow: scroll;padding: 10px;}.html-edit{border-bottom: 5px solid #2b2e2f;}/* 预览区域有50%的空间 */.preview{flex: 1;height: 100%;background-color: #2f1f47;}.preview-html{display: flex;align-items: center;justify-content: center;height: 100%;}/* 好啦~ 你应该看到一个编辑器的基本感觉了,我们要开始画灯笼咯 */`,// 2`/* 给灯笼的大肚子整样式 */.lantern-container {position: relative;}.lantern-light {position: relative;width: 120px;height: 90px;background-color: #ff0844;border-radius: 50%;box-shadow: -5px 5px 100px 4px #fa6c00;animation: wobble 2.5s infinite ease-in-out;transform-style: preserve-3d;}/* 让他动起来吧 */@keyframes wobble {0% {transform: rotate(-6deg);}50% {transform: rotate(6deg);}100% {transform: rotate(-6deg);}}`,// 3`/* 顶部的灯笼线 */.lantern-top-line {width: 4px;height: 50px;background-color: #d1bb73;position: absolute;left: 50%;transform: translateX(-50%);top: -20px;border-radius: 2px 2px 0 0;}`,// 4`/* 灯笼顶部、底部盖子样式 */.lantern-hat-top,.lantern-hat-bottom {content: "";position: absolute;width: 60px;height: 12px;background-color: #ffa500;left: 50%;transform: translateX(-50%);}/* 顶部位置 */.lantern-hat-top {top: -8px;border-radius: 6px 6px 0 0;}/* 底部位置 */.lantern-hat-bottom {bottom: -8px;border-radius: 0 0 6px 6px;}`,// 5`/* 灯笼中间的线条 */.lantern-line-out,.lantern-line-innner {height: 90px;border-radius: 50%;border: 2px solid #ffa500;background-color: rgba(216, 0, 15, 0.1);}/* 线条外层 */.lantern-line-out {width: 100px;margin: 12px 8px 8px 10px;}/* 线条内层 */.lantern-line-innner {margin: -2px 8px 8px 26px;width: 45px;display: flex;align-items: center;justify-content: center;}`,// 6`/* 灯笼底部线条 */.lantern-rope-top {width: 6px;height: 18px;background-color: #ffa500;border-radius: 0 0 5px 5px;position: relative;margin: -5px 0 0 60px;/* 让灯穗也有一个动画效果 */animation: wobble 2.5s infinite ease-in-out;}.lantern-rope-middle,.lantern-rope-bottom {position: absolute;width: 10px;left: -2px;}.lantern-rope-middle {border-radius: 50%;top: 14px;height: 10px;background-color: #dc8f03;z-index: 2;}.lantern-rope-bottom {background-color: #ffa500;border-bottom-left-radius: 5px;height: 35px;top: 18px;z-index: 1;}`,// 7`/* 福样式 */.lantern-fu {font-size: 30px;font-weight: bold;color: #ffa500;}`]
整体流程
实现原理和整个过程所需的知识点,通过简要解析相信你已经明白了,接下来我们要做的事情就是把这些知识点组合在一起,完成自动画画。
import Prism from 'prismjs'import htmls from './config/htmls'import styles from './config/styles'import { isMobile, delay } from '../../common/utils'export default {name: 'newYear2022',data () {return {// html代码展示片段htmlEditPre: '',htmlEditPreSource: '',// css代码展示片段styleEditPre: '',// 实际起作用的csspreviewStylesSource: '',previewStyles: '',// 预览的htmlpreviewHtmls: '',}},computed: {containerClasses () {// 做一个简单的适配return ['container',isMobile() ? 'container-mobile' : '']}},async mounted () {// 1. 打招呼await this.doHtmlStep(0)// 2. 说明主旨await this.doHtmlStep(1)await delay(500)// 3. 第一步声明await this.doHtmlStep(2)await delay(500)// 4. 创建写代码的编辑器await this.doHtmlStep(3)await delay(500)// 5. 准备写编辑器的样式await this.doHtmlStep(4)await delay(500)// 6. 基本样式await this.doStyleStep(0)await delay(500)// 7. 编辑器的样式await this.doStyleStep(1)await delay(500)// 8. 画灯笼的大肚子htmlawait Promise.all([this.doHtmlStep(5, 0),this.doEffectHtmlsStep(5, 0),])await delay(500)// 8. 画灯笼的大肚子cssawait this.doStyleStep(2)await delay(500)// 9. 提着灯笼的线htmlawait Promise.all([this.doHtmlStep(6, 1),this.doEffectHtmlsStep(6, 1),])await delay(500)// 10. 提着灯笼的线cssawait this.doStyleStep(3)await delay(500)// 11. 给灯笼加两个盖子htmlawait Promise.all([this.doHtmlStep(7, 2),this.doEffectHtmlsStep(7, 2),])await delay(500)// 12. 给灯笼加两个盖子cssawait this.doStyleStep(4)await delay(500)// 13. 感觉灯笼快要成了,再给他加上四根线吧htmlawait Promise.all([this.doHtmlStep(8, 3),this.doEffectHtmlsStep(8, 3),])await delay(500)// 14. 感觉灯笼快要成了,再给他加上四根线吧cssawait this.doStyleStep(5)await delay(500)// 15. 灯笼是不是还有底部的小尾巴呀htmlawait Promise.all([this.doHtmlStep(9, 4),this.doEffectHtmlsStep(9, 4),])await delay(500)// 16. 灯笼是不是还有底部的小尾巴呀cssawait this.doStyleStep(6)await delay(500)// 17. 最后当然少不了送给大家的福啦htmlawait Promise.all([this.doHtmlStep(10, 5),this.doEffectHtmlsStep(10, 5),])await delay(500)// 18. 最后当然少不了送给大家的福啦cssawait this.doStyleStep(7)await delay(500)},methods: {// 渲染cssdoStyleStep (step) {const cssEditRef = this.$refs.cssEditRefreturn new Promise((resolve) => {// 从css配置文件中取出第n步的样式const styleStepConfig = styles[ step ]if (!styleStepConfig) {return}let previewStylesSource = this.previewStylesSourcelet start = 0let timter = setInterval(() => {// 挨个累加let char = styleStepConfig.substring(start, start + 1)previewStylesSource += charif (start >= styleStepConfig.length) {console.log('css结束')clearInterval(timter)resolve(start)} else {this.previewStylesSource = previewStylesSource// 左边编辑器展示给用户看的this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)// 右边预览区域实际起作用的cssthis.previewStyles = `<style>${previewStylesSource}</style>`start += 1// 因为要不断滚动到底部,简单粗暴处理一下document.documentElement.scrollTo({top: 10000,left: 0,})// 因为要不断滚动到底部,简单粗暴处理一下cssEditRef && cssEditRef.scrollTo({top: 100000,left: 0,})}}, 0)})},// 渲染htmldoEffectHtmlsStep (step, insertStepIndex = -1) {// 注意html部分和css部分最大的不同在于后面的步骤是有可能插入到之前的代码中间的,并不是一味地添加到尾部// 所以需要先找到标识,然后插入const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1return new Promise((resolve) => {const htmlStepConfig = htmls[ step ]let previewHtmls = this.previewHtmlsconst index = previewHtmls.indexOf(insertStep)const stepInHtmls = index !== -1let frontHtml = stepInHtmls ? previewHtmls.slice(0, index + insertStep.length) : previewHtmlslet endHtml = stepInHtmls ? previewHtmls.slice(index + insertStep.length) : ''let start = 0let chars = ''let timter = setInterval(() => {let char = htmlStepConfig.substring(start, start + 1)// 累加字段chars += charpreviewHtmls = frontHtml + chars + endHtmlif (start >= htmlStepConfig.length) {console.log('html结束')clearInterval(timter)resolve(start)} else {// 赋值html片段this.previewHtmls = previewHtmlsstart += 1}}, 0)})},// 编辑区域html高亮代码doHtmlStep (step, insertStepIndex = -1) {const htmlEditRef = this.$refs.htmlEditRefconst htmlEditPreRef = this.$refs.htmlEditPreRef// 同上需要找到插入标志const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1return new Promise((resolve) => {const htmlStepConfig = htmls[ step ]let htmlEditPreSource = this.htmlEditPreSourceconst index = htmlEditPreSource.indexOf(insertStep)const stepInHtmls = index !== -1// 按照条件拼接代码let frontHtml = stepInHtmls ? htmlEditPreSource.slice(0, index + insertStep.length) : htmlEditPreSourcelet endHtml = stepInHtmls ? htmlEditPreSource.slice(index + insertStep.length) : ''let start = 0let chars = ''let timter = setInterval(() => {let char = htmlStepConfig.substring(start, start + 1)chars += charhtmlEditPreSource = frontHtml + chars + endHtmlif (start >= htmlStepConfig.length) {console.log('html结束')clearInterval(timter)resolve(start)} else {this.htmlEditPreSource = htmlEditPreSource// 代码高亮处理this.htmlEditPre = Prism.highlight(htmlEditPreSource, Prism.languages.html)start += 1if (insertStep !== -1) {// 当要插入到中间时,滚动条滚动到中间,方便看代码htmlEditRef && htmlEditRef.scrollTo({top: (htmlEditPreRef.offsetHeight - htmlEditRef.offsetHeight) / 2,left: 1000,})} else {// 否则直接滚动到底部htmlEditRef && htmlEditRef.scrollTo({top: 100000,left: 0,})}}}, 0)})},}}
