画皮卡丘、小黄人、哆啦A梦、瑞克和莫提、琦玉,任何CSS画出的非图片图形,Canvas SVG


目录 §

  • 1. 回顾技术栈 §
  • 2. 选择模仿目标 §
  • 3. 创建和使用 §
  • 4. 图形制作过程 §
  • 6. 动态显示代码 §
  • 7. 总结 §

回顾技术栈

CSS3 + ES6综合应用

  • 必备知识:CSS3布局和定位、CSS3 transform、JS DOM操作

浏览器 JS 的能力

  • 操作DOM
  • 操作AJAX

目前的目标

  • 用jQuery操作DOM(之后改成Vue React)
  • 用axios操作AJAX

一个项目

  • 60%的时间在写CSS 布局
  • 20%的时间在写JS
  • 20%的时间在想错在哪

选择模仿目标

勿原创,不是设计师

  • 模仿别人的界面,代码自己写
  • codepen.io

项目介绍

特点

  • 移动端支持
  • CSS绘制任意图形,并同步显示代码
  • 界面简洁友好
  • 使用指南

技术栈

  • SCSS
  • parcel实时预览
  • @media媒体查询
  • 表驱动编程-自动绑定事件
  • Git & GitHub
  • 短链处理
  • 二维码链接

后续更新

  • vue版本
  • react版本

思路

  • 实现手机端
    • 写 HTML
    • 写 SCSS
    • 写 JS(事件监听、DOM 操作)
  • 实现PC端
    • 加 媒体查询 CSS
    • 写 JS(单独处理PC端逻辑)
  • 发布到 GitHub Gitee

创建和使用

开发

1
2
yarn global add parcel-bundler
parcel src/index.html

build 命令

1
yarn build

修改 package.json

1
2
3
4
5
6
7
8
{
  "devDependencies": {
    "sass": "^1.26.9"
  },
  "scripts": {
    "build": "rm -rf dist && parcel build src/index.html --no-minify --public-url ./"
  }
}

取色工具 Snipaste


制作过程

index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover">
  <title>PiKaChu</title>
  <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/modern-normalize/0.6.0/modern-normalize.min.css">
  <link rel="stylesheet" href="style.scss">
</head>
<body>
  <div class="face">
    <div class="eye"></div>
    <div class="eye eye-right"></div>
    <div class="noseTip"></div>
    <div class="cheek">
      <div class="cheek-left"></div>
      <div class="cheek-right"></div>
    </div>
    <div class="mouth">
      <div class="lip">
        <div class="lip-left"></div>
        <div class="lip-right"></div>
      </div>
      <div class="jaw">
        <div class="lowerJaw"></div>
      </div>
    </div>
  </div>
  <script src="main.js"></script>
</body>

基础SCSS样式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
*,
*::before,
*::after {
  box-sizing: border-box;
}
body {
  width: 100vw;
  min-height: 100vh;
  background-color: #ffe035;
}

@mixin widthHeight($width: 0px, $height: 0px) {
  width: $width;
  height: $height;
}

@function boxSize($width: 0px, $height: false) {
  @if $height==false {
    $height: $width;
  }

  // @warn $width;
  // @warn $height;
  // while invoke need `...`
  @return ($width, $height);
}

%pseudo {
  content: '';
  display: block;
}

制作鼻子和眼睛样式

以页面宽度中线为基准,画鼻子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* ... */
@mixin widthHeight($width: 0px, $height: 0px) {
  width: $width;
  height: $height;
}
%pseudo {
  content: '';
  display: block;
}
/* ... */
.face {
  position: relative;

  .noseTip {
    z-index: 2;
    position: absolute;
    left: 50%;
    top: 310px;
    margin-left: -10px;

    border: 10px solid #ffe035;
    border-top-color: black;
    border-bottom-width: 0px; // border-bottom: none;

    // Nasal bridge
    &::before {
      @extend %pseudo;
      @include widthHeight(20px, 10px);

      position: absolute;
      top: -16px;
      left: -10px;

      background-color: black;
      border-radius: 10px / 5px;
      box-shadow: 0px 0px 1px 0px black;
    }
  }
  .eye{
  //...
  }
  .cheek{
  //...
  }
  .mouth{
  //...
  }
}
  • 注意当写border 不同方向上数值的时候, 指的是那个方向上的厚度
  • CSS用border模拟图形(三角 + 圆/椭圆)
  • tips提示条
  • 各同级元素的单位保持一致(px / em ),和百分比 / vh vm 混用注意
  • 使用绝对定位时,没有普通文档流元素,body 元素高度设为{min-height: 100vh;}
  • 对具体某个元素使用绝对定位时,父元素设为相对定位
  • 平滑椭圆边线圆弧 border-bottom-left-radius: 60px 30px;
  • transform属性会相互覆盖,必须一次性写出transform: translate(110px, -30px) scaleX(-1) rotate(-20deg);

