【JavaScript原生教程】实现一个简单的车牌输入法

1,181 阅读12分钟

今天闲来无事,看到了一个做车牌键盘的帖子。闲暇之余,我自己也设计了一个车牌输入键盘,大体如下。

image.png image.png

实现的比较匆忙,没有进行优化,且看。

省份键盘区:

image.png

字母键盘区:

image.png

带数字键盘区:

image.png

设计过程:

为了降低人群的输入车牌的步骤,设计以下几点。

  1. 省、市、新能源输入框的标志;
  2. 当前输入项的高亮
  3. 不同输入框的输入项不同
  4. 支持连续输入
  5. 排除非车牌字母
  6. 采用ABC键盘而非QWERTY键盘,ABC键盘查询字母更高效,对非单词输入有较快的帮助。
  7. 支持全键盘输入,相比传统的一字排列,9宫排列数字键盘缩短查找路径,输入更便捷。

当然,抛开这些不谈,我们对如何实现才是最关心的。实现并未考虑兼容需求,采用px作为基本单位,因此在屏幕较小的情况下存在错位的情况,但是并不妨碍教程内容。

下面就直接上代码:

代码讲解

文章的主要内容还是为了讲解代码的设计、开发、制作,因此并没有过多的追求应用的完成度。以下涉及到的知识点较为丰富,如果您还是初学者,可能需要一番较为深刻的理解后才能明白,如果您已经有所学习,那理解起来也可能有些许挑战,不过我都会逐一分析讲解,从而降低理解难度

1. html的部分

    <div id="app">
        <div class="card license-plate">
            <div class="input" id="province"></div>
            <div class="input" id="region"></div>

            <div class="input num" id="num1"></div>
            <div class="input num" id="num2"></div>
            <div class="input num" id="num3"></div>
            <div class="input num" id="num4"></div>
            <div class="input num" id="num5"></div>
            <div class="input num new-energy" id="new-energy"></div>
        </div>
    </div>
    <div class="keyboards">
        <div class="mergekeyboard kbdpanel" style="display: none;">
            <div class="k-alphabet kbdpanel">

            </div>
            <div class="k-num kbdpanel">

            </div>
        </div>
        <div class="k-pro kbdpanel" style="display: none;">
            <div class="card keyboard">

            </div>
        </div>
    </div>

当我们拿到明确的设计稿时,则应该首先明确项目结构,做好基础部分的结构拆分,避免过于混乱的代码结构。 而我这里就是一个负面教材,边写边改的结构上稍微混乱了一些。

页面结构大体分为以下几层:

    <div id="app"> </div>
    <div class="keyboards"></div>

在现代开发单页面应用时,通常会有一个以#app命名的根节点,所有页面视图的操作终将会在此节点下。与此节点的兄弟节点,我们一般拿来做弹窗组件,称为自由组件。这样可以有效避免因为app内部样式和层级的污染导致自由组件的渲染异常,是很好的做法。

1. 在app主体中,采用模块化布局,由card结构完整的将模块包裹住。

    <div id="app"> 
        <div class="card license-plate">
            <div class="input" id="province"></div>
            <div class="input" id="region"></div>

            <div class="input num" id="num1"></div>
            <div class="input num" id="num2"></div>
            <div class="input num" id="num3"></div>
            <div class="input num" id="num4"></div>
            <div class="input num" id="num5"></div>
            <div class="input num new-energy" id="new-energy"></div>
        </div>
    </div>

2. keyboard部分,分为了字母+数字与省份两种,采用了两种不同的代码化方法,用于讲解

<div class="keyboards">
    <div class="mergekeyboard kbdpanel" style="display: none;">
        <div class="k-alphabet kbdpanel"> </div>
        <div class="k-num kbdpanel"> </div>
    </div>
    <div class="k-pro kbdpanel" style="display: none;">
        <div class="card keyboard">  </div>
    </div>
