陈大鱼头

陈大鱼头 查看完整档案

深圳编辑  |  填写毕业院校卖鱼头的  |  Web前端工程师 编辑 www.krissarea.com 编辑
编辑

Kris不只是一只鱼头

个人动态

陈大鱼头 发布了文章 · 1月18日

你不知道的 CSS 进度条

进度条是一个非常常见的功能,实现起来也不难,一般我们都会用 div 来实现。

作为一个这么常见的需求, whatwg 肯定是不会没有原生组件提供(虽然有我们也不一定会用),那么就让我们来康康有哪些有意思的进度条实现方式。

常规版 — div 一波流

这是比较常规的实现方式,先看效果:

源码如下:

<style>
  .progress1 {
    height: 20px;
    width: 300px;
    background-color: #f5f5f5;
    border-bottom-right-radius: 10px;
    border-top-right-radius: 10px;
  }
  .progress1::before {
    counter-reset: progress var(--percent, 0);
    content: counter(progress) '%\2002';
    display: block;
    height: 20px;
    line-height: 20px;
    width: calc(300px * var(--percent, 0) / 100);
    font-size: 12px;
    color: #fff;
    background-color: #2486ff;
    text-align: right;
    white-space: nowrap;
    overflow: hidden;
    border-bottom-right-radius: 10px;
    border-top-right-radius: 10px;
  }
  .btn {
    margin-top: 30px;
  }
</style>
<div id="progress1" class="progress1"></div>
<button id="btn" class="btn">点我一下嘛~</button>
<script>
  'use strict';
  let startTimestamp = (new Date()).getTime();
  let currentPercentage = 0;
  let maxPercentage = 100;
  let countDelay = 100;
  let timer = null;
  let start = false;
  const percentageChange = () => {
    const currentTimestamp = (new Date()).getTime();
    if (currentTimestamp - startTimestamp >= countDelay) {
      currentPercentage++;
      startTimestamp = (new Date()).getTime();
      progress1.style = `--percent: ${currentPercentage}`;
    };
    if (currentPercentage < maxPercentage) {
      timer = window.requestAnimationFrame(percentageChange);
    } else {
      window.cancelAnimationFrame(timer);
    };
  };
  const clickHander = () => {
    if (!start) {
      start = true;
      percentageChange();
    };
  };
  btn.addEventListener('click', clickHander);
</script>

这种方法的核心就是以当前盒子为容器,以 ::before 为内容填充。用 <div> 的好处就是实现简单,兼容性强,拓展性高,但是美中不足的是标签语义化不强。

进阶版 — input type="range"

<input /> 是一个非常实用的替换元素,不同的 type 可以做不同的事情。第二种就是用 <input type="range" /> 来实现的。首先我们来看看效果:

源码如下:

<style>
  .progress2[type='range'] {
    display: block;    
    font: inherit;
    height: 20px;
    width: 300px;
    pointer-events: none;
    background-color: linear-gradient(to right, #2376b7 100%, #FFF 0%);
  }
  .progress2[type='range'],
  .progress2[type='range']::-webkit-slider-thumb { 
    -webkit-appearance: none;
  };
  .progress2[type='range']::-webkit-slider-runnable-track {
    border: none;
    border-bottom-right-radius: 10px;
    border-top-right-radius: 10px;
    height: 20px;
    width: 300px;
  }
  .btn {
    margin-top: 30px;
  }
</style>
<input id="progress2" class="progress2" type='range' step="1" min="0" max="100" value="0"/>
<button id="btn" class="btn">点我一下嘛~</button>
<script>
  'use strict';
  let startTimestamp = (new Date()).getTime();
  let currentPercentage = 0;
  let maxPercentage = 100;
  let countDelay = 100;
  let timer = null;
  let start = false;
  let percentageGap = 10;
  const percentageChange = () => {
    const currentTimestamp = (new Date()).getTime();
    if (currentTimestamp - startTimestamp >= countDelay) {
      currentPercentage++;
      startTimestamp = (new Date()).getTime();
      progress2.value = currentPercentage;
      progress2.style.background = `linear-gradient(to right, #2376b7 ${currentPercentage}%, #FFF 0%`;
    };
    if (currentPercentage < maxPercentage) {
      timer = window.requestAnimationFrame(percentageChange);
    } else {
      window.cancelAnimationFrame(timer);
    };
  };
  const clickHander = () => {
    if (!start) {
      start = true;
      percentageChange();
    };
  };
  btn.addEventListener('click', clickHander);
</script>

写完这个 demo 才发现,<input type="range" /> 并不适合做这个功能。。一个是实现困难,这个 type 组件的每个元件都可以单独修改样式,但是效果并不是很好。

另一个是因为 range 有专属语意 —— 范围,所以它更适合做下面这种事:

以上demo来自:https://developer.mozilla.org...

高级版 — progress 鸭

当然,上述两种方式都是模拟进度条,实际上我们并不需要模拟,因为 whatwg 有为我们提供原生的进度条标签 —— <progress>

我们先看效果:

实现如下:

<style>
  .progress3 {
    height: 20px;
    width: 300px;
    -webkit-appearance: none;
    display: block;
  }
  .progress3::-webkit-progress-value {
    background: linear-gradient(
      -45deg, 
      transparent 33%, 
      rgba(0, 0, 0, .1) 33%, 
      rgba(0,0, 0, .1) 66%, 
      transparent 66%
    ),
      linear-gradient(
        to top, 
        rgba(255, 255, 255, .25), 
        rgba(0, 0, 0, .25)
      ),
      linear-gradient(
        to left,
        #09c,
        #f44);
    border-radius: 2px; 
    background-size: 35px 20px, 100% 100%, 100% 100%;
  }
  .btn {
    margin-top: 30px;
  }
</style>
<progress id="progress3" class="progress3" max="100" value="0"></progress>
<button id="btn" class="btn">点我一下嘛~</button>
<script>
  'use strict';
  let startTimestamp = (new Date()).getTime();
  let currentPercentage = 0;
  let maxPercentage = 100;
  let countDelay = 100;
  let timer = null;
  let start = false;
  const percentageChange = () => {
    const currentTimestamp = (new Date()).getTime();
    if (currentTimestamp - startTimestamp >= countDelay) {
      currentPercentage++;
      startTimestamp = (new Date()).getTime();
      progress3.setAttribute('value', currentPercentage);
    };
    if (currentPercentage < maxPercentage) {
      timer = window.requestAnimationFrame(percentageChange);
    } else {
      window.cancelAnimationFrame(timer);
    };
  };
  const clickHander = () => {
    if (!start) {
      start = true;
      percentageChange();
    };
  };
  btn.addEventListener('click', clickHander);
</script>

虽然有原生的进度条标签,但是规范里并没有规定它的具体表现,所以各个浏览器厂商完全可以按照自己的喜好去定制,样式完全不可控,所以标签虽好。。可用性却不强,有点可惜。

终极版 — meter 赛高

当然,能够实现进度条功能的标签,除了上面所说的,还有 <meter> 标签。先看效果:

代码如下:

<style>
  .progress4 {
    display: block;    
    font: inherit;
    height: 50px;
    width: 300px;
    pointer-events: none;
  }
  .btn {
    margin-top: 30px;
  }
</style>
<meter id="progress4" class="progress4" low="60" high="80" min="0" max="100" value="0"></meter>
<button id="btn" class="btn">点我一下嘛~</button>
<script>
  'use strict';
  let startTimestamp = (new Date()).getTime();
  let currentPercentage = 0;
  let maxPercentage = 100;
  let countDelay = 100;
  let timer = null;
  let start = false;
  const percentageChange = () => {
    const currentTimestamp = (new Date()).getTime();
    if (currentTimestamp - startTimestamp >= countDelay) {
      currentPercentage++;
      startTimestamp = (new Date()).getTime();
      progress4.value = currentPercentage;
    };
    if (currentPercentage < maxPercentage) {
      timer = window.requestAnimationFrame(percentageChange);
    } else {
      window.cancelAnimationFrame(timer);
    };
  };
  const clickHander = () => {
    if (!start) {
      start = true;
      percentageChange();
    };
  };
  btn.addEventListener('click', clickHander);
</script>

这个标签可能比较陌生,实际上它跟 <input type="range"> 的语义是一样的,用来显示已知范围的标量值或者分数值。不一样的就是。。。它样式改起来更麻烦。

总结

本文测评了4种实现进度条的方式,得出的结论就是 —— <div> 赛高。。。虽然有的时候想优雅一点追求标签语义化,但是资源不支持,也很尴尬。

嗯,万能的 <div>

以上 demo 都可以我的 codepen 上查看:https://codepen.io/krischan77...

查看原文

赞 23 收藏 15 评论 2

陈大鱼头 发布了文章 · 2020-12-14

让Vue3 Composition API 存在于你 Vue 以外的项目中

作为新特性 Composition API ,在 Vue3 正式发布之前一段时间就发布过了。

距文档介绍, Composition API 是一组低侵入式的、函数式的 API,使得我们能够更灵活地「组合」组件的逻辑。

不仅在 Vue 中,在其他的框架或原生 JS 也可以很好地被使用,下面我们就选取几个比较重要的 Composition API ,通过一些简单的例子来看看如何在其他项目中使用。

注:本文仅列出各个分类下比较重要的 API,想要查看全部可以点击下方链接进行查看:

https://github.com/vuejs/vue-...

reactive API

createReactiveObject

createReactiveObject 函数是 reactive API 的核心,用于创建 reactive 对象 。

在分享 API 之前,我们先看看其核心实现,由于篇幅有限,本文仅展示出理解后的简化版代码,代码如下:

function createReactiveObject(
  target, // 要监听目标
  isReadonly, // 是否只读
  baseHandlers, // target 为 Object 或 Array 时的处理器,支持对数据的增删改查
  collectionHandlers // target 为 Map/WeakMap 或 Set/WeakSet 时的处理器,支持对数据的增删改查
) {
    if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) {
      // 当 target 已经是一个 Proxy 时,直接返回
      // 例外情况:在 Proxy 里调用 readonly()
        return target
    }
    // 当前对象已被监听过时,就直接返回被监听的对象
    if (existingProxy) {
      return existingProxy
    }
    // 如果是 Object Array Map/WeakMap Set/WeakSet 以外只能监听到值的数据,直接返回
    if (targetType === TargetType.INVALID) {
      return target
    }
    // 根据参数类型生成对应的 Proxy 对象,以及添加对应的处理器
    const proxy = new Proxy(
      target,
      targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}

reactive

接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

示例如下:

import {
  reactive,
  isReactive // 判断是否是 reactive 对象
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
const obj = {
  nested: {
    foo: 1
  },
  array: [{ bar: 2 }]
}
const value = 10

const observedObj = reactive(obj)
const observedValue = reactive(value)

console.log(isReactive(observedObj)) // true
console.log(isReactive(observedValue)) // true

shallowReactive

只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样。

示例如下:

const obj = {
  nested: {
    foo: 1
  },
  array: [{ bar: 2 }]
}
const value = 10

const unobservedObj = shallowReactive(obj)
const unobservedValue = shallowReactive(value)

console.log(isReactive(observedObj)) // false
console.log(isReactive(observedValue)) // false

effect API

createReactiveEffect

createReactiveEffect 是 effect API 的核心,用于创建监听用户自定义的 reactive 对象的函数

在分享 API 之前,我们先看看其核心实现,由于篇幅有限,本文仅展示出理解后的简化版代码,代码如下:

function createReactiveEffect(
    fn, // 用户创建的 reactive 对象变动执行回调
  options = {
    lazy, // 是否执行用户函数
    scheduler, // 收集数据记录
    onTrack, // 追踪用户数据的回调
    onTrigger, // 追踪变更记录
    onStop, // 停止执行
    allowRecurse // 是否允许递归
  }
) {
    const effect = function reactiveEffect() {
      if (!effectStack.includes(effect)) {
        cleanup(effect) // 清空 effect
        try {
          enableTracking() // 往追踪用户数据的栈内添加当前 effect
          effectStack.push(effect) // 往 effect 栈内添加 effect
          activeEffect = effect // 将活动 effect 变成当前 effect
          return fn() // 执行回调
        } finally { // 删除当前记录
          effectStack.pop()
          resetTracking()
          activeEffect = effectStack[effectStack.length - 1]
        }
      }
    }
        effect.id = uid++
    effect._isEffect = true
    effect.active = true
    effect.raw = fn
    effect.deps = []
    effect.options = options
    return effect
}

effect

effect 函数是 effect API 的核心。以 WeakMap 为数据类型,是一个用于存储用户自定义函数的 订阅者

示例如下:

let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy === 0) // true
counter.num = 7
console.log(dummy === 7) // true

ref API

RefImpl

RefImpl 是 ref API 的核心,用于创建 ref 对象

在分享 API 之前,我们先看看其核心实现,代码如下:

class RefImpl {
  private _value // 存储当前 ref 对象的值

  public __v_isRef = true // 确定是否为 ref 对象的变量 (只读)

  constructor(
    private _rawValue, // 用户传入的原始值
    public readonly _shallow = false // 当前 ref 对象是否为 shallowRef
  ) {
    // convert:如果传入的原始值为对象,则会被 convert 函数转换为 reactive 对象
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // 用于追踪用户输入的值变化
    // track:effect api 的 track 函数,用于追踪用户行为,当前则是追踪用户的 get 操作
    // toRaw:effect api 的 toRaw 函数,将 this 转化为用户输入值
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      // 当前 ref 对象有变化时
      // _rawValue / _value 变更
      // trigger:effect api 的 trigger 函数,根据用户传入的值与操作类型来进行操作,当前则为将用户传入的值添加到值 map 里
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换。

示例如下:

const count = ref({
  name: '鱼头',
  type: '帅哥'
})
console.log(count.value.type) // 帅哥
count.value.type = '超级大帅哥'
console.log(count.value.type) // 超级大帅哥

shallowRef

创建一个 ref ,将会追踪它的 .value 更改操作,但是并不会对变更后的 .value 做响应式代理转换(即变更不会调用 reactive

示例如下:

const __shallowRef = shallowRef({ a: 1 })
let dummy
effect(() => {
  dummy = __shallowRef.value.a
})
console.log(dummy) // 1

__shallowRef.value.a = 2
console.log(dummy) // 1
console.log(isReactive(__shallowRef.value)) // false

customRef

customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 getset 属性的对象。

示例如下:

let value = 1
let _trigger

const custom = customRef((track, trigger) => ({
  get() {
    track()
    return value
  },
  set(newValue) {
    value = newValue
    _trigger = trigger
  }
}))

let dummy
effect(() => {
  dummy = custom.value
})
console.log(dummy) // 1

custom.value = 2
console.log(dummy) // 1

_trigger()
console.log(dummy) // 2

triggerRef

triggerRef 用于主动触发 shallowRef

示例如下:

const __shallowRef = shallowRef({ a: 1 })
let dummy
effect(() => {
  dummy = __shallowRef.value.a
})
console.log(dummy) // 1

__shallowRef.value.a = 2
console.log(dummy) // 1
console.log(isReactive(__shallowRef.value)) // false

triggerRef(__shallowRef)
console.log(dummy) // 2

computed API

ComputedRefImpl

ComputedRefImpl 是 ref API 的核心,用于创建 computed 对象

在分享 API 之前,我们先看看其核心实现,由于篇幅有限,本文仅展示出理解后的简化版代码,代码如下:

class ComputedRefImpl {
  private _value // 当前值
  private _dirty = true // 当前值是否发生过变更

  public effect // effect 对象

  public __v_isRef = true; // 指定为 ref 对象
  public [ReactiveFlags.IS_READONLY]: boolean // 是否只读

  constructor(
    getter, // getter
    private _setter, // setter
    isReadonly // 是否只读
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          // 将变更状态变为 true
          // trigger:effect api 的 trigger 函数,根据用户传入的值与操作类型来进行操作,当前则为将用户传入的值添加到值 map 里
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    if (this._dirty) {
      // 返回当前值
      // 将变更状态变为 false
      this._value = this.effect()
      this._dirty = false
    }
       // track:effect api 的 track 函数,用于追踪用户行为,当前则是追踪用户的 get 操作
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue) {
    this._setter(newValue)
  }
}

computed

传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。或者传入一个拥有 getset 函数的对象,创建一个可手动修改的计算状态。

示例如下:

const count1 = ref(1)
const plus1 = computed(() => count1.value + 1)
console.log(plus1.value) // 2
plus1.value++ // Write operation failed: computed value is readonly

const count2 = ref(1)
const plus2 = computed({
  get: () => count2.value + 1,
  set: val => {
    count2.value = val - 1
  }
})
console.log(plus2.value) // 2
plus2.value = 0
console.log(plus2.value) // 0
查看原文

赞 10 收藏 6 评论 2

陈大鱼头 发布了文章 · 2020-09-06

『极限版』不掺水,用纯 CSS 来实现超飒的表单验证功能

去年的时候写过一篇文章 纯CSS实现表单验证 ,在发表之后不久就有网友跟鱼头说,打算拿我这篇文章作团队内部分享。

当时听到这个消息之后,在屏幕前的鱼头笑咧了嘴,但这位童鞋的下一段内容,就让我马上笑不起出来了。

不过因为初始化状态是这样的:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation/invalid.png

所以希望我能够改一下,改成这样:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/1.png

只有在进行输入且输入内容不对的时候才展示错误信息。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/2.jpg

这位童鞋:“所以这功能能实现吗?”

我:“。。。。。。”

既然有童鞋这么看得起鱼头,还打算拿鱼头的 DEMO 来作内部分享,那总得硬着头皮来实现这个功能。

首先我们来看一下最终成果图:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/3.gif

DEMO 在线查看地址:https://codepen.io/krischan77...

各位读者童鞋,来跟鱼头一起拆分下功能实现:

HTML

首先我们来看 HTML 的源码

<form class="form" id="form" method="get" action="/api/form">
    账号:
    <input data-title="账号" placeholder="请输入正确的账号" pattern="\w{6,10}" name="account" type="text" required />
    <span class="f-tips">请输入正确的账号</span>
    <br />
    密码:
    <input data-title="密码" placeholder="请输入正确的密码" pattern="\w{6,10}" name="password" type="password" required />
    <span class="f-tips">请输入正确的密码</span>
    <br />
    <input name="button" type="submit" value="提交" />
</form>

这里面的 HTML 标签都比较常规,但是我们要注意下 <input /> 所携带的几个属性:

required

<input required/> 中的 required 是一个布尔属性,用来告诉浏览器这个 <input> 是否是必填项。

我们来康康DEMO:

<section class="section">
    <h1>请输入信息</h1>
    <form action="/userInfo">
        <input name="text" type="text" required />
        <input name="submit" type="submit" value="提交信息">
    </form>
</section>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/4.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/7.png

原生样式体验也是不错的。

pattern

再来 pattern 属性。

<input pattern=""> 用于校验输入 value 是否有效。

我们康康DEMO:

<section class="section">
    <form>
        <h1>请输入 我爱鱼头</h1>
        <input name="text" type="text" pattern="我爱鱼头"  required />
        <button type="submit">提交信息</button>
    </form>
</section>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/5.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/css-form-validation2/6.png

不得不感慨,原生组件的能力也是很强的。

CSS

接下来我们康康CSS的部分,源码如下:

:root {
    --error-color: red;
}
.form > input {
    margin-bottom: 10px;
}
.form > .f-tips {
    color: var(--error-color);
    display: none;
}
input[type="text"]:invalid ~ input[type="submit"],
input[type="password"]:invalid ~ input[type="submit"] {
    display: none;
}
input[required]:focus:invalid + span {
    display: inline;
}
input[required]:empty + span {
    display: none;
}
input[required]:invalid:not(:placeholder-shown) + span {
    display: inline;
}

我们重点介绍以下几个 CSS 选择器:

:invalid 与 :valid

判断有效性的伪类选择器(:valid:invalid)匹配有效或无效,<input><form>元素。

:valid伪类选择器表示值通过验证的<input>,这告诉用户他们的输入是有效的。

:invalid伪类选择器表示值不通过通过验证的<input>,这告诉用户他们的输入是无效的。

例子如下:

<style>
    input:valid {
        outline: 1px solid green;
    }

    input:invalid {
        outline: 1px solid red;
    }
</style>
输入文字:
<input type="text" pattern="[\w]+" required />
<br />
输入电话号码:
<input type="tel" pattern="[0-9]+" required />

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/valid-demo.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/valid.png

:placeholder-shown

:placeholder-shown 伪类 在 <input><textarea> 元素显示 placeholder text 时生效。

例子如下:

<style>
    input {
        border: 2px solid black;
        padding: 3px;
    }

    input:placeholder-shown {
        border-color: silver;
    }
</style>
<input placeholder="Type something here!">

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/placeholder-shown-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/placeholder-shown.png

实现逻辑

有了上面的几个 <input /> 属性以及 css 选择器的伪类说明,那么这个纯CSS实现表单验证的功能就变得简单多了。

我们先来整理下功能要求:

  1. 初始化状态:不展示提交按钮以及错误提示
  2. 清空输入状态:不展示提交按钮以及错误提示
  3. 输入错误状态:输入框输入错误时,展示错误提示
  4. 输入正确状态:输入框输入正确时,隐藏错误提示,展示提交按钮

初始化状态

首先我们知道,初始化 时,是没有提示信息的,所以提示信息可以直接隐藏,至于提交按钮,我们就利用 :invalid 来隐藏,因为初始化的 input.value 内容是不匹配的。所以我们有:

<style>
    .form > .f-tips {
        color: var(--error-color);
        display: none;
    }
    input[type="text"]:invalid ~ input[type="submit"],
    input[type="password"]:invalid ~ input[type="submit"] {
        display: none;
    }
</style>
<input data-title="账号" placeholder="请输入正确的账号" pattern="\w{6,10}" name="account" type="text" required />
<input data-title="密码" placeholder="请输入正确的密码" pattern="\w{6,10}" name="password" type="password" required />
<span class="f-tips">请输入正确的密码</span>

清空输入状态

清空输入状态 也比较简单,可以直接用伪类选择器 :empty 来判断,只要内容为空,则隐藏错误信息,所以我们有:

input[required]:empty + span {
    display: none;
}

输入错误状态

初始化 时已经隐藏了错误信息,而 初始化 其实也是依赖于 输入错误 这个状态,不过好在我们有伪类选择器 :focus ,它表示获得焦点的元素(如表单输入),所以我们有:

input[required]:focus:invalid + span {
    display: inline;
}

虽然我们不能通过 输入错误 这个状态来处理,但是我们可以监听用户聚焦的行为来实现。

但是这么做有个弊端,就是当我在另外一个输入框输入信息的时候,错误提示也会消失,所以我们还需要判断是否有 placeholder,输入了 value ,自然没有 placeholder ,所以我们有:

input[required]:invalid:not(:placeholder-shown) + span {
    display: inline;
}

输入正确状态

当完成上述三个状态的实现之后, 输入正确 的状态就可以不用编写了,因为不匹配错误的,就是匹配正确。

总结

一个完整的 纯CSS表单功能 就这么完成了,DEMO地址在这:

https://codepen.io/krischan77...

由于实际项目的复杂度,这个功能不一定直接用起来,但是里面的知识点,思路我们都是可以复用的。

不得不感慨,如今 htmlcss 的能力变得强大了起来,只要我们愿意散发思维,一定能编写出更多有意思,有价值的效果。

欢迎大家多方尝试!

参考资料

  1. whatwg 4.10.5 The input element
  2. 纯CSS实现表单验证
  3. 『真香警告』这33个超级好用的CSS选择器,你可能见都没见过。
  4. CSS 选择器
查看原文

赞 10 收藏 7 评论 0

陈大鱼头 发布了文章 · 2020-07-21

『真香警告』这33个超级好用的CSS选择器,你可能见都没见过。

前言

CSS 选择器是 CSS 世界中非常重要的一环。

在 CSS 2 之后,所有的 CSS 属性都是按模块去维护的。

CSS 选择器也是如此,然而如今也已经发布了第四版 —— CSS Selectors Level 4 ,这一版最早的草案发布于2011年09月29日,最后更新是2018年11月21日。

下面让我们一起来看看 Level 4 新推出的一些选择器。

正文

下面我们按照类型来划分

逻辑组合(Logical Combinations)

在这个分类下,我们有以下四个选择器:

:not()

其实 :not() 不算是新标签,不过在 Level 4 里,增加了多选的功能,代码如下:

/* 除了.left, .right, .top之外所以的div的盒子模型都会变成flex
*/
div:not(.left, .right, .top) {
  display: flex;
}

/* 等价于 */
div:not(.left), div:not(.right), div:not(.top) {
  display: flex;
}

兼容性如下:

not().png

额。。。还不能用

:is()

:is() 伪类将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素。这对于以更紧凑的形式编写大型选择器非常有用。

看个栗子:

/* 选择header, main, footer里的任意一个悬浮状态的段落(p标签) */
:is(header, main, footer) p:hover {
  color: red;
  cursor: pointer;
}

/* 等价于 */
header p:hover,
main p:hover,
footer p:hover {
  color: red;
  cursor: pointer;
}

兼容如下:

is().png

:where()

:where() 伪类接受选择器列表作为它的参数,将会选择所有能被该选择器列表中任何一条规则选中的元素。

其实就是跟 :is() ,唯一不同的就是 :where() 的优先级总是为 0 ,但是 :is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。

代码如下:

<style>
    :is(section.is-styling, aside.is-styling, footer.is-styling) a {
        color: red;
    }

    :where(section.where-styling, aside.where-styling, footer.where-styling) a {
        color: orange;
    }

    footer a {
        color: blue;
    }
</style>
<article>
    <h2>:is()-styled links</h2>
    <section class="is-styling">
        <p>Here is my main content. This <a href="https://mozilla.org">contains a link</a>.
    </section>

    <aside class="is-styling">
        <p>Here is my aside content. This <a href="https://developer.mozilla.org">also contains a link</a>.
    </aside>

    <footer class="is-styling">
        <p>This is my footer, also containing <a href="https://github.com/mdn">a link</a>.
    </footer>
</article>

<article>
    <h2>:where()-styled links</h2>
    <section class="where-styling">
        <p>Here is my main content. This <a href="https://mozilla.org">contains a link</a>.
    </section>

    <aside class="where-styling">
        <p>Here is my aside content. This <a href="https://developer.mozilla.org">also contains a link</a>.
    </aside>

    <footer class="where-styling">
        <p>This is my footer, also containing <a href="https://github.com/mdn">a link</a>.
    </footer>
</article>

:is():where() 对比效果图如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/where-is-demo.png

兼容性如下:

where().png

:has()

:has() 伪类代表一个元素,其给定的选择器参数(相对于该元素的 :scope)至少匹配一个元素。

:has() 接受一个选择器组作为参数。在当前规范中 :has() 并未列为实时选择器配置的一部分,意味着其不能用于样式表中。

语法如下:

// 下面的选择器只会匹配直接包含 <img> 子元素的 <a> 元素
a:has(> img)

// 下面的选择器只会匹配其后紧跟着 <p> 元素的 <h1> 元素:
h1:has(+ p)

兼容性如下:

has().png

嗯,全红。。。

语言伪类(Linguistic Pseudo-classes)

:dir()

:dir()伪类匹配特定文字书写方向的元素。在HTML中, 文字方向由dir属性决定。其他的文档类型可能有其他定义文字方向的方法。

:dir() 并不等于使用 [dir=…] 属性选择器。后者匹配 dir 的值且不会匹配到未定义此属性的元素,即使该元素继承了父元素的属性;类似的, [dir=rtl][dir=ltr]不会匹配到dir属性的值为auto的元素。而:dir()会匹配经过客户端计算后的属性, 不管是继承的dir值还是dir值为auto的。

例子如下:

<style>
    :dir(ltr) {
        background-color: yellow;
    }

    :dir(rtl) {
        background-color: powderblue;
    }
</style>
<div dir="rtl">
      <span>test1</span>
      <div dir="ltr">test2
        <div dir="auto">עִבְרִית</div>
      </div>
</div>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/dir-demo.png

兼容性如下:

dir().png

又是一片红。。

:lang()

:lang() 伪类基于元素语言来匹配页面元素。

例子如下:

/* 下例表示选择文本语言带有-TN的div元素 (ar-TN, fr-TN). */

div:lang(*-TN) {
    background-color: green
}

浏览器支持状态:没有一个支持的。

位置伪类(Location Pseudo-classes)

:any-link

:any-link 伪类 选择器代表一个有链接锚点的元素,而不管它是否被访问过,也就是说,它会匹配每一个有 href 属性的 <a><area><link>元素。因此,它会匹配到所有的 :link:visited

例子如下:

<style>
    a:any-link {
        border: 1px solid blue;
        color: orange;
    }

    /* WebKit 内核浏览器 */
    a:-webkit-any-link {
        border: 1px solid blue;
        color: orange;
    }
</style>
<a href="https://example.com">External link</a><br>
<a href="#">Internal target link</a><br>
<a>Placeholder link (won't get styled)</a>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/any-link-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/any-link.png

:local-link

:local-link伪类可以单独格式化本地链接(原文是local links)(内部链接)。

例子如下:

a:local-link {
   text-decoration: none;
}

效果 & 兼容性

没有一个浏览器是支持的,看不到效果

:target-within

:target-within伪类适用于:target所匹配的元素,以及它DOM节点内所有匹配的元素。

例子如下:

div:target-within {
      border: 2px solid black;
}

效果 & 兼容性

没有一个浏览器是支持的,看不到效果

:scope

:scope伪类表示作为选择器要匹配的作用域的元素。不过目前它等效于 :root

因为尚未有浏览器支持CSS的局部作用域。

例子如下:

:scope {
      background-color: lime;
}

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/scope.png

浏览器算法不支持,兼容有跟没没区别~

用户行为伪类(User Action Pseudo-classes)

:focus-visible

当元素匹配 :focus 伪类并且客户端(UA)的启发式引擎决定焦点应当可见(在这种情况下很多浏览器默认显示“焦点框”。)时,:focus-visible 伪类将生效。

这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

例子如下:

<style>
    input, button {
        margin: 10px;
    }

    .focus-only:focus {
        outline: 2px solid black;  
    }

    .focus-visible-only:focus-visible {
        outline: 4px dashed darkorange;
    }
</style>
<input value="Default styles"><br>
<button>Default styles</button><br>
<input class="focus-only" value=":focus only"><br>
<button class="focus-only">:focus only</button><br>
<input class="focus-visible-only" value=":focus-visible only"><br>
<button class="focus-visible-only">:focus-visible only</button>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/focus-visible-demo.gif

兼容性如下:

目前只有Chrome 67+ 兼容...

:focus-within

:focus-within伪类适用于:focus所匹配的元素,以及它DOM节点内所有匹配的元素。

例子如下:

<style>
    form {
        border: 1px solid;
        color: gray;
        padding: 4px;
    }

    form:focus-within {
        background: #ff8;
        color: black;
    }

    input {
        margin: 4px;
    }
</style>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/focus-within-demo.gif

时间尺寸伪类(Time-dimensional Pseudo-classes)

:current && :past && :future

这个伪类选择器会选择HTML5<video>的语言渲染以及播放过程中的时间维度相对元素。所有相关的选择器都像:matches()。这几个伪类选择器的区别在于:past会选择:current所选的元素之前的所有节点。所以,:future就是指之后的所有节点了。

例子如下:

/* Current */
:current(p, span) {
      background-color: yellow;
}


/* Past */
:past,
/* Future */
:future {
      display: none;
}

兼容性如下:

目前没有任何浏览器支持

输入伪类(The Input Pseudo-classes)

:read-only:read-write

:read-only伪类选择器表示当前元素是用户不可修改的。

:read-write伪类选择器表示当前元素是用户可修改的。这个伪类选择器可以使用在一个可输入的元素或 contenteditable 元素(HTML5 属性)。

例子如下:

<style>
    :read-only {
      font-size: 20px;
      color: green;
    }

    :read-write {
      border: 1px solid orange;
      font-size: 18px;
    }
</style>
<input type="text" placeholder='text here'>
<input type="tel" placeholder='number here'>
<select>
      <option>1</option>
      <option>2</option>
</select>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/read-only-write-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/read-only-write.png

:placeholder-shown

:placeholder-shown 伪类 在 <input><textarea> 元素显示 placeholder text 时生效。

例子如下:

<style>
    input {
        border: 2px solid black;
        padding: 3px;
    }

    input:placeholder-shown {
        border-color: silver;
    }
</style>
<input placeholder="Type something here!">

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/placeholder-shown-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/placeholder-shown.png

:default

:default 伪类选择器 表示一组相关元素中的默认表单元素。

该选择器可以在 <button>, <input type="checkbox">, <input type="radio">, 以及 <option> 上使用。

例子如下:

<style>
    input:default {
          box-shadow: 0 0 2px 1px coral;
    }

    input:default + label {
          color: coral;
    }
</style>
<input type="radio" name="season" id="spring">
<label for="spring">Spring</label>

<input type="radio" name="season" id="summer" checked>
<label for="summer">Summer</label>

<input type="radio" name="season" id="fall">
<label for="fall">Fall</label>

<input type="radio" name="season" id="winter">
<label for="winter">Winter</label>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/default-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/default.png

:indeterminate

:indeterminate 伪类选择器表示状态不确定的表单元素。

它支持:

  • <input type="checkbox"> 元素,其 indeterminate 属性被JavaScript设置为 true
  • <input type="radio"> 元素, 表单中拥有相同 name值的所有单选按钮都未被选中时。
  • 处于不确定状态的 <progress> 元素

例子如下:

<style>
    input, span {
        background: red;
    }

    :indeterminate, :indeterminate + label {
        background: lime;
    }
    progress {
          margin: 4px;
    }
    progress:indeterminate {
          opacity: 0.5;
          background-color: lightgray;
          box-shadow: 0 0 2px 1px red;
    }
</style>
<div>
    <input type="checkbox" id="checkbox">
    <label for="checkbox">Background should be green</label>
</div>
<br />
<div>
    <input type="radio" id="radio">
    <label for="radio">Background should be green</label>
</div>
<br />
<progress></progress>
<script>
    'use strict'
    const inputs = document.querySelectorAll('input')
    inputs.forEach(input => {
        input.indeterminate = true
    })
</script>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/indeterminate-demo.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/indeterminate.png

:valid:invalid

判断有效性的伪类选择器(:valid:invalid)匹配有效或无效,<input><form>元素。

:valid伪类选择器表示值通过验证的<input>,这告诉用户他们的输入是有效的。

:invalid伪类选择器表示值不通过通过验证的<input>,这告诉用户他们的输入是无效的。

例子如下:

<style>
    input:valid {
        outline: 1px solid green;
    }

    input:invalid {
        outline: 1px solid red;
    }
</style>
输入文字:
<input type="text" pattern="[\w]+" required />
<br />
输入电话号码:
<input type="tel" pattern="[0-9]+" required />

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/valid-demo.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/valid.png

:in-range:out-of-range

如果一个时间或数字<input>具有maxmin属性,那么:in-range会匹配到输入值在指定范围内的<input>:out-of-input则匹配输入值不在指定范围的<input>。如果没有规定范围,则都不匹配。

例子如下:

<style>
    li {
        list-style: none;
        margin-bottom: 1em;
    }

    input {
        border: 1px solid black;
    }

    input:in-range {
        background-color: rgba(0, 255, 0, 0.25);
    }

    input:out-of-range {
        background-color: rgba(255, 0, 0, 0.25);
        border: 2px solid red;
    }

    input:in-range + label::after {
        content: 'okay.';
    }

    input:out-of-range + label::after {
        content: 'out of range!';
    }
</style>
<form action="" id="form1">
    <ul>Values between 1 and 10 are valid.
        <li>
            <input id="value1" name="value1" type="number" placeholder="1 to 10" min="1" max="10" value="12">
            <label for="value1">Your value is </label>
        </li>
    </ul>
</form>

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/in-out-range-demo.gif

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/in-out-range.png

:required:optional

伪类选择器:required:optional匹配了<input><select>, 或 <textarea>元素。

:required表示“必填”

:optional表示“可选”

例子如下:

<style>
    input:required {
        border: 1px solid orange;
    }
    input:optional {
        border: 1px solid green;
    }
</style>
必填的:<input type="text" required>
<br />
可选的:<input type="text">

效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/requirement-demo.png

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/optional.png

:required 的兼容性在上面有。

:blank

:blank 伪类选择器 用于匹配如下节点:

  1. 没有子节点;
  2. 仅有空的文本节点;
  3. 仅有空白符的文本节点。

有点类似于:empty,但是比:empty宽松,目前还是没有任何一款浏览器支持。

:user-invalid

:user-invalid伪类选择器匹配输入错误的元素。不过跟其它的输入伪类不同的是,它仅匹配用户输入时的错误,而不是静默状态下的错误,这样就会比较人性化,可惜,目前还是没有任何一款浏览器支持。

树型伪类(Tree-Structural pseudo-classes)

:nth-child:nth-last-child

:nth-child:nth-last-child并不是 Level 4 才推出的伪类选择器,但是在 Level 4 里 新增了在元素组里匹配的功能。

语法如下::nth-child/nth-last-child(An + B [of S] ?)

例子如下:

:nth-child(-n+3 of li.important)

上面的例子通过传递选择器参数,选择与之匹配的第n个元素,这里表示li.important中前三个子元素。

它跟以下规则不同:

li.important:nth-child(-n+3)

这里表示的时候如意前三个子元素刚才是li.important时才能被选择得到。

兼容性如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/selectors-4/list-nth-child.png

(鱼头注:牛皮,Safari居然弯道超车了,不过别的浏览器不支持,也没啥用...)

网格选择器(Grid-Structural Selectors)

||

|| 组合器选择属于某个表格行的节点。

例子如下:

<style>
    col.selected || td {
          background: gray;
          color: white;
          font-weight: bold;
    }
</style>
<table border="1">
      <colgroup>
            <col span="2"/>
            <col class="selected"/>
      </colgroup>
      <tbody>
            <tr>
                  <td>A
                  <td>B
                  <td>C
            </tr>
            <tr>
                  <td colspan="2">D</td>
                  <td>E</td>
            </tr>
            <tr>
                  <td>F</td>
                  <td colspan="2">G</td>
            </tr>
      </tbody>
</table>

上面的例子可以使C,E 与 G单元格变灰。

很可惜,目前还是没有任何浏览器给予支持。

:nth-col() :nth-last-col()

伪类选择器:nth-col() :nth-last-col()表示选择正向或反向的表格行的节点。

语法和:nth-child:nth-last-child类似,只不过它是选择表格内的元素。

目前还是没有任何浏览器支持。

最后

总结

以上便是CSS选择器 Level 4 里新出的所有选择器,其实都是非常有用的,虽然有些选择器的浏览器支持度并不乐观的。

希望各大浏览器厂商可以赶快增加对它们的支持吧。

参考资料

  1. can i use
  2. MDN
  3. Selectors Level 4 W3C Working Draft
查看原文

赞 30 收藏 20 评论 2

陈大鱼头 发布了文章 · 2020-07-14

二营长,快掏个CSS出来给我画个井字棋游戏

前言

不知道大家小时候有没有玩过一款游戏叫『井字棋』的。

它长这样:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/3.gif

(我赢了,快夸我 ~o(´^`)o)

上面的就是本次文章的最终结果,一个用纯CSS实现的AI井字棋游戏,Mmmm,虽然看起来有点蠢。。。

地址在此:

https://codepen.io/krischan77...

游戏的规则比较简单,就是在一个九宫格(据说十六宫格,二十五宫格也行~反正是格子就行),只要你下的棋能连成一条直线,就算赢。

所以这次鱼头就来教大家怎样才能在这个游戏中获胜。

额,不对,大雾呀~

是怎样通过纯CSS来实现上面这个游戏~

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/4.jpeg

正文

先手选择

通过开头的GIF图,我们可以看到其实这个游戏是有先手选择的。

我们可以选择是玩家先下,还是电脑先下。

那么如果通过单纯的HTML标签 + CSS属性,该如何完成呢?

首先我们转换下思路,先手选择不是“我方”跟“电脑方”的选择,而是“选择我”以及“不选择我”之间两种状态的切换,那么基于这个原理,我们就很快可以联想到<input type="checkbox"/>

有以下的效果:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/5.gif

但这里还有一个问题,就是虽然我们实现了双向选择的效果,但是开头的GIF图里先手选择是一个好看的 switch ,明显<input type="checkbox"/>无法实现这个功能,那怎么呢?

嗯,所以我们还是用JS模拟吧!

(吃瓜群众:说好的CSS呢?给我打)

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/7.gif

对不起,我们可以用<label>标签来模拟。

<label>标签可以通过for="#hash"来跟<input id="#hash">来进行关联,所以我们有以下效果:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/8.gif

源码如下:

<style>
    .switch {
        display: inline-block;
        width: 48px;
        height: 24px;
        background: #c4d7d6;
        vertical-align: bottom;
        margin: 0 10px;
        border-radius: 16px;
        position: relative;
        cursor: pointer;
    }
    .switch::before {
        content: '';
        position: absolute;
        display: block;
        width: 16px;
        height: 16px;
        top: 4px;
        left: 4px;
        background: #2e317c;
        border-radius: 100%;
        transition: all 0.25s;
    }
    #switch:checked ~ label[for='switch']::before {
        left: 28px;
        background: #863020;
    }
</style>
checkbox: <input type="checkbox" id="switch" />
<label for="switch" class="switch"></label>

然后我们再观察图1,可以发现,当我们选择时,是可以控制“ 电脑走 ”的按钮的。

那么这个又该怎么实现呢?

CSS实现不了,我们用JS吧。

(吃瓜群众:??????)

秋,秋,秋得嘛跌。CSS也可以实现!

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/9.jpg

我们看到上面的源码中有 ~ 这个选择器。

这玩意叫做“ 兄弟选择器 ”,可以选择同层级顺序排后的兄弟节点,而且不管距离由多远,总是心连心~。

例如有以下HTML结构:

<span>This is not red.</span>
<p>Here is a paragraph.</p>
<code>Here is some code.</code>
<span>And here is a span.</span>

以下CSS:

p ~ span {
  color: red;
}

这样一样可以选中<code>后面的<span>

所以我们有:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/10.gif

代码如下:

<style>
    #computer {
        width: 100px;
        display: inline-block;
        background: #131824;
        color: #eef7f2;
        border-radius: 5px;
        margin-top: 10px;
        padding: 5px;
        box-sizing: border-box;
        cursor: pointer;
        transition: all 0.25s;
    }
    #switch ~ #computer {
        display: none;
    }
    #switch:checked ~ #computer {
        display: block;
    }
</style>
checkbox: <input type="checkbox" id="switch" />
<label for="switch" class="switch"></label>
<div id="computer" class="computer">电脑走!</div>

选择完之后呢?

我们再回过头来看图1,选择先手的功能是以弹窗的形式出现的,就是为了确保选择先手之前不污染棋盘。所以这该怎么做呢?

通过上面的DEMO,我们发现有个:checked选择器,这个选择器任何可选元素的选中状态,例如<input type="radio"><input type="checkbox">以及<option>

所以我们有以下效果:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/11.gif

代码如下:

<style>
    .switch {
        display: inline-block;
        width: 48px;
        height: 24px;
        background: #c4d7d6;
        vertical-align: bottom;
        margin: 0 10px;
        border-radius: 16px;
        position: relative;
        cursor: pointer;
    }
    .switch::before {
        content: '';
        position: absolute;
        display: block;
        width: 16px;
        height: 16px;
        top: 4px;
        left: 4px;
        background: #2e317c;
        border-radius: 100%;
        transition: all 0.25s;
    }
    #switch:checked ~ label[for='switch']::before {
        left: 28px;
        background: #863020;
    }
    .btn {
        width: auto;
        display: inline-block;
        background: #131824;
        color: #eef7f2;
        border-radius: 5px;
        margin-top: 10px;
        padding: 5px;
        box-sizing: border-box;
        cursor: pointer;
        transition: all 0.25s;
    }
    #switch ~ #computer {
        display: none;
    }
    #switch:checked ~ #computer {
        display: inline-block;
    }
    #start:checked ~ .container {
        display: none;
    }
</style>
<input type="radio" id="start" />
checkbox: <input type="checkbox" id="switch" />
<div class="container">
    <br />
    <label for="switch" class="switch"></label>
    <br />
    <br />
    <label for="start" class="btn">皮皮虾,我们走</label>
</div>
<div id="computer" class="btn">电脑走!</div>

来画棋盘啦

接下来我们就是画棋盘,其实棋盘是个比较常规的九宫格,可以实现的方式有很多,不过这次鱼头要安利个gird布局在线生成的网站:http://grid.malven.co/

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/12.png

图一的DEMO布局就是用这个工具生成的,非常方便~

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/13.png

棋盘画好了,棋子呢?

好了,我们棋盘已经画好,那么棋子呢?

嗯,可以去文具店花15块钱买一盒黑白棋,然后就可以下了,好了,本文完结。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/14.jpeg

大雾啊~

有了棋盘我们就应该画棋子了,棋子该怎么画呢?

其实怎么画都不要紧,重要的是得保证每个格子都能下两方的棋子。

在我们画棋子之前我们先谈谈<input />的状态管理。

作为可替换元素的<input />,可真是个神器,因为有它以及后续浏览器对它功能的不断完善,所以也是变得越来越强大。

根据我们以往的开发经验以及上文的描述,我们很容易就能联系到两个存储正负状态的属性<input type="radio"><input type="checkbox">

以上两个不同属性的<input />都能存储选择状态。

唯一不同的是<input type="radio">选择状态本身是单向不可逆的,只有通过所关联的<input type="radio">才可以进行切换。

<input type="checkbox">则是双向可逆的,状态改变只在当前标签就可以完成。效果如下:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/15.gif

那么我们回到井字棋来。

我们棋盘的每个格子会有三种状态,一个是初始时,一个是我方落子,另一个是电脑落子。

如果以数字来表示,则有:

状态码含义
00无子
01我方落子
10电脑落子

结合上面的信息,我们不难选出<input type="radio">来画棋子,所以我们有:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/16.gif

所以思路就是每个格子放两个<input type="radio">,通过选择的一个标签来确定棋子内渲染的样式。棋子样式可以随自己美化,根据需求我们来画<label>就行。

所以我们棋盘的HTML就如下:

<form id="container" class="container">
    <input type="radio" name="c-radio-0" id="c-radio-0-X" />
    <input type="radio" name="c-radio-0" id="c-radio-0-O" />
    
    ......

    <div id="c-board" class="c-center">
        <div class="c-grid" id="c-grid-0">
            <label for="c-radio-0-X"></label>
            <div></div>
        </div>
        ......
    </div>
    <div id="c-computer" class="c-btn">
        电脑走!
        <label for="c-radio-0-O"></label>
        ......
    </div>

基本的棋盘布局就这么完成了,接下来就是下手规则的处理了。

来啦,互相伤害啊

那么下面我们就一步一步的解析落子程序。

首先我们来康康工具人标签:

<div class="c-grid" id="c-grid-0">
    <label for="c-radio-0-X"></label>
    <div></div>
</div>

通过上面我们不难知道<label for="c-radio-0-X"></label>就是落子标签,那么这个<div></div>是干啥的呢?

你可别看这个标签都没有,像个一无所有的舔狗一样,但是需要用到它的时候,它可以马上变成一个非常有用的工具人。

这个标签的作用就是用来承载落子的标记。

比如我们定义己方标签的id规则是input[id*='-编号-X'],电脑方是input[id*='-编号-0'],那么我们就可以通过 ~ 选择器来确定这个工具人渲染的样式,例如:

input[id*='-0-X']:checked~#c-board #c-grid-0 div::before {
    content: 'X';
    background: var(--color1);
    color: var(--color3);
}

input[id*='-0-O']:checked~#c-board #c-grid-0 div::before {
    content: 'O';
    background: var(--color2);
    color: var(--color3);
}

来到这里要格外提一点,每一个格子的input[id]都是 OX 两个的存在,而不是同一个的原因就是为了保证状态不可逆,当 checked 之后就不让它复原。

对,就是这样。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/17.jpg

我们确定了落子的渲染方式,接下来就是确定如何落子了。

我们知道,一个格子里可以渲染input[id*='-0-X']以及input[id*='-0-O'],我们也可以通过点击来确定渲染哪一个,可是我们如何确定点击的是哪个呢?

我们先来捋捋思路。

首先我方下棋,这没什么问题,就跟小X王学习机一样,哪里不懂点哪里就可以,so easy~

但是电脑方是由电脑控制,在本DEMO里,需要通过点击下方的“电脑走”按钮,来让它自动落子,所以最开始需要让它隐藏起来。

#c-computer { display: none; }

还有就是我方落完子之后,这个按钮需要出现,按了之后需要隐藏,所以我们只需要交替让它显示就可以,也就是这样:

#c-computer,
input:checked~input:checked~#c-computer,
input:checked~input:checked~input:checked~input:checked~#c-computer,
input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer,
input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer {
    display: none;
}

input:checked~#c-computer,
input:checked~input:checked~input:checked~#c-computer,
input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer,
input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer {
    display: block;
}

这里的意思就是我第一个:checked<input />后面的按钮要display: block,再来一个则要display: none起来,如此一个接着一个,一个接着一个,一个接着一个。。

电脑方落子位置

我方落子位置可以通过我们主动点击确定,那么电脑方呢?

毕竟是电脑,要是落子位置还要我们确定,那就尴大尬了。

首先我们来看下电脑方相关的HTML结构。

<div id="c-computer" class="c-btn">
    电脑走!
    <label for="c-radio-0-O"></label>
    <label for="c-radio-1-O"></label>
    <label for="c-radio-2-O"></label>
    <label for="c-radio-3-O"></label>
    <label for="c-radio-4-O"></label>
    <label for="c-radio-5-O"></label>
    <label for="c-radio-6-O"></label>
    <label for="c-radio-7-O"></label>
    <label for="c-radio-8-O"></label>
</div>

通过上面,我们可以发现,当我们点 “电脑走” 按钮时,实际上是点label[for$='-O']

但是label的层级结构也是确定的,那么不就很容易跟label[for$='-X']的位置冲突了吗?

既然我们这里提到了 “层级” ,那么我们不难想到,可以通过z-index来确定点击是的是哪个label

我们看实操栗子。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/18.gif

所以我们就可以控制每次电脑落子的位置。

怎么确定呢?

我们可以根据“ 玩家 ”的落子位置来确定。

比如玩家在“ 0号位置 ”已经有个:checked,那么我们就可以按照我们的想法来确定“ 电脑 ”的落子位置,以此类推。

例如这样:

#c-radio-0-X:checked~#c-radio-4-X:checked~#c-radio-8-O:checked~#c-computer label[for='c-radio-2-O'],
...... {
    z-index: 2;
}

#c-radio-0-O:not(:checked)~#c-radio-2-O:not(:checked)~#c-radio-4-X:checked~#c-radio-6-O:not(:checked)~#c-radio-8-O:not(:checked)~#c-computer label[for='c-radio-0-O'],
...... {
    z-index: 2;
}

输赢判断

好了,终于到了我们最后一个环节了,就是如何判断输赢。

这部分就是通过双方落子位置来确定。

众所周知,我们有以下几种赢法:

以字母“ X ”代表赢的规则:

<!--
XXX  OOO  OOO  XOO  OXO  OOX  XOO  OOX
OOO  XXX  OOO  XOO  OXO  OOX  OXO  OXO
OOO  OOO  xxx  XOO  OXO  OOX  OOX  XOO
-->

应该没有漏吧,就是以上几种,所以我们只需要判断双方的落子是否满足以上的规则即可,所以我们有:

#c-radio-0-X:checked~#c-radio-1-X:checked~#c-radio-2-X:checked~#c-result #c-info::before,
#c-radio-3-X:checked~#c-radio-4-X:checked~#c-radio-5-X:checked~#c-result #c-info::before,
#c-radio-6-X:checked~#c-radio-7-X:checked~#c-radio-8-X:checked~#c-result #c-info::before,
#c-radio-0-X:checked~#c-radio-3-X:checked~#c-radio-6-X:checked~#c-result #c-info::before,
#c-radio-1-X:checked~#c-radio-4-X:checked~#c-radio-7-X:checked~#c-result #c-info::before,
#c-radio-2-X:checked~#c-radio-5-X:checked~#c-radio-8-X:checked~#c-result #c-info::before,
#c-radio-0-X:checked~#c-radio-4-X:checked~#c-radio-8-X:checked~#c-result #c-info::before,
#c-radio-2-X:checked~#c-radio-4-X:checked~#c-radio-6-X:checked~#c-result #c-info::before {
    content: '恭喜你赢了~';
}

#c-radio-0-O:checked~#c-radio-1-O:checked~#c-radio-2-O:checked~#c-result #c-info::before,
#c-radio-3-O:checked~#c-radio-4-O:checked~#c-radio-5-O:checked~#c-result #c-info::before,
#c-radio-6-O:checked~#c-radio-7-O:checked~#c-radio-8-O:checked~#c-result #c-info::before,
#c-radio-0-O:checked~#c-radio-3-O:checked~#c-radio-6-O:checked~#c-result #c-info::before,
#c-radio-1-O:checked~#c-radio-4-O:checked~#c-radio-7-O:checked~#c-result #c-info::before,
#c-radio-2-O:checked~#c-radio-5-O:checked~#c-radio-8-O:checked~#c-result #c-info::before,
#c-radio-0-O:checked~#c-radio-4-O:checked~#c-radio-8-O:checked~#c-result #c-info::before,
#c-radio-2-O:checked~#c-radio-4-O:checked~#c-radio-6-O:checked~#c-result #c-info::before {
    content: '可惜你输了~';
}

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/19.jpg

(吃瓜群众:“完美个头,要是没输没赢呢?”)

要是没输没赢,没输没赢,没输没赢,该怎么办呢?没办法了,用JS吧。。。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/20.jpg

对不起,我错了,这个功能只需要给这个提示标签一个默认文本即可。

当然我们得写个让提示弹窗出现的逻辑。

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-result,
...... {
    display: block;
}

就是全部空格都:checked以及几个关键空格占满的时候,就让它展示。

初始化

如果我们想玩下一盘该怎么办?

刷新页面啊!!!

(吃瓜群众:“就这?”)

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/21.jpg

当然不是就这啊,接下来要给大家介绍最后一个姿势:<input type="reset">

<input type="reset">呈按钮状,可以一键初始化表单内所有的<input />,就像这样

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/22.gif

一键初始化,非常方便~

结语

<input />是一个非常有用且有趣的可替换标签,业界中大部分的纯CSS游戏差不多都是用它来完成的,虽然不是特别实用,但是结合选择器,是可以帮助我们在业务中解决很多问题的。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/23.jpg

参考资料

  1. 纯 CSS 井字棋:并不神秘的 CSS AI 编程之旅
查看原文

赞 10 收藏 6 评论 1

陈大鱼头 发布了文章 · 2020-07-01

从ES6到ES10的新特性万字大总结(不得不收藏)

介绍

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会)在标准ECMA-262中定义的脚本语言规范。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。

历史版本

至发稿日为止有九个ECMA-262版本发表。其历史版本如下:

  1. 1997年6月:第一版
  2. 1998年6月:修改格式,使其与ISO/IEC16262国际标准一样
  3. 1999年12月:强大的正则表达式,更好的词法作用域链处理,新的控制指令,异常处理,错误定义更加明确,数据输出的格式化及其它改变
  4. 2009年12月:添加严格模式("use strict")。修改了前面版本模糊不清的概念。增加了getters,setters,JSON以及在对象属性上更完整的反射。
  5. 2011年6月:ECMAScript标5.1版形式上完全一致于国际标准ISO/IEC 16262:2011。
  6. 2015年6月:ECMAScript 2015(ES2015),第 6 版,最早被称作是 ECMAScript 6(ES6),添加了类和模块的语法,其他特性包括迭代器,Python风格的生成器和生成器表达式,箭头函数,二进制数据,静态类型数组,集合(maps,sets 和 weak maps),promise,reflection 和 proxies。作为最早的 ECMAScript Harmony 版本,也被叫做ES6 Harmony。
  7. 2016年6月:ECMAScript 2016(ES2016),第 7 版,多个新的概念和语言特性。
  8. 2017年6月:ECMAScript 2017(ES2017),第 8 版,多个新的概念和语言特性。
  9. 2018年6月:ECMAScript 2018 (ES2018),第 9 版,包含了异步循环,生成器,新的正则表达式特性和 rest/spread 语法。
  10. 2019年6月:ECMAScript 2019 (ES2019),第 10 版。

发展标准

TC39(Technical Committee 39)是一个推动JavaScript发展的委员会,它的成语来自各个主流浏览器的代表成语。会议实行多数决,每一项决策只有大部分人同意且没有强烈反对才能去实现。

TC39成员制定着ECMAScript的未来。

每一项新特性最终要进入到ECMAScript规范里,需要经历5个阶段,这5个阶段如下:

  • Stage 0: Strawperson

    只要是TC39成员或者贡献者,都可以提交想法

  • Stage 1: Proposal

    这个阶段确定一个正式的提案

  • Stage 2: draft

    规范的第一个版本,进入此阶段的提案大概率会成为标准

  • Stage 3: Candidate

    进一步完善提案细则

  • Stage 4: Finished

    表示已准备好将其添加到正式的ECMAScript标准中

由于ES6以前的属性诞生年底久远,我们使用也比较普遍,遂不进行说明,ES6之后的语言风格跟ES5以前的差异比较大,所以单独拎出来做个记录。

ES6(ES2015)

ES6是一次重大的革新,比起过去的版本,改动比较大,本文仅对常用的API以及语法糖进行讲解。

Let 和 Const

在ES6以前,JS只有var一种声明方式,但是在ES6之后,就多了letconst这两种方式。用var定义的变量没有块级作用域的概念,而letconst则会有,因为这三个关键字创建是不一样的。

区别如下:

{
    var a = 10
    let b = 20
    const c = 30
}
a // 10
b // Uncaught ReferenceError: b is not defined
c // c is not defined
let d = 40
const e = 50
d = 60
d // 60
e = 70 // VM231:1 Uncaught TypeError: Assignment to constant variable.
varletconst
变量提升××
全局变量××
重复声明××
重新赋值×
暂时死区×
块作用域×
只声明不初始化×

类(Class)

在ES6之前,如果我们要生成一个实例对象,传统的方法就是写一个构造函数,例子如下:

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.information = function () {
    return 'My name is ' + this.name + ', I am ' + this.age
}

但是在ES6之后,我们只需要写成以下形式:

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    information() {
        return 'My name is ' + this.name + ', I am ' + this.age
    }
}

箭头函数(Arrow function)

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

在ES6以前,我们写函数一般是:

var list = [1, 2, 3, 4, 5, 6, 7]
var newList = list.map(function (item) {
    return item * item
})

但是在ES6里,我们可以:

const list = [1, 2, 3, 4, 5, 6, 7]
const newList = list.map(item => item * item)

看,是不是简洁了不少

函数参数默认值(Function parameter defaults)

在ES6之前,如果我们写函数需要定义初始值的时候,需要这么写:

function config (data) {
    var data = data || 'data is empty'
}

这样看起来也没有问题,但是如果参数的布尔值为falsy时就会出问题,例如我们这样调用config:

config(0)
config('')

那么结果就永远是后面的值

如果我们用函数参数默认值就没有这个问题,写法如下:

const config = (data = 'data is empty') => {}

模板字符串(Template string)

在ES6之前,如果我们要拼接字符串,则需要像这样:

var name = 'kris'
var age = 24
var info = 'My name is ' + this.name + ', I am ' + this.age

但是在ES6之后,我们只需要写成以下形式:

const name = 'kris'
const age = 24
const info = `My name is ${name}, I am ${age}`

解构赋值(Destructuring assignment)

我们通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

比如我们需要交换两个变量的值,在ES6之前我们可能需要:

var a = 10
var b = 20
var temp = a
a = b
b = temp

但是在ES6里,我们有:

let a = 10
let b = 20
[a, b] = [b, a]

是不是方便很多

模块化(Module)

在ES6之前,JS并没有模块化的概念,有的也只是社区定制的类似CommonJS和AMD之类的规则。例如基于CommonJS的NodeJS:

// circle.js
// 输出
const { PI } = Math
exports.area = (r) => PI * r ** 2
exports.circumference = (r) => 2 * PI * r

// index.js
// 输入
const circle = require('./circle.js')
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`)

在ES6之后我们则可以写成以下形式:

// circle.js
// 输出
const { PI } = Math
export const area = (r) => PI * r ** 2
export const circumference = (r) => 2 * PI * r

// index.js
// 输入
import {
    area
} = './circle.js'
console.log(`半径为 4 的圆的面积是: ${area(4)}`)

扩展操作符(Spread operator)

扩展操作符可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

比如在ES5的时候,我们要对一个数组的元素进行相加,在不使用reduce或者reduceRight的场合,我们需要:

function sum(x, y, z) {
    return x + y + z;
}
var list = [5, 6, 7]
var total = sum.apply(null, list)

但是如果我们使用扩展操作符,只需要如下:

const sum = (x, y, z) => x + y + z
const list = [5, 6, 7]
const total = sum(...list)

非常的简单,但是要注意的是扩展操作符只能用于可迭代对象

如果是下面的情况,是会报错的:

var obj = {'key1': 'value1'}
var array = [...obj] // TypeError: obj is not iterable

对象属性简写(Object attribute shorthand)

在ES6之前,如果我们要将某个变量赋值为同样名称的对象元素,则需要:

var cat = 'Miaow'
var dog = 'Woof'
var bird = 'Peet peet'

var someObject = {
  cat: cat,
  dog: dog,
  bird: bird
}

但是在ES6里我们就方便很多:

let cat = 'Miaow'
let dog = 'Woof'
let bird = 'Peet peet'

let someObject = {
  cat,
  dog,
  bird
}

console.log(someObject)

//{
//  cat: "Miaow",
//  dog: "Woof",
//  bird: "Peet peet"
//}

非常方便

Promise

Promise 是ES6提供的一种异步解决方案,比回调函数更加清晰明了。

Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

new Promise((resolve, reject) => {
  resolve('success')
  // 无效
  reject('reject')
})

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

当然了,Promise 也很好地解决了回调地狱的问题,例如:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

可以改写成:

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

for...of

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

例子如下:

const array1 = ['a', 'b', 'c'];

for (const element of array1) {
      console.log(element)
}

// "a"
// "b"
// "c"

Symbol

symbol 是一种基本数据类型,Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

例子如下:

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

迭代器(Iterator)/ 生成器(Generator)

迭代器(Iterator)是一种迭代的机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要内部有 Iterator 接口,就可以完成依次迭代操作。

一旦创建,迭代器对象可以通过重复调用next()显式地迭代,从而获取该对象每一级的值,直到迭代完,返回{ value: undefined, done: true }

虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用 function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。

可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。

所以我们可以有以下例子:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}

Set/WeakSet

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

所以我们可以通过Set实现数组去重

const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)]) 
// [2, 3, 4, 5, 6, 7, 32]

WeakSet 结构与 Set 类似,但区别有以下两点:

  • WeakSet 对象中只能存放对象引用, 不能存放值, 而 Set 对象都可以。
  • WeakSet 对象中存储的对象值都是被弱引用的, 如果没有其他的变量或属性引用这个对象值, 则这个对象值会被当成垃圾回收掉. 正因为这样, WeakSet 对象是无法被枚举的, 没有办法拿到它包含的所有元素。

所以代码如下:

var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo)    // false, 对象 foo 并没有被添加进 ws 中 

ws.delete(window) // 从集合中删除 window 对象
ws.has(window)    // false, window 对象已经被删除了

ws.clear() // 清空整个 WeakSet 对象

Map/WeakMap

Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

例子如下,我们甚至可以使用NaN来作为键值:

var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN); // "not a number"

var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number"

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

Map的区别与SetWeakSet的区别相似,具体代码如下:

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37
wm3.clear();
wm3.get(o1); // undefined,wm3已被清空
wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false

Proxy/Reflect

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

ProxyReflect是非常完美的配合,例子如下:

const observe = (data, callback) => {
      return new Proxy(data, {
            get(target, key) {
                return Reflect.get(target, key)
            },
            set(target, key, value, proxy) {
                  callback(key, value);
                  target[key] = value;
                    return Reflect.set(target, key, value, proxy)
            }
      })
}

const FooBar = { open: false };
const FooBarObserver = observe(FooBar, (property, value) => {
  property === 'open' && value 
          ? console.log('FooBar is open!!!') 
          : console.log('keep waiting');
});
console.log(FooBarObserver.open) // false
FooBarObserver.open = true // FooBar is open!!!

当然也不是什么都可以被代理的,如果对象带有configurable: falsewritable: false 属性,则代理失效。

Regex对象的扩展

正则新增符号

  • i 修饰符

    // i 修饰符
    /[a-z]/i.test('\u212A') // false
    /[a-z]/iu.test('\u212A') // true
  • y修饰符

    // y修饰符
    var s = 'aaa_aa_a';
    var r1 = /a+/g;
    var r2 = /a+/y;
    
    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]
    
    r1.exec(s) // ["aa"]
    r2.exec(s) // null
  • String.prototype.flags

    // 查看RegExp构造函数的修饰符
    var regex = new RegExp('xyz', 'i')
    regex.flags // 'i'
  • unicode模式

    var s = '𠮷'
    /^.$/.test(s) // false
    /^.$/u.test(s) // true
  • u转义

    // u转义
    /\,/ // /\,/
    /\,/u // 报错 没有u修饰符时,逗号前面的反斜杠是无效的,加了u修饰符就报错。
  • 引用

    const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false
    
    const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false