制作眼睛

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  .eye {
    border: 2px solid #000;
    @include widthHeight(boxSize(64px)...);
    background: #2e2e2e;
    border-radius: 100%;

    position: absolute;
    top: 270px;
    left: 50%;
    margin-left: -32px;
    transform: translateX(-150px);

    &-right {
      transform: translateX(150px);
      ;
    }

    // pupil
    &::before {
      @extend %pseudo;
      @include widthHeight(boxSize(30px)...);
      border-radius: 100%;
      background-color: whitesmoke;
      position: relative;
      left: 5px;
      top: 5px;
    }
  }

制作上嘴唇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  .mouth {
    @include widthHeight(220px, 200px);
    position: absolute;
    left: 50%;
    top: 350px;
    margin-left: -110px;

    .lip {
      z-index: 1;
      position: relative;

      %lips {
        background-color: #ffe035;
        @include widthHeight(110px, 30px);

        border: 4px solid black;
        border-top-width: 0px;
        border-right-width: 0px;
        border-top-color: #ffe035;
        border-top-right-radius: 60px 30px;

        // mustache hide lip
        &::after {
          @extend %pseudo;

          @include widthHeight(100%, 0.01px);
          border: 2px solid #ffe035;
          outline: 1px solid #ffe035;
          position: absolute;
          top: -3px;
          left: 1px;
        }
      }

      @mixin border($bd-b-l-rad: 60px 30px, $rotate:rotate(-20deg)) {
        border-bottom-left-radius: $bd-b-l-rad;
        transform: $rotate;
      }

      &>.lip-left {
        @extend %lips;
        @include border;
      }

      &>.lip-right {
        @extend %lips;
        @include border(60px 30px, translate(110px, -30px) scaleX(-1) rotate(-20deg));
      }
    }
    .jaw{
        //...
    }
  }

制作下嘴唇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    .jaw {
      @include widthHeight(100%, 220px);

      position: absolute;
      top: 8px;

      // hide lower jaw
      overflow: hidden;
      border-radius: 50% 50% 00/ 25% 25% 0 0;

      .lowerJaw {
        $jaw-width: 180px;
        @include widthHeight($jaw-width, 580px);

        position: absolute;
        bottom: 0;
        left: 50%;
        margin-left: -($jaw-width)/2;

        border: 3px solid #000;
        border-radius: 50% / 50%;
        background-color: #9b000a;

        // hide tongue
        overflow: hidden;

        // tongue
        &::after {
          @extend %pseudo;
          $tongue-width: 200px;
          $tongue-height: 380px;
          @include widthHeight($tongue-width, $tongue-height);
          background-color: #ff485f;

          position: absolute;
          bottom: -$tongue-height/2;
          left: 50%;
          margin-left: -$tongue-width/2;
          border-radius: 100px;
        }
      }
    }

添加动态效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.noseTip{
    /* ... */
    $n: 5deg;
    $step: 33%;

    @keyframes shakeNose {
      0% {
        transform: rotate(0deg);
      }

      @for $i from 1 to 3 {
        #{$i * $step} {
          @if $i % 2==0 {
            transform: rotate(-$n);
          }

          @else {
            transform: rotate($n);
          }
        }
      }

      100% {
        transform: rotate(0);
      }
    }

    &:hover {
      transform-origin: center bottom;
      animation: shakeNose infinite 300ms linear;
    }
}

移动端适配