</div>

以上就是html的部分,由于项目简单没有更多可描述的内容。重点还是接下来的js和css的部分。

2. js部分

1. 选择器

const $ = (selector) => {
  const nodes = document.querySelectorAll(selector);
  if (nodes.length == 1)
    return nodes[0]
  return nodes
}

在编写函数时,对于脚本类型的文件遵循先定义后使用的模式。这里采用()=>{}箭头函数提到传统的function定义, 使用 const限定变量为不可变,在定义一个不可变的变量时,尽可能的使用而非其他申明方式。

获取元素有两个方法,第一个是 document.querySelector ,第二个是 document.querySelectorAll。 前者获取元素对象HTMLElement,后者获取对象列表NodeList。在没有此方法之前,jQuery就是简单优秀的元素查询方式而得到多数人的青睐。

2. 创建元素

const $c = (el) => {
  return document.createElement(el)
}

增删改查的几个部分中增加的方式就是如此,通过document.createElement创建一个元素对象。至于修改,一般我们说的是针对本身的styleattributeprop 的修改,删除则使用元素自带的el.remove()方法,将自己和下级子元素一并从dom树上移除。

3. 事件绑定

const $on = (el, name, call) => {
  if (typeof el == "string") {
    el = $(el)
  }
  if (el.length === undefined) {
    el.addEventListener(name, call)
    return () => el.removeEventListener(name, call)
  }
  else {
    const clears = [].map.call(el, (el) => {
      el.addEventListener(name, call)
      return () => el.removeEventListener(name, call)
    })
    return () => clears.forEach(remove => remove());
  }
}

事件绑定是指,元素的交互中的一般事件或者自定义事件,使用元素自带的addEventListener监听,使用removeEventListener取消监听。和传统的on绑定事件不同,采用监听方式时,可以同时对某种事件增加多个监听操作。因此,在删除事件时,也需要指定被删除的监听函数。

4. 初始化命名 xxx...Init

4.1 省份初始化
const provinceInit = () => {
  const provinces = (""
    + "京津沪渝"
    + "蒙宁新藏桂"
    + '黑吉辽'
    + '晋冀鲁'
    + '苏浙皖赣'
    + '湘鄂豫'
    + '粤闽琼'
    + '川滇黔'
    + '陕甘青'
    + '港澳台'
  )
    .split("")

  provinces.forEach((pro) => {
    const c = $c('div')
    c.innerText = pro
    c.className = "input keyboard"
    $(".k-pro>.card.keyboard").appendChild(c)
  });
}
  • 字符串拼接

字符串拼接有几种方式,第一种直接使用 + 来连接左右字符串,属于常见的操作方式之一。除此以外,还有几种方式:

// 1. 采用数组拼接形式,也是较为常用的形式之一
const str = ['a','b'].join("");

// 2. 连续的换行拼接在断句末尾使用 \ 符号

const str = "a\
b\
c";

// 3.使用格式化字符串
const str = `a
b
c
d`

但是要注意的是,2和3的方式都会包含到空白字符串,因此需要顶格书写。

  • 字符串分割 split

对于连续的字符串想要转成数组的形式,我们就会使用split("")这个方法,而想要按照某一种格式分割比如 |,就会使用到他的正则匹配。关于正则可以参考我编写的这一章做一个Emmet[html速写]【二】正则 - 掘金 (juejin.cn),希望对你有一些启发和帮助

  • 数组的循环 forEach

我们很少会使用for...loop去循环数组,更推荐使用数组自带的一些方法去操作他们。对数组的增pushunshift,删popshift,改 使用下标赋值 ,查findfindIndex

此外对于数组的一些操作还有 还会有数组的排序sort,从现有数组构建新的数组map,数据过滤filter,数组匹配includes,条件匹配 someevery,数组扁平flatflatMap,以及数组合并join,分割slice等等,当然还是不能少了一个重量级的操作方法splice,数组的元素插入全靠它了(也可以删除指定下标元素)。

  • 元素的操作