字符串方法的实现改为调用RegExp方法

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

正则新增属性

  • RegExp.prototype.sticky 表示是否有y修饰符

    /hello\d/y.sticky // true
  • RegExp.prototype.flags获取修饰符

    /abc/ig.flags // 'gi'

Math对象的扩展

  • 二进制表示法 : 0b或0B开头表示二进制(0bXX0BXX)
  • 二进制表示法 : 0b或0B开头表示二进制(0bXX0BXX)
  • 八进制表示法 : 0o或0O开头表示二进制(0oXX0OXX)
  • Number.EPSILON : 数值最小精度
  • Number.MIN_SAFE_INTEGER : 最小安全数值(-2^53)
  • Number.MAX_SAFE_INTEGER : 最大安全数值(2^53)
  • Number.parseInt() : 返回转换值的整数部分
  • Number.parseFloat() : 返回转换值的浮点数部分
  • Number.isFinite() : 是否为有限数值
  • Number.isNaN() : 是否为NaN
  • Number.isInteger() : 是否为整数
  • Number.isSafeInteger() : 是否在数值安全范围内
  • Math.trunc() : 返回数值整数部分
  • Math.sign() : 返回数值类型(正数1负数-1零0)
  • Math.cbrt() : 返回数值立方根
  • Math.clz32() : 返回数值的32位无符号整数形式
  • Math.imul() : 返回两个数值相乘
  • Math.fround() : 返回数值的32位单精度浮点数形式
  • Math.hypot() : 返回所有数值平方和的平方根
  • Math.expm1() : 返回e^n - 1
  • Math.log1p() : 返回1 + n的自然对数(Math.log(1 + n))
  • Math.log10() : 返回以10为底的n的对数
  • Math.log2() : 返回以2为底的n的对数
  • Math.sinh() : 返回n的双曲正弦
  • Math.cosh() : 返回n的双曲余弦
  • Math.tanh() : 返回n的双曲正切
  • Math.asinh() : 返回n的反双曲正弦
  • Math.acosh() : 返回n的反双曲余弦
  • Math.atanh() : 返回n的反双曲正切

