先睹为快

如下图,代码在本人一行一行写程序,逐步画出一个喜气灯笼的模样(PC挪动端都反对噢),想不想晓得是它怎么实现的呢?和胖头鱼一起来探索一番吧O(∩_∩)O~

你也能够间接点击 用程序主动画了一个灯笼 体验一番,胖头鱼的掘金流动仓库查看源码

原理探索

这个成果就如同一个打字员在一直地录入文字,页面出现动态效果。又如同一个早曾经录制好影片,而咱们只是坐在放映机前观看。

原理自身也非常简单,只有你会一点点前端常识,就能够马上亲手做一个进去。

1. 滚动的代码

定时器字符累加: 置信聪慧的你早曾经猜到屏幕中滚动的htmlcss代码就是通过启动一个定时器,而后将事后筹备好的字符,一直累加到一个pre标签中。

2. 灯笼的布局

动静增加html片段css片段,一张动态网页由htmlcss组成,灯笼能一直地发生变化,背地天然是组成灯笼的htmlcss一直变动的后果。

3. 例子解释

设想一下你要往一张网页每距离0.1秒减少一个字,是不是开个定时器,间断地往body外面塞,就能够啊!没错,做到这一步就实现了原理的第一局部

再设想一下,在往页面外面塞的时候,我还想扭转啊字的字体色彩以及网页背景色彩,那应该怎么做呢,是不是执行上面的代码就能够呢?

.xxx{  color: blue;  background: red; }

没错,只不过更改字体和背景色不是忽然扭转的,而是开个定时器,间断地往style标签中塞入以下代码,这样就实现了原理的第二步,是不是好简略 , 接下来让咱们一步步实现它。

简要解析

1.编辑器布局

工欲善其事,必先利其器。在实现代码本人画画的前提是有个相似编辑器中央给他show,所以会有编辑htmlcss和预览三个区域。

挪动端布局

高低构造布局,下面是htmlcss的编辑区域,上面的灯笼的展现区域

PC端布局

左右构造布局,右边是htmlcss的编辑区域,左边是灯笼的展现区域

模板

<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.代码高亮

示例中的代码高亮是借助prismjspre进行转化解决的,只须要填充你想要高亮的代码,以及抉择高亮的语言就能够实现上述成果。
// 外围代码,只有一行this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)

3. 灯笼布局实现