px -> rem


动态显示代码

简单的测试

蹦字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// <div id="demo"></div>
let n = 1;
const string = `大家好,我是这里是`;
demo.innerHTML = string.substr(0, n);
// console.log(n);
let id = setInterval(() => {
    n += 1;
    // console.log(n + ':' + string.substr(0, n) + '/' + string.length);
    if(n > string.length){
        window.clearInterval(id);
        return
    }
    demo.innerHTML = string.substr(0, n);
    console.log(n);
}, 300);

动态样式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
let n = 1;
// const string = `大家好,我是这里是`;
const string = `
<style>
    body {
    background-color: red;
    }
</style>
`;
demo.innerHTML = string.substr(0, n);
// console.log(n);
let id = setInterval(() => {
    n += 1;
    // console.log(n + ':' + string.substr(0, n) + '/' + string.length);
    if(n > string.length){
        window.clearInterval(id);
        return
    }
    demo.innerHTML = string.substr(0, n);
    console.log(n);
}, 300);

同时以文本和HTML的形式展示style标签

同时输出两种形式,一个用 innerHTML,另一个用innerText

  • innerText,以文本形式展示,但标签不会生效
  • 同时用innerHTML写入标签
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// <div id="demo"></div>
// <div id="demo2"></div>
const string = `
<style>
    body {
    background-color: red;
    }
</style>
`;
let n = 1;
demo.innerText = string.substr(0, n);
demo2.innerHTML = string.substr(0, n);
// console.log(n);
let id = setInterval(() => {
    n += 1;
    // console.log(n + ':' + string.substr(0, n) + '/' + string.length);
    if(n > string.length){
        window.clearInterval(id);
        return
    }
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = string.substr(0, n);
    console.log(n);
}, 100);

先写好HTML到 <div id="html"></div>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<div id="html">
    <div class="face">
        <div class="eye"></div>
        <div class="eye eye-right"></div>
        <div class="nose-tip "></div>
        <div class="cheek">
            <div class="cheek-left"></div>
            <div class="cheek-right"></div>
        </div>
        <div class="mouth">
            <div class="lip">
                <div class="lip-left"></div>
                <div class="lip-right"></div>
            </div>
            <div class="jaw">
                <div class="lower-jaw"></div>
            </div>
        </div>
    </div>
</div>

再动态显示 CSS 代码 动态加载CSS

  • 分屏固定显示代码文本和效果
  • 隐藏展示样式#demo2的代码,不影响效果
  • 写自动滚动到底部的代码,每写一句,就往下拉滚动条
  • 滚动距离为 可滚动高度 demo.scrollHeight,再减去滚动条本身的高度
  • 下拉的距离等于滚动的高度demo.scrollTop = demo.scrollHeight
  • 对JS来说,有没有滚动条,都可以滚动页面,只是用户不能操作滚动,设置overflow-y: auto
  • 或者用另一种隐藏滚动条的方法(搜CSS 隐藏滚动条) #demo::-webkit-scrollbar{display: none;}只是只能效果上看不见,仍可以用滚轮滚动页面
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<style>
    #demo2{
        display: none;
    }
    #demo {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 30vh;
        border: 1px solid red;
        overflow: scroll;
    }
    #html {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 80vh;
    }
</style>

优化

  • 先写好style标签,将#demo2的div标签改为style标签
  • 去除会影响页面整体样式的代码,写到别处,或者用BootCDN 的normalize.css
  • 只显示对页面有直观效果的CSS代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<body>
<style id="demo2"></style>
<div id="demo"></div>
<style>
    #demo2{
        display: none;
    }
    #demo {
        background-color: whitesmoke;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 30vh;
        border: 1px solid red;
        overflow-y: auto;
    }
    #demo::-webkit-scrollbar{
        display: none;
    }
    #html {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 80vh;
    }
</style>
<div id="html">
    <div class="face">
        <div class="eye"></div>
        <div class="eye eye-right"></div>
        <div class="nose-tip "></div>
        <div class="cheek">
            <div class="cheek-left"></div>
            <div class="cheek-right"></div>
        </div>
        <div class="mouth">
            <div class="lip">
                <div class="lip-left"></div>
                <div class="lip-right"></div>
            </div>
            <div class="jaw">
                <div class="lower-jaw"></div>
            </div>
        </div>
    </div>