Array对象的扩展

  • Array.prototype.from:转换具有Iterator接口的数据结构为真正数组,返回新数组。

    console.log(Array.from('foo')) // ["f", "o", "o"]
    console.log(Array.from([1, 2, 3], x => x + x)) // [2, 4, 6]
  • Array.prototype.of():转换一组值为真正数组,返回新数组。

    Array.of(7)       // [7] 
    Array.of(1, 2, 3) // [1, 2, 3]
    
    Array(7)          // [empty, empty, empty, empty, empty, empty]
    Array(1, 2, 3)    // [1, 2, 3]
  • Array.prototype.copyWithin():把指定位置的成员复制到其他位置,返回原数组

    const array1 = ['a', 'b', 'c', 'd', 'e']
    
    console.log(array1.copyWithin(0, 3, 4)) // ["d", "b", "c", "d", "e"]
    
    console.log(array1.copyWithin(1, 3)) // ["d", "d", "e", "d", "e"]
  • Array.prototype.find():返回第一个符合条件的成员

    const array1 = [5, 12, 8, 130, 44]
    
    const found = array1.find(element => element > 10)
    
    console.log(found) // 12
  • Array.prototype.findIndex():返回第一个符合条件的成员索引值

    const array1 = [5, 12, 8, 130, 44]
    
    const isLargeNumber = (element) => element > 13
    
    console.log(array1.findIndex(isLargeNumber)) // 3
  • Array.prototype.fill():根据指定值填充整个数组,返回原数组

    const array1 = [1, 2, 3, 4]
    
    console.log(array1.fill(0, 2, 4)) // [1, 2, 0, 0]
    
    console.log(array1.fill(5, 1)) // [1, 5, 5, 5]
    
    console.log(array1.fill(6)) // [6, 6, 6, 6]
  • Array.prototype.keys():返回以索引值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.keys()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // 0
    // 1
    // 2
  • Array.prototype.values():返回以属性值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.values()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // a
    // b
    // c
  • Array.prototype.entries():返回以索引值和属性值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.entries()
    
    console.log(iterator.next().value) // [0, "a"]
    console.log(iterator.next().value) // [1, "b"]
  • 数组空位:ES6明确将数组空位转为undefined或者empty

    Array.from(['a',,'b']) // [ "a", undefined, "b" ]
    [...['a',,'b']] // [ "a", undefined, "b" ]
    Array(3) //  [empty × 3]
    [,'a'] // [empty, "a"]