向一个元素内部添加内容的方式有三种,

  • 第一种赋值元素的innerText或者textContent添加纯文本内容,注意:此操作将会完全覆盖掉元素原有的内容。
  • 第二种赋值元素的innerHTML添加子元素,可以是注释,也可以是html代码片段。同样的将会覆盖掉原有的内容。
  • 第三种使用Element.prototype.append|prepend或者Node.prototype.appendChild,前者支持追加文本或者其他元素,后者只支持Node对象。

~ 衍生:Node和Element的关系为: Element继承于Node ,Node是更为基础的一个类型。针对dom的操作多数来自于Element,比如querySelector,而针对节点的操作多数自于Node,比如cloneNode

  • 给元素添加class

现有的class有两种管理形式,一种是古老的className,获取class属性的字符串值,另一种使用classListDomTokenList,使用列表的形式增删改查,确实方便很多。不过赋初值一般还是用字符串的形式,比较直接。 比如切换class可以直接使用classList.toggle('classname') 就很很方便的做切换。

4.2 字母键盘和数字键盘
const alphabetInit = () => {
  const alphabet = ["ABCDEFG", "HIJKLMN", "OPQ RST", "UVW XYZ"]
  alphabet.forEach(abc => {
    const ct = $c('div')
    ct.className = "card keyboard"

    abc.split("").forEach((ab) => {
      const c = $c('div')
      c.innerText = ab
      c.className = "input keyboard"

      if (ab == " ")
        c.classList.add("empty")
      else if (['I', "O"].includes(ab)) {
        c.classList.add("disabled")
      }
      ct.appendChild(c)
    })

    $(".k-alphabet").appendChild(ct)
  })
}

const numberInit = () => {
  const numbers = ["123", "456", "789", "0d"]
  numbers.forEach(abc => {
    const ct = $c('div')
    ct.className = "card keyboard"

    abc.split("").forEach((ab) => {
      const c = $c('div')
      c.innerText = ab;
      if (ab == 'd') {
        c.innerText = 'del'
      }
      c.className = "input keyboard"
      ct.appendChild(c)
    })

    $(".k-num").appendChild(ct)
  })
}

我们使用数组循环时,需要考虑到时间复杂度,一般forEach不应该超过三层。超过三层的循环一定是代码设计有问题,同样的使用嵌套判断也不应该超过三层。多层情况下应该考虑到程序设计模式,交叉使用多种设计模式,降低代码理解难度。

初始化所需元素后,就可以绑定事件了

4.3 响应事件 keyTouch
const keytouch = () => {
  let target = null
  const hidePanel = (t) => {
    $(".kbdpanel").forEach((el) => {
      el.style.display = "none";
    })
    if (target) {
      target.classList.remove("highlight")
    }
    if (t) {
      t.classList.add("highlight");
      target = t;
    }
  };
  const toNext = () => {

    if (target.nextElementSibling && target.nextElementSibling.id != "new-energy") {
      target.nextElementSibling.click();
    }
  }
  const els = [
    [
      $("#province"), () => $('.k-pro').style.display = "block"],

    [
      $("#region"), () => {
        $('.mergekeyboard').style.display = "flex";
        $('.k-alphabet').style.display = "block";
      }
    ],
    [
      $(".num"), () => {
        $('.mergekeyboard').style.display = "flex";
        $('.k-alphabet').style.display = "block";
        $('.k-num').style.display = "block";
      }
    ]
  ]

  els.forEach(([el, call]) => {
    $on(el, 'click', (e) => {
      hidePanel(e.currentTarget)
      call();
      e.stopPropagation()
      e.preventDefault();
    })
  })

  $on(".keyboards", 'click', (e) => {
    e.stopPropagation()
    e.preventDefault();
    if (target && e.target.className.match("input")) {
      if (e.target.innerText == 'del') {
        target.innerText = ""
      } else {
        target.innerText = e.target.innerText
      }
      toNext();
    }

  })
  $on(document, 'click', () => {
    console.log("false");
    hidePanel()
  })
}