</div>
<script src="test.js"></script>
</body>

抽出默认样式代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const stringDefault = `
.face .nose-tip::after, .face .nose-tip::before, .face .eye::before, .face .mouth .lip > .lip-left::after, .face .mouth .lip > .lip-right::after, .face .mouth .jaw .lower-jaw::after {
  content: '';
  display: block;
}

.face .cheek {
  z-index: 2;
}
`;

抽出动画代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const stringAnimation = `
@keyframes shakeNose {
  0% {
    transform: rotate(0deg);
  }
  33% {
    transform: rotate(5deg);
  }
  66% {
    transform: rotate(-5deg);
  }
  100% {
    transform: rotate(0);
  }
}

.face .nose-tip:hover {
  animation: shakeNose infinite 300ms linear;
  transform-origin: center bottom;
}
`;

抽出string样式代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
const string = `
body {
  background-color: #ffe035;
  min-height: 100vh;
  width: 100vw;
}

.face {
  min-height: 35vh;
  height: 300px;
  left: 50%;
  position: absolute;
  top: 50%;
  transform: translateY(-50%) translateX(-50%);
  width: 500px;
}

.face .nose-tip {
  border: 10px solid #ffe035;
  border-bottom-width: 1px;
  border-left-color: #ffe035;
  border-right-color: #ffe035;
  border-top-color: #000;
  left: 50%;
  margin-left: -10px;
  position: absolute;
  top: 100px;
  z-index: 2;
}

.face .nose-tip::after {
  height: 22px;
  width: 22px;
  border: 3px solid #ffe035;
  border-radius: 0 0 12px 12px / 0 0 20px 20px;
  border-top-color: transparent;
  left: -11px;
  overflow: hidden;
  position: absolute;
  top: -11px;
  z-index: 1;
}

.face .nose-tip::before {
  height: 10px;
  width: 20px;
  background-color: #000;
  border-radius: 10px / 5px;
  left: -10px;
  position: absolute;
  top: -16px;
}

.face .eye {
  height: 64px;
  width: 64px;
  background: #2e2e2e;
  border: 2px solid #000;
  border-radius: 100%;
  left: 50%;
  margin-left: -32px;
  position: absolute;
  top: 70px;
  transform: translateX(-150px);
}

.face .eye-right {
  transform: translateX(150px);
}

.face .eye::before {
  height: 30px;
  width: 30px;
  background-color: #f5f5f5;
  border-radius: 50%;
  left: 5px;
  position: relative;
  top: 5px;
}

.face .cheek .cheek-left, .face .cheek .cheek-right {
  height: 86px;
  width: 86px;
  background-color: #f00;
  border: 2px solid #000;
  border-radius: 50%;
  left: 50%;
  margin-left: -43px;
  position: absolute;
  top: 180px;
}

.face .cheek-left {
  transform: translateX(-180px);
}

.face .cheek-right {
  transform: translateX(180px);
}

.face .mouth {
  height: 200px;
  width: 220px;
  left: 50%;
  margin-left: -110px;
  position: absolute;
  top: 135px;
}

.face .mouth .lip > .lip-left, .face .mouth .lip > .lip-right {
  height: 30px;
  width: 110px;
  background-color: #ffe035;
  border: 4px solid #000;
  border-top-color: #ffe035;
  border-top-right-radius: 60px 30px;
  border-top-width: 0;
  border-right-width: 0;
}

.face .mouth .lip > .lip-left::after, .face .mouth .lip > .lip-right::after {
  height: 0.01px;
  width: 100%;
  border: 2px solid #ffe035;
  left: 1px;
  outline: 1px solid #ffe035;
  position: absolute;
  top: -3px;
}

.face .mouth .lip {
  position: relative;
  z-index: 1;
}

.face .mouth .lip > .lip-left {
  border-bottom-left-radius: 60px 30px;
  transform: rotate(-20deg);
}

.face .mouth .lip > .lip-right {
  border-bottom-left-radius: 60px 30px;
  transform: translate(110px, -30px) scaleX(-1) rotate(-20deg);
}

.face .mouth .jaw {
  height: 220px;
  width: 100%;
  border-radius: 50% 50% 0 0 / 25% 25% 0 0;
  overflow: hidden;
  position: absolute;
  top: 8px;
}

.face .mouth .jaw .lower-jaw {
  height: 580px;
  width: 180px;
  background-color: #9b000a;
  border: 3px solid #000;
  border-radius: 50% / 50%;
  bottom: 0;
  left: 50%;
  margin-left: -90px;
  overflow: hidden;
  position: absolute;
}

.face .mouth .jaw .lower-jaw::after {
  height: 380px;
  width: 200px;
  background-color: #ff485f;
  border-radius: 100px;
  bottom: -190px;
  left: 50%;
  margin-left: -100px;
  position: absolute;
}
`;