ES7(ES2016)

Array.prototype.includes()

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

代码如下:

const array1 = [1, 2, 3]
console.log(array1.includes(2)) // true

const pets = ['cat', 'dog', 'bat']
console.log(pets.includes('cat')) // true
console.log(pets.includes('at')) // false

幂运算符**

幂运算符**,具有与Math.pow()一样的功能,代码如下:

console.log(2**10) // 1024
console.log(Math.pow(2, 10)) // 1024

模板字符串(Template string)

自ES7起,带标签的模版字面量遵守以下转义序列的规则:

  • Unicode字符以"u"开头,例如\u00A9
  • Unicode码位用"u{}"表示,例如\u{2F804}
  • 十六进制以"x"开头,例如\xA9
  • 八进制以""和数字开头,例如\251

这表示类似下面这种带标签的模版是有问题的,因为对于每一个ECMAScript语法,解析器都会去查找有效的转义序列,但是只能得到这是一个形式错误的语法:

latex`\unicode`
// 在较老的ECMAScript版本中报错(ES2016及更早)
// SyntaxError: malformed Unicode character escape sequence

ES8(ES2017)

async/await

虽然Promise可以解决回调地狱的问题,但是链式调用太多,则会变成另一种形式的回调地狱 —— 面条地狱,所以在ES8里则出现了Promise的语法糖async/await,专门解决这个问题。