查询元素的兄弟节点可以使用ELement.prototype.nextElementSibling|previousElementSibling这两个方法。好比数据链,不关心下一个是什么,我只需要知道下一个是否存在就可以了。

对于解构赋值 els.forEach(([el, call]) => {})

我们可以对数组、对象使用解构的形式获取所需的数据。比如 :当我们有一个指定长度的数组,且其数据是确定的,则可以使用 const [a,b,c,...rest] = [1,2,3,4,5,6] 获取其中a=1,b=2,c=3的数据变量,并且获取剩余的数组变量rest。

同样的,对于对象也是可以使用指定的 const {a,b,c,...rest} = {a:1,b:2,c:3} 来获取a=1,b=2,c=3的数据变量,以及剩余对象属性rest。

此外还可以对变量名进行重命名与默认值 const { a:num1, d=4 } = {a:1,b:2,c:3} ,同样还可以解构嵌套对象属性比如:const {user:{ name }} = {user:{name:'Lee'}} ,但是user此时并不可用,这点还需要注意。如果你需要user又希望获取到name,那还是需要拆分成两步解构的。

事件的触发对象

绑定回调事件中会有target与currentTarget两个属性,target代表当前事件的触发元素,currentTarget代表当前事件的绑定元素。

冒泡与默认事件 Event事件分为向上冒泡事件与非冒泡事件,绝大多数的事件都是冒泡的,比如click。被触发的对象将会依次冒泡查询绑定此动作的元素监听事件,如果在冒泡的过程中未被阻止,则会继续向上冒泡,直到document。而我们可以使用 stopPropagation方法阻止冒泡,这样事件从这里就终止了。

利用好事件冒泡,我们可以很简单的将某一对象的子元素重复绑定事件转而采用父元素代理事件的形式,一方面降低了绑定次数,另一方面对于新增加的元素也不需要进行新的事件绑定。高效又节省资源而且方便管理。

此外,一些元素会有默认的点击事件,比如a标签,将会发生默认的跳转动作,我们可以使用preventDefault来阻止此行为。

js的部分到这里就结束了。在使用这些方法或者代码组织时,一定审慎对待,才可以编写出良好的代码。

3. css部分

css是层叠样式表,理解为盖房子,默认情况下相同选择器下后定义的总会覆盖到前定义的,是随着时间轴的进程而更新的。当我们想强制使用某些属性而不被覆盖时,可以使用选择器的优先级,比如id优先级大于class,而class又大于tag。我们不推荐css scoped,也就是标签上直接定义样式,那样会难以维护,除非你想偷懒。 凡事总有例外,对于某些较为复杂的层级,快速而直接的覆盖除了css scoped,可以在属性值后追加 !important。例如: font-size:60px !important;

  • 消除浏览器默认样式
 body {
    margin: 0;
}

对于样式的初始化时,通常会做一些比如reset.css或者normalize.css这种初始化动作,以消除浏览器的user-agent代理样式的差异。让样式在不同的浏览器反应一致。也许有人说上面的行为重置样式过时了,现在也许有更好的替代品。

  • 样式的继承
.card {
    font-size: 14px;
}
.card {
    font-size: 14px;
}

.input {
    height: 25px;
    width: 25px;
    line-height: 25px;
    display: inline-block;
    border: 1px solid;
    border-radius: 5px;
    text-align: center;
}

跟字体font-相关的基本都是可以继承的,比如font-wight,或者color。我们也可以使用inherit主动继承某不支持继承的属性,比如background:inherit,不过他们一般都没什么意义。我们还可以使用initial或者unset呈现这个属性的默认情况。

  • inline元素的vertical-align inline元素有一个vertical-align对齐方式,如果不设置默认为baseline,当兄弟元素的内部有不同的内容时就会产生一些奇怪的问题:

image.png

.keyboard {
    display: flex;
    flex-wrap: wrap;
}
  • display:flex

flex弹性盒子,现在属于基础中的基础了,对于布局有更简单的操作,而grid布局因为较为复杂的理解度,未能广泛应用。

  • :not(.disabled, .empty)
.k-pro .input:nth-child(2n) {
    background-color: aliceblue;
}

.input.keyboard {
    display: flex;
    justify-content: center;
    align-items: center;
    box-shadow: 0px 4px 4px -2px gray;
    background: white;
    margin: 5px;
}

.input.keyboard:not(.disabled, .empty):active {
    background: #52aeff !important;
}

这是一个选择否定,对某些选择器内容的否定,通常用于某个元素不包含某些选择器时生效。使用逗号判断多个

  • :active

跟 :active 动作相关的属性还有6种,他们是:

image.png

使用他们时,需要注意定义时前后顺序。

  • :nth-child(2n) nth-child除了可以指定元素的索引(从1开始),还可以使用一定的算术表达式。2n代表的是偶数。

  • 简写 inset 属性

.card.keyboard {
    position: fixed;
    bottom: 0;
    width: 100%;
    justify-content: center;
    padding-block: 10px;
    box-shadow: 1px -11px 5px aliceblue;
    background: #f3f3f3;

}

.card.keyboard:nth-child(1) {
    border-top: 1px solid gray;
}

.k-alphabet>.card,
.k-num>.card {
    position: relative;
    padding: 0;
    box-shadow: none;
}

.input.keyboard.empty {
    border: none;
    background-color: initial;
    box-shadow: none;
}

.input.keyboard.disabled {
    color: gray;
    box-shadow: none;
}


.k-num>.card.keyboard {
    padding-left: 5px;
}

.k-num>.card.keyboard>.input {
    background-color: beige;
}

.mergekeyboard {
    background: #f3f3f3;
    display: flex;
    position: fixed;
    padding-bottom: 10px;
    bottom: 0;
    inset-inline: 0;
    justify-content: center;
}

.license-plate {
    text-align: center;
    padding-top: 20vh;
}

#province,
#region,
#new-energy {
    position: relative;
}

#province {
    background: cornsilk;
}

inset是一个不常用的属性,代表的是 top,right,bottom,left的属性组合

  • -inline 还是 -block

在方向上有一个排版方向缩写,inline和block,比如margin-inline,代表的是inline排版方向上的margin值,也就是margin-left和margin-right。而这两个代词将会受到writing-mode的影响。

  • vh还是px viewport视窗单位出来了很久,也很受欢迎,尤其是移动端。

  • :empty 如果一个元素的内部什么也没有,可以使用此选择器辅助项来额外声明。而配合上伪元素则可以生成占位符的效果,很nice。

image.png

  • 伪元素::after

伪元素是一种shadow元素,不占渲染dom单位,在某些时候非常有用。为元素的content可以接纳字符串,特定标签属性。默认的display为none,因此需要手动设置display属性才能显示。

#province:empty::after {
    content: "省";
    display: block;
    position: absolute;
    inset: 0;
    color: rgb(103, 103, 103);
}

#region {
    margin-right: 10px;
}

#region:empty::after {
    content: "市";
    display: block;
    position: absolute;
    inset: 0;
    color: rgb(103, 103, 103);
}

#new-energy:empty::after {
    content: "新";
    display: block;
    position: absolute;
    inset: 0;
    color: rgb(103, 103, 103);
}

.highlight {
    border-color: #52aeff;
    background-color: aqua !important;
}

结束语:以上的内容都是很简单的内容,旨在对所学的巩固与融会贯通,从一种高度去看待问题,多种方法结合才能达到想要的效果。