模块化导出JS独立代码

将不相关的独立代码写到单独的文件中,导出,在需要的时候导入

1
2
3
4
// 模块化
import stringDefault from './stringDefault.js';
import string from './string.js';
import stringAnimation from './stringAnimation.js';

stringDefault.js

1
2
3
const stringDefault = `
`
export default stringDefault;

string.js

1
2
3
const string = `
`
export default string;

stringAnimation.js

1
2
3
const stringAnimation = `
`
export default stringAnimation;

集中导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模块化
import stringDefault from './stringDefault.js';
import string from './string.js';
import stringAnimation from './stringAnimation.js';

let n = 1;
demo.innerText = string.substr(0, n);
demo2.innerHTML = stringDefault + string.substr(0, n);
// console.log(n);
let id = setInterval(() => {
    n += 1;
    // console.log(n + ':' + string.substr(0, n) + '/' + string.length);
    if(n > string.length){
        window.clearInterval(id);
        demo2.innerHTML += stringAnimation;
        return
    }
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    // console.log(n);
    demo.scrollTop = demo.scrollHeight;
}, 0);
  • 导出的可以使变量、函数等
  • 重构代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const run = () => {
    n += 1;
    if(n > string.length){
        window.clearInterval(id);
        demo2.innerHTML += stringAnimation;
        return
    }
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    demo.scrollTop = demo.scrollHeight;
}

添加暂停、变速按钮

1
2
3
4
5
6
7
8
<div id="demo"></div>
<div id="buttons">
    <button id="btnPause">暂停</button>
    <button id="btnPlay">播放</button>
    <button id="btnSlow">快速</button>
    <button id="btnNormal">超快速</button>
    <button id="btnFast">最快速</button>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#buttons {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 10;
    display: flex;
    flex-direction: column;
    margin-top: 20px;
    margin-right: 20px;
}

#buttons>button {
    margin-bottom: 10px;
}

@media ( min-width: 320px) and (max-width: 720px) {
    #buttons>button {
        padding: 4px 8px;
        font-size: 20px;
    }
}

暂停就是取消计时器,播放就是再设置一个计时器,变速就是更改间隔秒数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 暂停:取消计时器
btnPause.onclick = () => {
    window.clearInterval(id);
}
// 播放:再设置一个计时器
btnPlay.onclick = () => {
    id = setInterval( ()=>{
        run();
    }, intervalTime);
}
// 变速:更改间隔秒数
let intervalTime = 80;
btnSlow.onclick = () =>{
    window.clearInterval(id);
    let intervalTime = 80;
    id = setInterval(run, intervalTime);
};

重复即丑,重构JS代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
btnSlow.onclick = () =>{
    window.clearInterval(id);
    let intervalTime = 80;
    id = setInterval(run, intervalTime);
};
btnNormal.onclick = () => {
    window.clearInterval(id);
    let intervalTime = 16;
    id = setInterval(run, intervalTime);
};
btnFast.onclick = () => {
    window.clearInterval(id);
    let intervalTime = 0;
    id = setInterval(run, intervalTime);
};

按作用抽象成函数,重构后的代码,主逻辑清晰

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// const run = () => {...}
// const play = () => {...}
// const pause = () => {...}