我们先看一下下面的Promise代码:

fetch('coffee.jpg')
    .then(response => response.blob())
    .then(myBlob => {
          let objectURL = URL.createObjectURL(myBlob)
          let image = document.createElement('img')
          image.src = objectURL
          document.body.appendChild(image)
    })
    .catch(e => {
          console.log('There has been a problem with your fetch operation: ' + e.message)
    })

然后再看看async/await版的,这样看起来是不是更清晰了。

async function myFetch() {
      let response = await fetch('coffee.jpg')
      let myBlob = await response.blob()

      let objectURL = URL.createObjectURL(myBlob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
}

myFetch()

当然,如果你喜欢,你甚至可以两者混用

async function myFetch() {
      let response = await fetch('coffee.jpg')
      return await response.blob()
}

myFetch().then((blob) => {
      let objectURL = URL.createObjectURL(blob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
})

Object.values()

Object.values()方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。

代码如下:

const object1 = {
      a: 'somestring',
      b: 42,
      c: false
}
console.log(Object.values(object1)) // ["somestring", 42, false]

Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。

代码如下:

const object1 = {
      a: 'somestring',
      b: 42
}

for (let [key, value] of Object.entries(object1)) {
      console.log(`${key}: ${value}`)
}

// "a: somestring"
// "b: 42"

padStart()

padStart() 方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。填充从当前字符串的开始(左侧)应用的。

代码如下:

const str1 = '5'
console.log(str1.padStart(2, '0')) // "05"

const fullNumber = '2034399002125581'
const last4Digits = fullNumber.slice(-4)
const maskedNumber = last4Digits.padStart(fullNumber.length, '*') 
console.log(maskedNumber) // "************5581"

padEnd()

padEnd() 方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。

const str1 = 'Breaded Mushrooms'
console.log(str1.padEnd(25, '.')) // "Breaded Mushrooms........"
const str2 = '200'
console.log(str2.padEnd(5)) // "200  "

函数参数结尾逗号(Function parameter lists and calls trailing commas)

在ES5里就添加了对象的尾逗号,不过并不支持函数参数,但是在ES8之后,便开始支持这一特性,代码如下:

// 参数定义
function f(p) {}
function f(p,) {} 

(p) => {}
(p,) => {}

class C {
  one(a,) {},
  two(a, b,) {},
}

var obj = {
  one(a,) {},
  two(a, b,) {},
};

// 函数调用
f(p)
f(p,)

Math.max(10, 20)
Math.max(10, 20,)

但是以下的方式是不合法的:

仅仅包含逗号的函数参数定义或者函数调用会抛出 SyntaxError。 而且,当使用剩余参数的时候,并不支持尾后逗号,例子如下:

function f(,) {} // SyntaxError: missing formal parameter
(,) => {}       // SyntaxError: expected expression, got ','
f(,)             // SyntaxError: expected expression, got ','

function f(...p,) {} // SyntaxError: parameter after rest parameter
(...p,) => {}        // SyntaxError: expected closing parenthesis, got ','

在解构里也可以使用,代码如下:

// 带有尾后逗号的数组解构
[a, b,] = [1, 2]

// 带有尾后逗号的对象解构
var o = {
  p: 42, 
  q: true,
}
var {p, q,} = o

同样地,在使用剩余参数时,会抛出 SyntaxError,代码如下:

var [a, ...b,] = [1, 2, 3] // SyntaxError: rest element may not have a trailing comma

ShareArrayBuffer(因安全问题,暂时在Chrome跟FireFox中被禁用)

SharedArrayBuffer 对象用来表示一个通用的,固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。

代码如下:

let sab = new SharedArrayBuffer(1024) // 必须实例化
worker.postMessage(sab)

Atomics对象

Atomics对象 提供了一组静态方法用来对 SharedArrayBuffer 对象进行原子操作。

方法如下:

  • Atomics.add() :将指定位置上的数组元素与给定的值相加,并返回相加前该元素的值。
  • Atomics.and():将指定位置上的数组元素与给定的值相与,并返回与操作前该元素的值。
  • Atomics.compareExchange():如果数组中指定的元素与给定的值相等,则将其更新为新的值,并返回该元素原先的值。
  • Atomics.exchange():将数组中指定的元素更新为给定的值,并返回该元素更新前的值。
  • Atomics.load():返回数组中指定元素的值。
  • Atomics.or():将指定位置上的数组元素与给定的值相或,并返回或操作前该元素的值。
  • Atomics.store():将数组中指定的元素设置为给定的值,并返回该值。
  • Atomics.sub():将指定位置上的数组元素与给定的值相减,并返回相减前该元素的值。
  • Atomics.xor():将指定位置上的数组元素与给定的值相异或,并返回异或操作前该元素的值。
  • Atomics.wait():检测数组中某个指定位置上的值是否仍然是给定值,是则保持挂起直到被唤醒或超时。返回值为 "ok"、"not-equal" 或 "time-out"。调用时,如果当前线程不允许阻塞,则会抛出异常(大多数浏览器都不允许在主线程中调用 wait())。
  • Atomics.wake():唤醒等待队列中正在数组指定位置的元素上等待的线程。返回值为成功唤醒的线程数量。
  • Atomics.isLockFree(size):可以用来检测当前系统是否支持硬件级的原子操作。对于指定大小的数组,如果当前系统支持硬件级的原子操作,则返回 true;否则就意味着对于该数组,Atomics 对象中的各原子操作都只能用锁来实现。此函数面向的是技术专家。

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。代码如下:

const object1 = {
  property1: 42
}

const descriptors1 = Object.getOwnPropertyDescriptors(object1)

console.log(descriptors1.property1.writable) // true

console.log(descriptors1.property1.value) // 42

// 浅拷贝一个对象
Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj) 
)

// 创建子类
function superclass() {}
superclass.prototype = {
  // 在这里定义方法和属性
}
function subclass() {}
subclass.prototype = Object.create(superclass.prototype, Object.getOwnPropertyDescriptors({
  // 在这里定义方法和属性
}))

ES9(ES2018)

for await...of

for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 StringArrayArray-like 对象(比如arguments 或者NodeList),TypedArrayMapSet和自定义的异步或者同步可迭代对象。其会调用自定义迭代钩子,并为每个不同属性的值执行语句。

配合迭代异步生成器,例子如下:

async function* asyncGenerator() {
      var i = 0
      while (i < 3) {
            yield i++
      }
}

(async function() {
      for await (num of asyncGenerator()) {
            console.log(num)
      }
})()
// 0
// 1
// 2

模板字符串(Template string)

ES9开始,模板字符串允许嵌套支持常见转义序列,移除对ECMAScript在带标签的模版字符串中转义序列的语法限制。

不过,非法转义序列在"cooked"当中仍然会体现出来。它们将以undefined元素的形式存在于"cooked"之中,代码如下:

function latex(str) { 
 return { "cooked": str[0], "raw": str.raw[0] }
} 

latex`\unicode` // { cooked: undefined, raw: "\\unicode" }

正则表达式反向(lookbehind)断言

首先我们得先知道什么是断言(Assertion)

断言(Assertion)是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。

正则表达式的断言一共有 4 种形式:

  • (?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)
  • (?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)
  • (?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)

在ES9之前,JavaScript 正则表达式,只支持正向断言。正向断言的意思是:当前位置后面的字符串应该满足断言,但是并不捕获。例子如下:

'fishHeadfishTail'.match(/fish(?=Head)/g) // ["fish"]

反向断言和正向断言的行为一样,只是方向相反。例子如下:

'abc123'.match(/(?<=(\d+)(\d+))$/) //  ["", "1", "23", index: 6, input: "abc123", groups: undefined]

正则表达式 Unicode 转义

正则表达式中的Unicode转义符允许根据Unicode字符属性匹配Unicode字符。 它允许区分字符类型,例如大写和小写字母,数学符号和标点符号。

部分例子代码如下:

// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true

// 匹配所有空格
\p{White_Space}

// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配 Emoji
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

具体的属性列表可查看:https://developer.mozilla.org...

正则表达式 s/dotAll 模式

在以往的版本里,JS的正则的.只能匹配emoji跟行终结符以外的所有文本,例如:

let regex = /./;

regex.test('\n');       // false
regex.test('\r');       // false
regex.test('\u{2028}'); // false
regex.test('\u{2029}'); // false

regex.test('\v');       // true
regex.test('\f');       // true
regex.test('\u{0085}'); // true

/foo.bar/.test('foo\nbar');     // false
/foo[^]bar/.test('foo\nbar');   // true