要实现灯笼一直变动的布局,须要两个货色,一个是灯笼自身的html元素还有就是管制html款式的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: '',      // 理论起作用的css      previewStylesSource: '',      previewStyles: '',      // 预览的html      previewHtmls: '',    }  },  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. 画灯笼的大肚子html    await Promise.all([       this.doHtmlStep(5, 0),       this.doEffectHtmlsStep(5, 0),    ])    await delay(500)    // 8. 画灯笼的大肚子css    await this.doStyleStep(2)    await delay(500)    // 9. 提着灯笼的线html    await Promise.all([       this.doHtmlStep(6, 1),       this.doEffectHtmlsStep(6, 1),    ])    await delay(500)    // 10. 提着灯笼的线css    await this.doStyleStep(3)    await delay(500)    // 11. 给灯笼加两个盖子html    await Promise.all([       this.doHtmlStep(7, 2),       this.doEffectHtmlsStep(7, 2),    ])    await delay(500)    // 12. 给灯笼加两个盖子css    await this.doStyleStep(4)    await delay(500)    // 13. 感觉灯笼快要成了,再给他加上四根线吧html    await Promise.all([       this.doHtmlStep(8, 3),       this.doEffectHtmlsStep(8, 3),    ])    await delay(500)    // 14. 感觉灯笼快要成了,再给他加上四根线吧css    await this.doStyleStep(5)    await delay(500)    // 15. 灯笼是不是还有底部的小尾巴呀html    await Promise.all([       this.doHtmlStep(9, 4),       this.doEffectHtmlsStep(9, 4),    ])    await delay(500)    // 16. 灯笼是不是还有底部的小尾巴呀css    await this.doStyleStep(6)    await delay(500)    // 17. 最初当然少不了送给大家的福啦html    await Promise.all([       this.doHtmlStep(10, 5),       this.doEffectHtmlsStep(10, 5),    ])    await delay(500)    // 18. 最初当然少不了送给大家的福啦css    await this.doStyleStep(7)    await delay(500)  },  methods: {    // 渲染css    doStyleStep (step) {      const cssEditRef = this.$refs.cssEditRef      return new Promise((resolve) => {        // 从css配置文件中取出第n步的款式        const styleStepConfig = styles[ step ]        if (!styleStepConfig) {          return        }        let previewStylesSource = this.previewStylesSource        let start = 0        let timter = setInterval(() => {          // 挨个累加          let char = styleStepConfig.substring(start, start + 1)          previewStylesSource += char          if (start >= styleStepConfig.length) {            console.log('css完结')            clearInterval(timter)            resolve(start)          } else {            this.previewStylesSource = previewStylesSource            // 右边编辑器展现给用户看的            this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)            // 左边预览区域理论起作用的css            this.previewStyles = `              <style>                ${previewStylesSource}              </style>            `            start += 1            // 因为要一直滚动到底部,简略粗犷解决一下            document.documentElement.scrollTo({              top: 10000,              left: 0,            })            // 因为要一直滚动到底部,简略粗犷解决一下            cssEditRef && cssEditRef.scrollTo({              top: 100000,              left: 0,            })          }        }, 0)      })    },    // 渲染html    doEffectHtmlsStep (step, insertStepIndex = -1) {      // 留神html局部和css局部最大的不同在于前面的步骤是有可能插入到之前的代码两头的,并不是一味地增加到尾部      // 所以须要先找到标识,而后插入      const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1      return new Promise((resolve) => {        const htmlStepConfig = htmls[ step ]        let previewHtmls = this.previewHtmls        const index = previewHtmls.indexOf(insertStep)        const stepInHtmls = index !== -1                let frontHtml = stepInHtmls ? previewHtmls.slice(0, index + insertStep.length) : previewHtmls        let endHtml = stepInHtmls ? previewHtmls.slice(index + insertStep.length) : ''                let start = 0        let chars = ''        let timter = setInterval(() => {          let char = htmlStepConfig.substring(start, start + 1)          // 累加字段          chars += char          previewHtmls = frontHtml + chars + endHtml          if (start >= htmlStepConfig.length) {            console.log('html完结')            clearInterval(timter)            resolve(start)          } else {            // 赋值html片段            this.previewHtmls = previewHtmls            start += 1          }        }, 0)      })    },    // 编辑区域html高亮代码    doHtmlStep (step, insertStepIndex = -1) {      const htmlEditRef = this.$refs.htmlEditRef      const htmlEditPreRef = this.$refs.htmlEditPreRef      // 同上须要找到插入标记      const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1      return new Promise((resolve) => {        const htmlStepConfig = htmls[ step ]        let htmlEditPreSource = this.htmlEditPreSource        const index = htmlEditPreSource.indexOf(insertStep)        const stepInHtmls = index !== -1        // 依照条件拼接代码        let frontHtml = stepInHtmls ? htmlEditPreSource.slice(0, index + insertStep.length) : htmlEditPreSource        let endHtml = stepInHtmls ? htmlEditPreSource.slice(index + insertStep.length) : ''                let start = 0        let chars = ''        let timter = setInterval(() => {          let char = htmlStepConfig.substring(start, start + 1)          chars += char          htmlEditPreSource = frontHtml + chars + endHtml          if (start >= htmlStepConfig.length) {            console.log('html完结')            clearInterval(timter)            resolve(start)          } else {            this.htmlEditPreSource = htmlEditPreSource            // 代码高亮解决            this.htmlEditPre = Prism.highlight(htmlEditPreSource, Prism.languages.html)            start += 1            if (insertStep !== -1) {              // 当要插入到两头时,滚动条滚动到两头,不便看代码              htmlEditRef && htmlEditRef.scrollTo({                top: (htmlEditPreRef.offsetHeight - htmlEditRef.offsetHeight) / 2,                left: 1000,              })            } else {              // 否则间接滚动到底部              htmlEditRef && htmlEditRef.scrollTo({                top: 100000,                left: 0,              })            }          }        }, 0)      })    },  }}

结尾

马上就要新年啦!愿大家新年快乐,“码”到胜利。

参考

  1. 过年了~我用CSS画了个灯笼,看着真喜庆
  2. 用原生 js 写一个 "多动症" 的简历