const run = () => {
    n += 1;
    if(n > string.length){
        window.clearInterval(id);
        demo2.innerHTML += stringAnimation;
        return
    }
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    demo.scrollTop = demo.scrollHeight;
}
const play = () => {
    setInterval(run, intervalTime);
}
const pause = () => {
    window.clearInterval(id);
}
let id = play();

把相似的代码封装成一个函数,取个名字,调用它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
btnPause.onclick = () => {
    pause();
}
btnPlay.onclick = () => {
    id = play();
}
btnSlow.onclick = () =>{
    pause();
    let intervalTime = 80;
    id = play();
};
btnNormal.onclick = () => {
    pause();
    let intervalTime = 16;
    id = play();
};
btnFast.onclick = () => {
    pause();
    let intervalTime = 0;
    id = play();
};

抽象事件函数,再重构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// let intervalTime = 80;
// const run = () => {...}
// const play = () => {...}
// const pause = () => {...}
// const slow = () => {...}
// const normal = () => {...}
// const fast = () => {...}
const slow = () => {
    pause();
    intervalTime = 80;
    id = play();
}
const normal = () => {
    pause();
    intervalTime = 16;
    id = play();
}
const fast = () => {
    pause();
    intervalTime = 0;
    id = play();
}
// ...

btnSlow.onclick = slow;
btnNormal.onclick = normal;
btnFast.onclick = fast;

使用正规的语法,获取元素

1
2
3
4
5
6
7
8
const demo = document.querySelector('#demo');
const demo2 = document.querySelector('#demo2');
/* 以下之后可以省略到属性 eventBind 里 document.querySelector(key).onclick = player[value]; */
const btnPause = document.querySelector('#btnPause');
const btnPlay = document.querySelector('#btnPlay');
const btnSlow = document.querySelector('#btnSlow');
const btnNormal = document.querySelector('#btnNormal');
const btnFast = document.querySelector('#btnFast');

用面向对象,再重构

  • 先声明const player = {...},后使用let id = player.play();
  • player.run
  • player.pause
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//init
// 先声明 n, 再使用 n
let n = 1;
demo.innerText = string.substr(0, n);
demo2.innerHTML = stringDefault + string.substr(0, n);

// let intervalTime = 80;
// const run = () => {...}
// const play = () => {...}
// const pause = () => {...}
// let id = play();
// const slow = () => {...}
// const normal = () => {...}
// const fast = () => {...}
// btnPause.onclick = pause;
// btnPlay.onclick = () => {...}
// btnSlow.onclick = slow;
// btnNormal.onclick = normal;
// btnFast.onclick = fast;

// init
let intervalTime = 80;
let n = 1;

const player = {
    run:() => {
        n += 1;
        if(n > string.length){
            window.clearInterval(id);
            demo2.innerHTML += stringAnimation;
            return
        }
        demo.innerText = string.substr(0, n);
        demo2.innerHTML = stringDefault + string.substr(0, n);
        demo.scrollTop = demo.scrollHeight;
    },
    play:() => {
        return setInterval(player.run, intervalTime);
    },
    pause: () => {
        window.clearInterval(id);
    },
    slow: () => {
        player.pause();
        intervalTime = 80;
        id = player.play();
    },
    normal: () => {
        player.pause();
        intervalTime = 16;
        id = player.play();
    },
    fast: () => {
        player.pause();
        intervalTime = 0;
        id = player.play();
    }
};

let id = player.play();

btnPause.onclick = player.pause;
btnPlay.onclick = () => {
    id = player.play();
}
btnSlow.onclick = player.slow;
btnNormal.onclick = player.normal;
btnFast.onclick = player.fast;

初始化方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ...
let n = 1;
let intervalTime = 80;
let id; // id初始化放外面
const player = {
    init: () => {
        demo.innerText = string.substr(0, n);
        demo2.innerHTML = stringDefault + string.substr(0, n);
        id = player.play();
    },
    // ...
}
player.init();