/foo.bar/.test('foo\nbar');     // false
/foo[\s]bar/.test('foo\nbar');   // true

但是在ES9之后,JS正则增加了一个新的标志 s 用来表示 dotAll,这可以匹配任意字符。代码如下:

/foo.bar/s.test('foo\nbar');    // true

const re = /foo.bar/s;  //  等价于 const re = new RegExp('foo.bar', 's');
re.test('foo\nbar');    // true
re.dotAll;      // true
re.flags;       // "s"

正则表达式命名捕获组

在以往的版本里,JS的正则分组是无法命名的,所以容易混淆。例如下面获取年月日的例子,很容易让人搞不清哪个是月份,哪个是年份:

const matched = /(\d{4})-(\d{2})-(\d{2})/.exec('2019-01-01')
console.log(matched[0]);    // 2019-01-01
console.log(matched[1]);    // 2019
console.log(matched[2]);    // 01
console.log(matched[3]);    // 01

ES9引入了命名捕获组,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。代码如下:

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined
'as' in matchObj.groups // true

对象扩展操作符

ES6中添加了数组的扩展操作符,让我们在操作数组时更加简便,美中不足的是并不支持对象扩展操作符,但是在ES9开始,这一功能也得到了支持,例如:

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

上面便是一个简便的浅拷贝。这里有一点小提示,就是Object.assign() 函数会触发 setters,而展开语法则不会。所以不能替换也不能模拟Object.assign()

如果存在相同的属性名,只有最后一个会生效。

Promise.prototype.finally()

finally()方法会返回一个Promise,当promise的状态变更,不管是变成rejected或者fulfilled,最终都会执行finally()的回调。

例子如下:

fetch(url)
      .then((res) => {
        console.log(res)
      })
      .catch((error) => { 
        console.log(error)
      })
      .finally(() => { 
        console.log('结束')
    })

ES10(ES2019)

Array.prototype.flat() / flatMap()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

flatMap()map() 方法和深度为1的 flat() 几乎相同.,不过它会首先使用映射函数映射每个元素,然后将结果压缩成一个新数组,这样效率会更高。

例子如下:

var arr1 = [1, 2, 3, 4]

arr1.map(x => [x * 2]) // [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]) // [2, 4, 6, 8]

// 深度为1
arr1.flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]]

flatMap()可以代替reduce()concat(),例子如下:

var arr = [1, 2, 3, 4]
arr.flatMap(x => [x, x * 2]) // [1, 2, 2, 4, 3, 6, 4, 8]
// 等价于
arr.reduce((acc, x) => acc.concat([x, x * 2]), []) // [1, 2, 2, 4, 3, 6, 4, 8]

但这是非常低效的,在每次迭代中,它创建一个必须被垃圾收集的新临时数组,并且它将元素从当前的累加器数组复制到一个新的数组中,而不是将新的元素添加到现有的数组中。

String.prototype.trimStart() / trimLeft() / trimEnd() / trimRight()

在ES5中,我们可以通过trim()来去掉字符首尾的空格,但是却无法只去掉单边的,但是在ES10之后,我们可以实现这个功能。

如果我们要去掉开头的空格,可以使用trimStart()或者它的别名trimLeft()

同样的,如果我们要去掉结尾的空格,我们可以使用trimEnd()或者它的别名trimRight()

例子如下:

const Str = '   Hello world!  '
console.log(Str) // '   Hello world!  '
console.log(Str.trimStart()) // 'Hello world!  '
console.log(Str.trimLeft()) // 'Hello world!  '
console.log(Str.trimEnd()) // '   Hello world!'
console.log(Str.trimRight()) // '   Hello world!'

不过这里有一点要注意的是,trimStart()trimEnd()才是标准方法,trimLeft()trimRight()只是别名。

在某些引擎里(例如Chrome),有以下的等式:

String.prototype.trimLeft.name === "trimStart"

String.prototype.trimRight.name === "trimEnd"

Object.fromEntries()

Object.fromEntries() 方法把键值对列表转换为一个对象,它是Object.entries()的反函数。

例子如下:

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])

const obj = Object.fromEntries(entries)

console.log(obj) // Object { foo: "bar", baz: 42 }

Symbol.prototype.description

description 是一个只读属性,它会返回Symbol对象的可选描述的字符串。与 Symbol.prototype.toString() 不同的是它不会包含Symbol()的字符串。例子如下:

Symbol('desc').toString();   // "Symbol(desc)"
Symbol('desc').description;  // "desc"
Symbol('').description;      // ""
Symbol().description;        // undefined

// 具名 symbols
Symbol.iterator.toString();  // "Symbol(Symbol.iterator)"
Symbol.iterator.description; // "Symbol.iterator"

//全局 symbols
Symbol.for('foo').toString();  // "Symbol(foo)"
Symbol.for('foo').description; // "foo"

String.prototype.matchAll

matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。并且返回一个不可重启的迭代器。例子如下:

var regexp = /t(e)(st(\d?))/g
var str = 'test1test2'

str.match(regexp) // ['test1', 'test2']
str.matchAll(regexp) // RegExpStringIterator {}
[...str.matchAll(regexp)] // [['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4], ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]]

Function.prototype.toString() 返回注释与空格

在以往的版本中,Function.prototype.toString()得到的字符串是去掉空白符号的,但是从ES10开始会保留这些空格,如果是原生函数则返回你控制台看到的效果,例子如下:

function sum(a, b) {
      return a + b;
}

console.log(sum.toString())
// "function sum(a, b) {
//         return a + b;
//  }"

console.log(Math.abs.toString()) // "function abs() { [native code] }"

try-catch

在以往的版本中,try-catchcatch后面必须带异常参数,例如:

    // ES10之前
try {
      // tryCode
} catch (err) {
      // catchCode
}

但是在ES10之后,这个参数却不是必须的,如果用不到,我们可以不用传,例如:

try {
      console.log('Foobar')
} catch {
      console.error('Bar')
}

BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

在以往的版本中,我们有以下的弊端:

// 大于2的53次方的整数,无法保持精度
2 ** 53 === (2 ** 53 + 1)
// 超过2的1024次方的数值,无法表示
2 ** 1024 // Infinity

但是在ES10引入BigInt之后,这个问题便得到了解决。

以下操作符可以和 BigInt 一起使用: +*-**% 。除 >>> (无符号右移)之外的位操作也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigIntBigInt 不支持单目 (+) 运算符。

/ 操作符对于整数的运算也没问题。可是因为这些变量是 BigInt 而不是 BigDecimal ,该操作符结果会向零取整,也就是说不会返回小数部分。

BigIntNumber不是严格相等的,但是宽松相等的。

所以在BigInt出来以后,JS的原始类型便增加到了7个,如下:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ES6)
  • BigInt (ES10)

globalThis

globalThis属性包含类似于全局对象 this值。所以在全局环境下,我们有:

globalThis === this // true

import()

静态的import 语句用于导入由另一个模块导出的绑定。无论是否声明了 严格模式,导入的模块都运行在严格模式下。在浏览器中,import 语句只能在声明了 type="module"script 的标签中使用。

但是在ES10之后,我们有动态 import(),它不需要依赖 type="module" 的script标签。

所以我们有以下例子:

const main = document.querySelector("main")
for (const link of document.querySelectorAll("nav > a")) {
      link.addEventListener("click", e => {
            e.preventDefault()

            import('/modules/my-module.js')
              .then(module => {
                    module.loadPageInto(main);
              })
              .catch(err => {
                    main.textContent = err.message;
              })
      })
}

私有元素与方法

在ES10之前,如果我们要实现一个简单的计数器组件,我们可能会这么写:

// web component 写法
class Counter extends HTMLElement {
      get x() { 
              return this.xValue
      }
      set x(value) {
              this.xValue = value
              window.requestAnimationFrame(this.render.bind(this))
      }

      clicked() {
            this.x++
      }

      constructor() {
            super()
            this.onclick = this.clicked.bind(this)
            this.xValue = 0
      }

      connectedCallback() { 
              this.render()
      }

      render() {
            this.textContent = this.x.toString()
      }
}
window.customElements.define('num-counter', Counter)

但是在ES10之后我们可以使用私有变量进行组件封装,如下:

class Counter extends HTMLElement {
      #xValue = 0

      get #x() { 
          return #xValue
      }
      set #x(value) {
            this.#xValue = value
            window.requestAnimationFrame(this.#render.bind(this))
      }

      #clicked() {
            this.#x++
      }

      constructor() {
            super();
            this.onclick = this.#clicked.bind(this)
      }

      connectedCallback() { 
              this.#render()
      }

      #render() {
            this.textContent = this.#x.toString()
      }
}
window.customElements.define('num-counter', Counter)