内置功能 id = player.play();player.play

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const player = {    
    //...
    play:() => {
            player.pause();
            //return setInterval(player.run, intervalTime);
            id = setInterval(player.run, intervalTime);
        },
    slow: () => {
        player.pause();
        intervalTime = 80;
        // id = player.play();
        player.play();
    },
    normal: () => {
        player.pause();
        intervalTime = 16;
        // id = player.play();
        player.play();
    },
    fast: () => {
        player.pause();
        intervalTime = 0;
        // id = player.play();
        player.play();
    },
    //...
}
    //...
    btnPlay.onclick = player.play();

将绑定事件放入初始化函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const player = {
    init: () => {
        demo.innerText = string.substr(0, n);
        demo2.innerHTML = stringDefault + string.substr(0, n);
        player.play();
        player.eventBind();
    },
    eventBind: () => {
        console.log("绑定了事件");
        btnPause.onclick = player.pause;
        btnPlay.onclick = player.play;
        btnSlow.onclick = player.slow;
        btnNormal.onclick = player.normal;
        btnFast.onclick = player.fast;
    },
    // ...
}

表驱动编程:哈希Map 避免重复代码声明事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const player = {
    init: () => {
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    player.play();
    player.eventBind(); // `player.eventBind()`是`player.init()`执行后才执行的;再去绑定事件是可以的
    },
    eventBind: () => {
     // `player.eventBind()`是`player.init()`执行后才执行的;再去绑定事件是可以的
        const events = {
            '#btnPause': player.pause,
            '#btnPlay': player.play,
            '#btnSlow': player.slow,
            '#btnNormal': player.normal,
            '#btnFast': player.fast
        };
        for(let key in events) {
            document.querySelector(key).onclick = events[key];
        }
    },
    //...
}

将hashTable放到声明中去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const player = {
    init: () => {
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    player.play();
    player.eventBind();
    },
    // 在声明player时用使用player,报错 player 是 undefined
    events : {
        '#btnPause': player.pause, // 报错 在定义对象时调用此对象
        '#btnPlay': player.play, // 报错 在定义对象时调用此对象
        '#btnSlow': player.slow, // 报错 在定义对象时调用此对象
        '#btnNormal': player.normal, // 报错 在定义对象时调用此对象
        '#btnFast': player.fast // 报错 在定义对象时调用此对象
    },
    eventBind: () => {
        for(let key in events) {
            document.querySelector(key).onclick = events[key];
        }
    },
    //...
}
  • 哈希Map,读取字符串
  • 不能在定义对象时调用此对象
  • 区别于函数,函数可以延迟定义
1
2
3
4
5
let a = {
    xxx: a; // a 未定义
}
a
// {xxx: undefined}

防御型编程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const player = {
    init: () => {
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    player.play();
    player.eventBind();
    },
    events : {
        '#btnPause': 'pause',
        '#btnPlay': 'play',
        '#btnSlow': 'slow',
        '#btnNormal': 'normal',
        '#btnFast': 'fast'
    },
    eventBind: () => {
        for(let key in player.events) {
        // 沙雕
        Object.prototype.地雷 = 1;
        console.log('key');
        console.log(key); // ...#btn... x
        const value = player.events[key]; // pause /play / slow /...
        document.querySelector(key).onclick = player[value];
        }
    },
    //...
}
  • player.events.toString player.events.valueOf等原型链上继承的方法虽然默认是不可枚举
  • 但不排除对象的原型链被沙雕加了可遍历的属性Object.prototype.x = 1,然而这是不希望被遍历到的console.log('key'+ key)
  • for in 遍历对象时会遍历到继承的属性,必须判断排除
  • hasOwnProperty判断
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const player = {
    init: () => {
    demo.innerText = string.substr(0, n);
    demo2.innerHTML = stringDefault + string.substr(0, n);
    player.play();
    player.eventBind();
    },
    events : {
        '#btnPause': 'pause',
        '#btnPlay': 'play',
        '#btnSlow': 'slow',
        '#btnNormal': 'normal',
        '#btnFast': 'fast'
    },
    eventBind: () => {
        for(let key in player.events) {
            // 防御型编程
            if(player.events.hasOwnProperty(key)){
                const value = player.events[key]; // pause /play / slow /...
                document.querySelector(key).onclick = player[value]; // 用不同的变量去作为对象属性名
            }
        }
    },
    //...
}
  • 这就属于防御型编程