参考资料

  1. ECMAScript 6 入门
  2. 1.5万字概括ES6全部特性
  3. MDN
  4. ES2018 新特征之:非转义序列的模板字符串
  5. 正则表达式反向(lookbehind)断言
  6. Unicode property escapes
  7. exnext提案
  8. ES7、ES8、ES9、ES10新特性大盘点
  9. Ecma TC39
  10. [[ECMAScript] TC39 process](https://www.jianshu.com/p/b08...
  11. The TC39 Process
查看原文

赞 164 收藏 138 评论 5

陈大鱼头 关注了用户 · 2020-05-11

伍陆 @n_makuangmo

如果觉得我的文章对大家有用的话, 可以去我的github start一下https://github.com/Ray-56

关注 10

陈大鱼头 关注了用户 · 2019-12-27

Royal @royal_5a571ef0e2940

关注 7

陈大鱼头 关注了用户 · 2019-12-25

前端小智 @minnanitkong

我不是什么大牛,我其实想做的就是一个传播者。内容可能过于基础,但对于刚入门的人来说或许是一个窗口,一个解惑之窗。我要先坚持分享20年,大家来一起见证吧。

关注 9772

陈大鱼头 发布了文章 · 2019-11-27

CSS的未来已来

前言

最近听说TypeScript3.7添加了对Optional Chaining的支持,然后就想着给鱼头的脚手架ying-template的TS版本升级,然后在命令行发现这样的一句信息:

'postcss-cssnext' 已经被 'postcss-preset-env'代替了。详情请查看 https://moox.io/blog/deprecat...

其实鱼头的脚手架里早就把postcss-cssnext换成了postcss-preset-env,不过一直没删,但是看到这句话之后,处于好奇,就去翻了翻PostCSS的官网,然后又思考了下这些年CSS的发展历程,遂有这篇文章的出炉。

浅谈现代化的CSS

从1997年 CSS1.0 发布到如今,从最开始只支持简单的文字排版到如今已经可以做出酷炫的3D动画,CSS已经走过了22个年头,其发展如图所示:

image

图片来自[MDN]

随着互联网的发展,人们对网页的要求已经是从只要展示图文就好变成了各种交互跟视觉效果都需要有着更多的体验要求。CSS为此也是不断的更新着。

随着web业务日益复杂化和多元化,前端开发也从单纯的web page转变成web app,在此也诞生了“前端工程化”的概念,一个完备的web app往往会很大很复杂,甚至会有很多人共同维护,以往的拼页面,写jQuery已经是不足以支撑现代的需求。同样的,CSS也是如此,不再是内联写几个marginpadding或者HTML一股脑引入几个CSS就足够的,而且由于人员配置的增多,不同的开发,命名习惯,样式是否会冲突也是必须要考虑的。

除了工程问题,还有就是CSS与浏览器之间的关系也是我们不得不考虑的,虽然CSS发展的很快,但是浏览器对CSS新特性支持的进度确实非常缓慢的。所以虽然某些属性已经推出了很多年,但是也往往因为浏览器的原因而无法进行大规模的使用。

虽然在实际开发过程中,CSS有着这样那样让人无法忽略的问题,但是“方法总比困难多”,在前端界也有许多热心的大牛们在尝试着解决这些问题。这次让鱼头与大家一起分享下这些与CSS相关的技巧与方法。

最初的CSS模块化 —— CSS命名规则

命名一直是开发者比较头疼的问题,在前端里,除了JS各种变量的命名,还有元素class的命名,虽然我们可以随意起名,愿意的话甚至可以使用.a .b .c等无意义的规则来命名,但是如果是一个长期的,大型的或多人协作的项目里这么命名,恐怕容易被人胖揍。这次我们来分享下业界常用的用来防挨揍的命名规则。

OOCSS(Object-Oriented CSS)

OOCSS有两个编写原则:

  • 结构与样式分离
  • 容器与内容分离

我们来看看官网的一个例子:

image

<div class="mod grab"> 
    <b class="top">
        <b class="tl"></b>
        <b class="tr"></b>
    </b> 
    <div class="inner">
        <div class="hd">
            <h3>grab</h3>
        </div>
        <div class="bd">
            <p>Body</p>
        </div>
    </div>
    <b class="bottom">
        <b class="bl"></b>
        <b class="br"></b>
    </b> 
</div>

在这里.mod是父类,所有的类都是继承自它,.grab便是子类。

至于.top.innerbottom,顾名思义就是不同位置的子盒子。

这里是以“容器”为命名法则。

BEM

BEM 是块(Block)、 元素(Element)、修饰符( Modifier)的单词集合。

在选择器中,我们用以下三种符号来表示以上内容

  • - 中划线 :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
  • __ 双下划线:双下划线用来连接块和块的子元素
  • _ 单下划线:单下划线用来描述一个块或者块的子元素的一种状态

就像这样:type-block__element_modifier

官网的例子如下:

image

<style>
    .button {
        display: inline-block;
        border-radius: 3px;
        padding: 7px 12px;
        border: 1px solid #D5D5D5;
        background-image: linear-gradient(#EEE, #DDD);
        font: 700 13px/18px Helvetica, arial;
    }
    .button--state-success {
        color: #FFF;
        background: #569E3D linear-gradient(#79D858, #569E3D) repeat-x;
        border-color: #4A993E;
    }
    .button--state-danger {
        color: #900;
    }
</style>
<button class="button">
    Normal button
</button>
<button class="button button--state-success">
    Success button
</button>
<button class="button button--state-danger">
    Danger button
</button>

SMACSS

SMACSS,一个长得很像OOCSS的规则。

核心只有以下6个:

  • Base:页面的基本样式命名规则
  • Layout:布局命名规则
  • Module:模块规命名规则
  • State:状态命名规则
  • Theme:主题命名规则
  • Changing State:可变状态的命名规则

修饰符是--,子模块是__

官网的例子如下:

image

<style>
    #header { … }
    #primarynav { … }
    #maincontent { … }
</style>
<div id="header"></div>
<div id="primarynav"></div>
<div id="maincontent"></div>

为CSS赋能 —— 预处理器

CSS 预处理器是一个能让你通过预处理器自己独有的语法来生成CSS的程序。市面上有很多CSS预处理器可供选择,且绝大多数CSS预处理器会增加一些原生CSS不具备的特性,例如代码混合,嵌套选择器,继承选择器等。这些特性让CSS的结构更加具有可读性且易于维护。

sass

image

sass是诞生最早,也是世界上最成熟、最稳定、最强大的专业级CSS扩展语言!(官网说的(O_o)?? )

sass可用使用变量,嵌套规则,混合器,继承等编程语言才有的概念,代码例子如下:

$nav-color: #F90;
nav {
  $width: 100px;
  width: $width;
  color: $nav-color;
}

//编译后

nav {
  width: 100px;
  color: #F90;
}

less

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgwxMfHk-1573627304548)(http://lesscss.org/public/img...)]

Less 是一门 CSS 预处理语言,它扩展了 CSS 语言,增加了变量、Mixin、函数等特性,使 CSS 更易维护和扩展。

代码例子如下:

@base: #f938ab;

.box-shadow(@style, @c) when (iscolor(@c)) {
  -webkit-box-shadow: @style @c;
  box-shadow:         @style @c;
}
.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) {
  .box-shadow(@style, rgba(0, 0, 0, @alpha));
}
.box {
  color: saturate(@base, 5%);
  border-color: lighten(@base, 30%);
  div { .box-shadow(0 0 5px, 30%) }
}

// 编译后
.box {
  color: #fe33ac;
  border-color: #fdcdea;
}
.box div {
  -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}

stylus

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCdAdqND-1573627304550)(https://timgsa.baidu.com/timg...]

Stylus,富于表现力、动态的、健壮的 CSS

代码例子如下:

body
  font 12px Helvetica, Arial, sans-serif

a.button
  border-radius 5px

完全不需要{} : ;的预处理器,个人是特别不喜欢这种写法,但是对于很多喜欢简洁的开发者来说,这确实非常好的编写方式

如魔法师一般的存在 —— CSS Houdini

有点时候眼看CSS出来新的属性,但是因为浏览器兼容的问题,所以往往是只能看而不能用,即便有的属性可以用,但也因为各浏览器的现实情况而存在意想不到的BUG,那么这就意味着一个属性出来之后我们要等到5年甚至更久之后才能使用吗?都9012年了耶?

当然不是,接下来我们可以了解一下这个如魔法师一般的存在 —— CSS Houdini

CSS Houdini是什么?

CSS Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发者可以通过这组API来扩展CSS。它让开发者拥有了直接访问CSSOM的能力,开发者可以通过这组API来编写浏览器可解析的CSS代码,这让开发者可以在不需要等待浏览器的实现的前提下实现自己想要的CSS功能。

image

[图片来自:https://www.qed42.com/blog/bu...]

如上所示,不同的API所对应的就是浏览器不同的渲染环节,用时下流行的概念来解释就是浏览器加载时不同生命周期的钩子函数。

简单来说,CSS Houdini就是JS IN CSS,niubility ..

CSS Houdini是怎么工作的?

我们可访问的7个API如下:

  1. Typed OM API
  2. Properties & Values API
  3. Paint API
  4. Layout API
  5. Animation worklet
  6. Parser API
  7. Font Metrics API

Mmmm,虽然是有7个API(Houdini drafts上还有一些),但浏览器实际的支持情况其实是这样的:

image

[图片来自:https://ishoudinireadyyet.com/]

CSS Houdini的工作流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EJvSHWB-1573627304556)(https://www.qed42.com/sites/d...]

[图片来自:https://www.qed42.com/blog/bu...]

  1. 钩子进入渲染的进程中
  2. JS是这个钩子的核心
  3. 使用JS的Typed OM,可以挂载自定义的属性,绘制图形,布局以及动画
  4. 还有其他两个API:Parser API 和 Font Metrics API。它们用于注册CSS相关的新事物

一些示例

本篇不打算细讲CSS Houdini,所以不会画出所有的DEMO,有兴趣的可以查看底部的“资料来源”,从而获取更加详细的信息。

Typed OM

<style>
    * {
        margin: 0;
        padding: 0;
    }
    .box {
        background: linear-gradient(to right, #2c3e50, #4ca1af);
    }
</style>
<div class="box" id="box"></div>
<script>
    'use strict'
    box.attributeStyleMap.set('width', CSS.px(200))
    box.attributeStyleMap.set('height', CSS.px(200))
    const [x, y] = 'width,height'
    .split(',')
    .map(val => Number.parseInt(box.computedStyleMap().get(val)))
    box.attributeStyleMap.set('transform', new CSSTranslate(CSS.px(x), CSS.px(y)))
    console.log(box.computedStyleMap().get('transform'))
    console.log(window.getComputedStyle(box, null)['transform'])
</script>

image

上面就是Typed OM的示例,这里值得一提的就是,如果我们用getComputedStyle去获取transform的值,最终结果是个矩阵,这其实不太方便我们做二次操作,但是用Typed OM的JS API computedStyleMap,去取的结果就是一个具体属性的集合,这是非常有利于我们进行二次操作的。

Paint API

Paint API就是允许你例如Canvas的属性来编写CSS样式,使用方法也很简单,我们可以看看https://slides.iamvdo.me/waq1...上的示例

首先我们新建个文件叫registerPaint.js,在里面写下以下代码:

registerPaint('circle-ripple', class {
  static get inputProperties() { return [ '--circle-color',
    '--circle-radius', '--circle-x', '--circle-y'
  ]}
  paint(ctx, geom, props, args) {
    const x = props.get('--circle-x').value;
    const y = props.get('--circle-y').value;
    const radius = props.get('--circle-radius').value;
  }
}

然后再新建一个index.html,并且在JS代码里注册上面写好的registerPaint.js,方式如下:CSS.paintWorklet.addModule('registerPaint.js');

具体代码如下:

<style>
    .el {
          --circle-radius: 0;
          --circle-color: deepskyblue;
          background-image: paint(circle-ripple);
    }
    .el.animating {
          transition: --circle-radius 1s,
                      --circle-color 1s;
          --circle-radius: 300;
          --circle-color: transparent;
    }
</style>
<div class="el" id="el"></div>
<script>
    'use strict'
    CSS.paintWorklet.addModule('registerPaint.js');
    el.addEventListener('click', e => {
          el.classList.add('animating');
          el.attributeStyleMap.set('--circle-x', e.offsetX);
          el.attributeStyleMap.set('--circle-y', e.offsetY);
    });
</script>

所以我们有以下的效果:

image

CSS届的Babel —— PostCSS

说到底CSS Houdini其实也只是JS IN CSS,并不是纯正的CSS,那么对于一些新的CSS属性,我们相用的话,真的还得等5年后吗?还有即便是有各种工具,但是像一些兼容性写法,厂商前缀,循环,原生CSS也没有,我们不是还得需要依赖CSS预处理器吗?

其实也不是,这时候我们可以利用CSS届的Babel —— PostCSS

PostCSS是什么?

简单来说PostCSS就是可以让开发者使用JS来处理CSS的处理器,它分了以下5大类功能:

增强代码的可读性

利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。

例如我们输入以下代码:

:fullscreen {
}

那么就会输出:

:-webkit-:full-screen {
}
:-moz-:full-screen {
}
:full-screen {
}

将未来的 CSS 特性带到今天!

PostCSS Preset Env 帮你将现代 CSS 语法转换成大多数浏览器都能理解的东西,根据你的目标浏览器或运行时环境来确定你需要的 polyfills,基于 cssdb 实现

例如我们输入以下代码:

@custom-media --med (width <= 50rem);

@media (--med) {
  a { 
    &:hover {
      color: color-mod(black alpha(54%));
    }
  }
}

就会输出:

@media (max-width: 50rem) {
  a:hover  { 
    color: rgba(0, 0, 0, 0.54);
  }
} 

终结全局 CSS

CSS 模块 就是说你永远不用担心命名太大众化而造成冲突太普通,只要用最有意义的名字就行了。

例如我们输入以下代码:

.name {
  color: gray;
}

就会输出:

.Logo__name__SVK0g {
  color: gray;
}

避免 CSS 代码中的错误

通过使用 stylelint 强化一致性约定并避免样式表中的错误,stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,包括类似 CSS 的语法,例如 SCSS 。

例如我们输入以下代码:

a { 
  color: #d3;
}

那么控制台会抛出错误:

app.css
2:10 Invalid hex color

强大的网格系统

LostGrid 利用 calc() 和你所定义的分割方式来创建网格系统,无需传递大量参数。

例如我们输入以下代码:

div {
  lost-column: 1/3 
}

就会输出:

div {
  width: calc(99.9% * 1/3 -  
  (30px - 30px * 1/3)); 
}
div:nth-child(1n) {
  float: left; 
  margin-right: 30px; 
  clear: none; 
}
div:last-child {
  margin-right: 0; 
}
div:nth-child(3n) {
  margin-right: 0; 
  float: right; 
}
div:nth-child(3n + 1) {
  clear: both; 
}

可窥探的未来 —— cssdb

cssdb是postcss-preset-env的实现基准,主要就是CSS的新功能功能及这些功能从提出到成为标准时所在的进程。

cssdb跟ecma一样,对新属性分了不同的进程,具体的进程如下:

  1. Stage 0:脑袋风暴阶段。高度不稳定,可能会发生变化。
  2. Stage 1:实验阶段。也非常不稳定,可能会发生变化,但是该提案已得到W3C成员的认可。
  3. Stage 2:承认阶段。高度不稳定并且可能会发生变化,但是正在积极研究中。
  4. Stage3:拥抱阶段。稳定且变化不大,此功能可能会成为标准。
  5. Stage4:标准阶段。最终的解决方案,所有主流浏览器都支持。

这就是postcss-preset-env依赖的实现基准,那么如果我们想要在我们的代码里使用这些Stage,该怎么做呢?

以我的脚手架ying-template为例,我们来查看在webpack中的实际配置:

首先我们先安装postcss以及其相应的插件:

npm install postcss postcss-loader postcss-preset-env postcss-nesting --save-dev

然后我们在webpack的config配置module中输入以下配置:

module: {
    rules: [
        {
            test: /\.css$/,
            include,
            exclude,
            use: [/* 你其它的loader */ 'postcss-loader']
        }
    ]
}

然后在根目录新建一个postcss.config.js

const postcssConfig = {
    plugins: {
        precss: {},
        'postcss-preset-env': {
            browsers: 'last 2 versions', // 浏览器兼容的版本
            stage: 3 // 你用的属性所在的阶段
        },
        'postcss-nesting': {} // 这里就是你所使用的插件
    }
};
module.exports = postcssConfig

这样就完成了,如果想看完整的配置,可以clone我的脚手架:https://github.com/KRISACHAN/...

(这是个多页面的webpack4脚手架,集成了babel 7,precss 4,typescript3.7,karma以及eslint等现代前端开发所需常用的东西,有兴趣的可以去看看。)

我们可以通过https://preset-env.cssdb.org/...这个网站来查看具体的编译结果。

编译结果图如下:

image

是不是非常神奇呢?

后话

随着前端工程的普及,某E浏览器的没落,CSS的发展可谓是一日千里,近日也有一些数学属性的提案在发起,以后会发展成什么样,没人可以知道。只是总的来说,CSS的未来是一片光明的。本文简单分享了一些现代化的CSS知识,通过这些知识,我们很容易就能写出完备且现代化的CSS代码,能够给创造出更多的效益,希望大家可以积极地用起这些知识,并对CSS可以有更多的思考以及想象。

CSS,未来可期

资料来源

  1. https://developer.mozilla.org...
  2. http://oocss.org/
  3. http://getbem.com/
  4. http://smacss.com/
  5. https://sass-lang.com/
  6. http://lesscss.org/
  7. http://stylus-lang.com/
  8. https://blog.techbridge.cc/20...
  9. https://www.smashingmagazine....
  10. https://slides.iamvdo.me/waq1...
  11. https://www.qed42.com/blog/bu...
  12. https://www.postcss.com.cn/
  13. https://cssdb.org/#staging-pr...
  14. https://s0dev0to.icopy.site/a...

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。
鱼头的微信号是:krisChans95
也可以扫码添加好友,备注“SF”就行
https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/base/wx-qrcode1.jpg

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 259 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-01-08
个人主页被 3.8k 人浏览