另一种方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const player = {
    // ...
    eventBind: () => {
        /*
        for(let key in player.events) {
            // 防御型编程
            /!* 避免取到原型链上的属性 *!/
            if(player.events.hasOwnProperty(key)){
                const value = player.events[key] // pause /play / slow /...
                document.querySelector(key).onclick = player[value]
            }
        }
        */
        // 防御型编程
        Object.getOwnPropertyNames(player.events).forEach(
            (key) => {
                /* 避免取到原型链上的属性 */
                const value = player.events[key] // pause /play / slow /...
                // console.log(player.events);
                // console.log(player.events[key]);
                // console.log(typeof player.events[key]);
                document.querySelector(key).onclick = player[value]
            });
    // ...
}

目前代码的结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const demo = document.querySelector('#demo');
const demo2 = document.querySelector('#demo2');
let n = 1;
let intervalTime = 80;
let id;

const player = {
    init: () => { /* ... */ },
    events : { /* ... */ },
    eventBind: () => { /* ... */ },
    pause: () => { /* ... */ },
    play:() => { /* ... */ },
    slow: () => { /* ... */ },
    normal: () => { /* ... */ },
    fast: () => { /* ... */ }
};
player.init();

player对象声明和初始化以及模块化的外代码全干掉

  • n: 1,放到player里 -> player.n
  • intervalTime: 80,放到player里 -> player.intervalTime
  • id: undefined放到player里 -> player.id
  • ui放两个页面元素,表示UI界面 player.ui.demo player.ui.demo2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 模块化
import stringDefault from './stringDefault.js'
import string from './string.js'
import stringAnimation from './stringAnimation.js'

const player = {
    id: undefined,
    n: 1,
    intervalTime: 80,
    ui: {
        demo: document.querySelector('#demo'),
        demo2: document.querySelector('#demo2')
    },
    init: () => {
        player.ui.demo.innerText = string.substr(0, player.n)
        player.ui.demo2.innerHTML = stringDefault + string.substr(0, player.n)
    player.play()
    player.eventBind()
    },
    events : {
        '#btnPause': 'pause',
        '#btnPlay': 'play',
        '#btnSlow': 'slow',
        '#btnNormal': 'normal',
        '#btnFast': 'fast'
    },
    eventBind: () => {
    /* 避免取到原型链上的属性 */
    Object.getOwnPropertyNames(player.events).forEach(
        (key) => {
            const value = player.events[key] // pause /play / slow /...
            document.querySelector(key).onclick = player[value]
        })
    },
    pause: () => {
        window.clearInterval(player.id)
    },
    run:() => {
        player.n += 1
        if(player.n > string.length){
            player.pause()
            player.ui.demo2.innerHTML += stringAnimation
            return
        }
        player.ui.demo.innerText = string.substr(0, player.n)
        player.ui.demo2.innerHTML = stringDefault + string.substr(0, player.n)
        player.ui.demo.scrollTop = player.ui.demo.scrollHeight
    },
    play:() => {
        player.pause()
        player.id = setInterval(player.run, player.intervalTime)
    },
    slow: () => {
        player.pause()
        player.intervalTime = 80
        player.play()
    },
    normal: () => {
        player.pause()
        player.intervalTime = 16
        player.play()
    },
    fast: () => {
        player.pause()
        player.intervalTime = 0
        player.play()
    }
}
player.init()

美化按钮

1

1

1


总结

VSCode V.S. WebStorm

xxx.log -> console.log(xxx)


部署到 GitHub Pages

演示中,不能把<style></style>写到<body></body>里,会被parcel删掉

  • 单独保存样式到一个style.css里,就好了

.gitignore

1
2
3
4
5
/node_modules/
/.cache/
/.idea
/.vscode/
/.sass-cache/

部署方式

  • 每次改完代码,必须运行这一行,才能正确的请求 JS 和 CSS:
1
parcel build src/index.html --public-url .

yarn build 一键发布

再次build的时,只需用yarn init -y 创建package.json

  • package.json中加一段脚本
1
2
3
"scripts": {
"build":"rm -rf dist && parcel build src/index.html --no-minify --public-url ./"
},

再次 yarn build



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名