Leon82 发布于 05月22, 2019

谈谈StorageEvent

纷纷红紫已成尘,布谷声中夏令新。夹路桑麻行不尽,始知身是太平人。 ——宋.陆游 《初夏绝句》

我们在开发多Tab应用时候,常常会遇到多个Tab状态同步的问题。

想象如下场景:用户主界面,显示用户购物车内待结算的商品总数。此时,用户可能打开多个Tab。当用户添加新商品到购物车的时候,需要更新购物车的数量。

此时,当前页面需要向服务器发起请求,在得到添加成功响应的时候,可以更新用户界面。为了兼顾体验和可靠性,如果确信添加成功概率比较高的时候,也可以先更新界面,当多数返回错误的时候,可以给用户界面做状态回滚。为了下次展示方便,我们还会把这个数据写到LocalStorage里面。用户再次打开时候,可以优先从localStorage中取值。

当前页面解决了,那么如果同时打开多个Tab该如何解决呢?这里使用StorageEvent可能是一种代价较小的解决方案。

StorageEvent是什么呢?

  1. 是一种Event,可以通过标准的Event监听器操作。
  2. 当storage变化时候,事件会被派发到所有同域下的其他页面。
  3. 触发变化的当前页面,没有事件派发。

这里有一个简单的示例可以展示这个API的用法。

const STORAGE_KEY = "cartlist"

const getStorage = () => {
    try {
        let rets = window.localStorage.getItem(STORAGE_KEY)

        if (rets === null) {
            return []
        }
        return JSON.parse(rets)
    }
    catch(e){
        return []
    }
}

const addCart = (value) => {
    let rets = getStorage()

    rets.push(value)

    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(rets))

    return rets    
}

const minusCart = (value) => {
    let rets = getStorage()
    let idx = rets.indexOf(value)

    if (idx !== -1){
        rets.splice(idx, 1)
        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(rets))
    }


    return rets    
}

const render = () => {
    let rets = getStorage()

    if (rets.length){
        $("#num").html(rets.length).show()
    }
    else {
        $("#num").hide()
    }

    $(".list li").each((i,el) => {
        if (rets.includes(i)){
            $(el).find("a:nth-child(1)").css("visibility", "hidden")
            $(el).find("a:nth-child(2)").css("visibility", "visible")

        }
        else {
            $(el).find("a:nth-child(1)").css("visibility", "visible")
            $(el).find("a:nth-child(2)").css("visibility", "hidden")

        }
    })
}

$(".list a").on("click", (e)=> {
    let opIdx = $(e.target).parent().find("a").index(e.target)
    let line = $(e.target).parent().parent()
    let idx = $(".list li").index(line)

    opIdx === 0 ? addCart(idx) : minusCart(idx)

    render()

    return false
})

window.addEventListener('storage', (e) => {
    render()
})

render()

其中,下面这行代码是实现的关键:

window.addEventListener('storage', (e) => {
    render()
})

当我们注释掉这个语句,我们的页面同步就不能运行了。

读者可以打开多个Tab并观察页面的变化https://jsbin.com/radekilosu/1/edit?html,css,js,output。

实际上,这个事件e上还带有很多信息,方便编程时,对于事件做精确的控制。

字段 含义
key 发生变化的storageKey
newValue 变换后新值
oldValue 变换前原值
storageArea 相关的变化对象
url 触发变化的URL,如果是frameset内,则是触发帧的URL

上述各值都是只读的。

还有一点没有解决掉,就是触发storage变化的本页面,不能接收这个值,这个一般情况下是没问题。当然,为了一致性,我们可以自行new一个事件,在发生时候主动触发它。

此时我们可以包装一个新的Storage对象:

var Storage = {
    setItem : function(k,v){
      localStorage.setItem(k,v);
    },
    removeItem : function(k){
      localStorage.removeItem(k);
    },
    clear: function (){},
    getItem: function(k){}
}

此时,我们再包装一个函数:

function dispatchMe(key, oldval, newval, url, storage){
    var se = document.createEvent("StorageEvent");
    se.initStorageEvent('storage', false, false, key, oldval, newval, url, storage);
    window.dispatchEvent(se);
}

此时,我们只需要在setItem、removeItem、clear中获取对应的值,并手动调用一下dispatchMe,同时把和localStorage打交道的地方改为调用我们包装的Storage即可。

参考资料

  1. https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
  2. https://www.cnblogs.com/cczw/p/3196195.html

阅读全文 »

Leon82 发布于 05月21, 2019

从谷歌华为暂停合作说起

引子

2019年5月20日,科技界最火爆的消息莫过于谷歌暂停支持华为部分业务。关于这个事件将产生的影响,大家可以从相关的科技媒体上找到详细的分析。比较一致的看法是:谷歌的这个做法,对于国内的华为用户,影响不大;对于海外的华为用户,尤其是在上游应用生态环境上,会有一定的影响。

这是由于,谷歌暂停合作的服务,都是在用户层面的服务GMS。用户层与安卓底层内核遵从不同的协议,因此不必开源,进而导致替换成本高企。基于现有生态,在海外市场构建应用生态环境难度非常大。

这篇文章将尝试从这则新闻涉及的开源协议来分析一下,这个做法背后相关的逻辑,并籍此,跟各位读者聊一聊主流的开源协议。

阅读全文 »

Leon82 发布于 04月21, 2019

Puppeteer PK 滑动验证码

天街小雨润如酥,草色遥看近却无。最是一年春好处,绝胜烟柳满皇都。 —— 唐.韩愈 《早春呈水部张十八员外二首》

引子

几个月之前,chunpu小编曾经在《震惊! 滑动验证码竟然能这样破解》一文中给大家展示了使用支持WebDriver标准的Firefox破解滑动验证码的示例,脑洞之大,构思之巧,笔法幽默,叹为观止,也极大的燃起了笔者对此的兴趣。

这篇文章就带大家使用目前较为流行的Puppeteer完成这件事情。

阅读全文 »

Leon82 发布于 04月03, 2019

NodeJS和命令行程序

造物无言却有情,每于寒尽觉春生。千红万紫安排著,只待新雷第一声。 —— 清.张维屏 《新雷》

源起

植根于Unix系统环境下的程序,很多都把贯彻Unix系统设计的哲学作为一种追求。Unix系统管道机制的发明者Douglas McIlroy把Unix哲学总结为三点:

  1. 专注做一件事,并做到极致。
  2. 程序协同工作。
  3. 面向通用接口,如文本数据流。

随着Unix/Linux系统在服务器上影响力越发强大,以及各种跨平台解决方案的发展,这种哲学也被带到了各种平台上。若干年前,笔者第一次接触NodeJS和其包管理解决方案NPM时候,就感觉到其官方倡导的风格,和Unix系统哲学非常契合。近年来,随着NodeJS在服务端以及前端构建领域上的不断开拓,NodeJS的这种思想也正快速的渗透到这些领域。

其实,NodeJS的本身,也是开发命令行程序的一个重要利器。本文就将介绍几个常用的NodeJS相关命令行程序,之后介绍几个开发命令行中常用的组件,最后介绍发布npm包以及带scope的包的发布方法。

阅读全文 »

Leon82 发布于 03月12, 2019

NodeJS与模块系统

孤山寺北贾亭西,水面初平云脚低。几处早莺争暖树,谁家新燕啄春泥。乱花渐欲迷人眼,浅草才能没马蹄。最爱湖东行不足,绿杨阴里白沙堤。 ———— 唐.白居易《钱塘湖春行》

自从 Node8.5 以后, Node 开始支持引入 ES 模块。在新开的项目中,笔者尝试使用了这种方式。由于目前 NodeJS 对于 ES 模块尚属试验性支持,因此需要在启动时候加入参数:--experimental-modules,完整的命令如:node --experimental-modules index.mjs。这样程序就愉快地运行起来了。不过当笔者尝试加入一些新的依赖之后问题出现了:

阅读全文 »

Leon82 发布于 01月18, 2019

GraphQL初探(一)

白玉堂前一树梅,为谁零落为谁开。唯有春风最相惜,一年一度一归来。 宋.王安石《梅花》

引子

仿佛一瞬间,2019 年的一月份就快过去了,然而迎接元旦的时刻似乎还历历在目。时间总是这么安静的流走,以它自己的节奏,不疾不徐。猛的发现,21 世纪也过去了将近 1/5 的时间了。

每一年,都会诞生各种各样的技术。有的经过实践的检验和逐步的完善,慢慢成为业界的标准或是最佳实践;有的则慢慢淡化直至完成使命,或者演化为其他技术。

在技术发展的上升阶段,总会有一些热点的技术,在一段时间被反复提起。它们诞生的初衷往往是一个很具象的痛点,然后在解决、完善之后,外延不断扩大,并最终成长为一个成熟的技术或者技术栈。

今天要说的 GraphQL 可能就是这样的技术。

为什么会有 GraphQL

随着前后端分离的开发模式不断的深入人心,越来越多的项目采用这种方式开发和部署。前端程序或是 App,多采用数据驱动的方式完成。数据可以采用数据接口的方式从服务端请求获得。主流的方式一般采用 REST API。

在 REST 风格的 API 中,接口数据被认为是一种“资源”,服务端提供 RESTful API,客户端通过 GET/POST/DELETE/PUT 等动作,通过 URI 对“资源”进行操作。

比如:GET /books/1 即是通过 GET 请求,访问 URI /books/1,也即服务端提供的/books/:id的 RESTful API,“获取”资源,得到如下返回:

{
    "bookId": 1,
    "title": "Black Hole Blues",
    "author": {
        "firstName": "Janna",
        "lastName": "Levin"
    },
    "page": 260,
    "press": "..."
}

这样的情景适应于现在的主流开发场景。不过也由一些痛点,下面列出几种:

  1. 后一个请求依赖前一个请求。在依赖比较多情况下,一般会造成多次请求。对于每次请求也都需要做失败情况的处理。

举个例子,我们需要查询某本书作者的详细信息。

现有接口:GET /books/:bookId返回形如下列格式:

{
    "bookId": 1,
    "title": "Black Hole Blues",
    "authors": [
        "authorId": 100
    ],
    "page": 260,
    "press": "..."
}

另有一个接口,可以获得作者信息:GET /authors/:userID返回形如下列格式:

{
    "userId": 100,
    "name":{
        "firstName": "Janna",
        "lastName": "Levin"
    },
    "age": ***,
    "gender": "***",
    "photos":[2, 3, 5, 7, 11, 13, 17, 19, 23]

如果只有这两个个最简单的接口,咱们的需求还是可以完成的:首先,先通过GET /books/1取得作者 ID,然后再通过GET /authors/100就取到了作者的信息。

这里,会有两次相继的请求。后面的请求严格依赖前面的请求。这样可能会造成以下几个问题:

(1)在业务上,对于多个作者的情况,可能需要请求的次数更多(可以提供多 ID 查询接口变相解决)

(2)请求时间长。即便启用了Keep-Alive也仅仅减少了重复建立链接的时间。

(3)如果第一次请求失败,需要对错误进行处理或重试。一方面会增大复杂度,另一方面极端的网络条件下,频繁的重试,对于服务端的压力是几何级的增长。

这样的情况,有的团队可能的做法是另外提供一个面对业务的整合接口,如: GET books/1/author/,可以间接地解决问题。

这样做的问题在于,一个前端界面上的问题,直接透给了服务端。业务的变化,会造成这种接口的爆炸增长抑或成批的废弃,给维护造成负担。

  1. 依然是上面的例子。现在对于多端的开发,需要的数据不同。如:手机上仅需获取作者,而 PC 上需要通过photo字段所示的照片集取得第一张照片显示......。为了达到这样的效果,一般的团队可能会为每种端开发一种特定 API,或者是统一把最冗余的数据接口提供出去。这样显然都不是最理想的。

这些痛点至少代表大家在开发中经常遇到的情况。GraphQL 也许可以成为解决这些问题的一个不错的选择。

什么是 GraphQL

那么,什么是 GraphQL 呢?

GraphQL 是一套关于数据查询和操作的语法标准,它极为适合用于 API 交互。它由 Facebook 在 2012 年开发并在内部使用。2015 年对外发布。2018 年 11 月 7 日,GraphQL 由 Facebook 转交由新成立的 GraphQL 基金会管理。

简单的说来,GraphQL 提供了一种机制,允许客户端定义返回的数据结构。当数据返回时候,精确的根据定义返回所需的数据结构。这种机制避免了大量冗余数据的返回,也使得合并多次请求成为可能。

这里是 GraphQL 官方网站。国内还有一份中文的镜像网站

GraphQL 确切讲是一种语言标准,是与实现语言无关的。实际上,Facebook 和开源届,已经有了针对 GraphQL 的各种语言的实现

感受 GraphQL

为了对 GraphQL 有一个感性的认识,读者可以参考官方示例进行操作,完成一个 GraphQL 查询。

GraphQL 引入了类型系统、参数、查询与变更类型、标量类型、枚举类型、接口、联合类型、输入类型等概念。读者可以在这里学习相关概念的描述。这些概念至关重要。

接下来我们打交道的各种程序,无论是前端还是服务端,始终都回避不了一个名字:Apollo。Apollo 是一个通过社区力量帮助你在应用中使用 GraphQL 的一套工具,由 Meteor Development Group开发。本文选型的前端和服务端都最终依赖 Apollo 这个利器。下图是 Apollo 涵盖的领域:

在 Vue 项目中整合 GraphQL

我们以使用 Vue 的项目为例,看一下在前端如何使用 GraphQL。整合 Vue 与 GraphQL 依赖库:vue-apollo。vue-cli 3.x 已经支持工具化引入这个库。

我们以 vue-cli 3.x 为例。

  1. 建立工程,以graphql-test为例
vue create graphql-test
  1. 添加 apollo 插件
cd graphql-test
vue add apollo

此后将安装vue-cli-plugin-apollo,用于配置请求。

为了简单地展示原理,我们暂时不安装样例代码。

此时,目录结构如下图:

默认地,vue-cli-plugin-apollo已经在入口的main.js为我们引入好了配置文件,并全局地,引入到 vue 组件中:

OK,一起来修改一下HelloWorld.vue,如下:

<template>
    <div>
        <ApolloQuery :query="require('../graphql/hello.gql')">
            <template slot-scope="{ result: { loading, error, data } }">
                <div v-if="data">{{ data.hello }}</div>
                <div class="book">
                    <div class="title">Book</div>
                    <ul v-if="data">
                        <li v-for="item in data.books">
                            <span>title: {{item.title}}</span> <span>author: {{item.author}}</span>
                        </li>
                    </ul>
                </div>
            </template>
        </ApolloQuery>
    </div>
</template>

<style scoped>
    .title {
        font-size: 14px;
        font-weight: bold;
    }

    .book ul {
        margin: 0;
        padding: 0;
        margin-left: 50%;
        transform: translateX(-50%);
    }

    .book li {
        margin: 0;
        padding: 0;
        list-style: none;
        text-align: left;
        display: flex;
        align-content: space-between;
    }
</style>

<script>
    export default {
        apollo: {},
    };
</script>

hello.gql 的内容如下:

query Hello {
    hello
    books {
        title
        author
    }
}

此时运行:npm run serve。打开浏览器访问:http://localhost:8080

大家会看到,目前还没有网络返回。为了实现和服务器的交互,我们还需要把服务端建立起来。

在服务器端响应请求

这里需要说明一点,无论是前端还是服务端的响应代码不一定是 Javascript 的。前文提到的各种库列出了主流语言的实现。本文中,以服务端 Javascript 为例来说明。

特别地,如果使用上文提到的 vue-cli 3.0 生成服务端代码也是可以的。这里为了清晰,还是以手动建立为例。

  1. 建立服务端工程,以graphql-server-test为例
vue create graphql-server-test
  1. 安装依赖
cd graphql-server-test
npm init -y
npm install --save apollo-server graphql
  1. 建立响应文件server.js

此时,刷新浏览器,可以看到响应。

优势与劣势分析

对于 GraghQL,优势在于可以有效地降低请求数和请求载荷,提升效率。同时可以灵活地应对各种客户端需求以及多变的业务需求。当然也客观地增加了请求实现的复杂度,同时由于请求对象的个性化,不能充分利用 HTTP 的缓存,也是一个值得重视的问题。

这里Nate Barbettini在 From Iterate Conference 2018 上的演讲以及问答视频,比较客观的比较了 RPC、REST 和 GraghQL 三种方式的优劣,有兴趣的同学可以了解。

Next

本系列下一篇,将以一个完整的前后端示例,带读者亲自体验在实际项目中的 GraphQL。

阅读全文 »

Leon82 发布于 12月31, 2018

形神兼备——谈谈CSS Shapes

东风夜放花千树。更吹落、星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。

蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。 ——宋.辛弃疾《青玉案.元夕》

引子

马上就要进入 9102 年的今天,仅仅借助于 CSS,Web 开发者就已经可以绘制出各种丰富多彩的图形了,而且方式还是多种多样的。比如下面的鹰嘴形,就可以用多种方式实现。

<span class="icon"></span> <span class="icon icon--blue"></span>
.icon{
    display: inline-block;
    width: 60px;
    height: 100px;
    margin: 100px 30px;

    border-top: 30px solid #000;
    border-top-left-radius: 55px 60px;
}
.icon--blue{
    border-top-left-radius: 60px 70px;
    border-color: blue;
}
.icon{
    display: inline-block;
    width: 60px;
    height: 100px;
    margin: 100px 30px;

    border-bottom: 60px solid transparent;
    border-left: 80px solid #000;
    border-top-left-radius: 100%;

    transform: rotateX(180deg) rotateZ(-90deg);
}
.icon--blue{
    border-left-color: blue;
}

看上去很完美。不过这些我们创建的图形,还不能影响图形内部和周围的内容排布。举个例子来讲,我们可以简单地在页面上创建一个三角形,但是也许无法控制周围的内容围绕这个三角形。

自从 7102 年,iPhoneX 引入“刘海屏”之后,异形屏的全屏适配交互也成为了前端的又一个课题。随之而来的各种适配交互方案,也是层出不穷。下面是一种很讨巧的方案。

这种在排布内容的问题可以使用 CSS 标准的 CSS Shapes 解决。特别地,针对上文提到的“刘海屏”的适配交互方案,张鑫旭老师的这篇文章给出了解决方案的详细解析。

阅读全文 »

Leon82 发布于 12月02, 2018

给你点颜色看看

万事有不平,尔何空自苦;长将一寸身,衔木到终古?我愿平东海,身沉心不改;大海无平期,我心无绝时。呜呼!君不见,西山衔木众鸟多,鹊来燕去自成窠。 —— 清.顾炎武 《精卫》

引子

五颜六色的世界,是大自然对人类的一种馈赠。在计算机领域,显示器是颜色显示的主要介质。笔者接触过的最早的单色显示器到目前的广色域显示器,不得不说是技术的一个又一个飞跃。

在 Web 领域,我们不可避免的要和颜色打交道。其实除了我们最常见的十六进制表示方法,W3C 色彩标准还支持许多不同的颜色表示方法。它们各有所长。本文就带读者了解这些表示方法的原理、标准写法、浏览器支持程度以及相互转换方法。

阅读全文 »

Leon82 发布于 11月01, 2018

从一道面试题谈起

人生无根蒂,飘如陌上尘。分散逐风转,此已非常身。落地为兄弟,何必骨肉亲!得欢当作乐,斗酒聚比邻。盛年不重来,一日难再晨。及时当勉励,岁月不待人。 晋.陶渊明《杂诗》

笔者做面试官时候,除了考察常规的前端技能、框架的特性外,经常会出一些很简单的算法题目。这种经典题目的好处是言简意赅,在很短的时间就可以完成。但却能考察出工程师的基本计算机功力,以及知识储备。特别对于Javascript而言,还可以避免技术栈不同对于工程师能力的误判。

下面就是一道笔者经常考核的题目:

创建一个函数来判断给定的表达式中的大括号是否闭合,返回True/False,对于空字串,返回True:

var expression = "{{}}{}{}"
var expressionFalse = "{}{{}";

function isBalanced(exp) {}

题目本身比较简单。不同的同学拿到题目会有不同的第一反应。

有的同学仅仅查找各种括号的个数,这种是不可以的,这种括号是不匹配的:}{

有很多同学试图使用正则表达式解决问题。其思路是找到所有邻接的匹配的括号对,然后将其替换为空,直到不能替换为止。此时如果最后得到的字符串为空即为匹配,否则不匹配,代码很简洁,如下:

function isBalanced(exp) {
  var reg =  /\{\}/g, len;
  do{
      len  = exp.length;
      exp = exp.replace(reg, "")
  } while (len != exp.length)
  return exp.length === 0
}

好像也是可以的。但是问题点至少有二:

第一、此算法的最坏的时间复杂度是O(n^2)级别的,对于长篇大论是不友好的。

第二、此算法的正则表达式普适性较差,对于表达式含有其他干扰字符时候需要频繁修改正则表达式。当正则表达式过于复杂时候,反过来又会影响到检索效率。

其实,有时候最淳朴的做法,可能会更有效。

大家在数据结构课程中曾经学习过“栈”这种数据结构。“栈”是一种满足后进先出的抽象数据结构。这个结构在这道题目中可以帮助到我们。

思路如下:

  1. 巡检字符串,将括号分类,一类是左括号、一类是右括号。
  2. 左括号看作是入栈信号,右括号是出栈信号。
  3. 当出栈时,如果栈定没有与之匹配的元素,则宣告不匹配。
  4. 当巡检完毕,如果得到空栈,则匹配,否则不匹配。

写成代码大致就是这样,时间复杂度是O(n):

function isBalanced(exp){
    let info = exp.split("")
    let stack = []

    for(let i = 0; i < info.length; ++i){
                let el = info[i]
        if (el == "{"){
            stack.push("{")
        }
        else if (el == "}"){
            if(stack.length === 0){
                return false;
            }
            stack.pop()
        }
    }

    return stack.length === 0
}

在Javascript里,Array可以很方便的模拟栈的行为。读者有兴趣也可以自己实现一个简单的栈,用以丰富自己的代码工具箱。

另外,由于题目中的括号情形比较简单,很多同学用标志位来解决,即当为左括号时候,置标志位,为右括号时候,当标志位存在,取消标志位,否则判定不匹配。最后没有标志位则为匹配。这种解法,对于题目中的状况是可以的。

我们还可以把题目再向前面推进一步,看一看更一般的括号和字符串的情形:

实现函数isBalanced,用true或false表示给定的字符串的括号是否平衡(一一对应)。注意了是要支持三种类型的括号{},[],和()。带有交错括号的字符串应该返回false。

isBalanced('(foo { bar (baz) [boo] })') // true
isBalanced('foo { bar { baz }')         // false
isBalanced('foo { (bar [baz] } )')      // false 

基本思路和上面的解法是类似的。只是这里面需要注意两点:

  1. 过滤掉非括号的干扰字符。
  2. 每一种右括号有一种唯一的左括号与之对应。出现右括号时候,栈顶的左括号必须是和它匹配的。

对于第二种,大家应该很自然的联想到用JSON对象控制,这个是可以的。其实在ES6中,有Map这样的数据结构作为更专业的键值对存储结构可以使用。同时,一些好玩的语法和特性如扩展语法,箭头函数可以让我们把程序写得更加一气呵成。下面是一个可行的代码:

const isBalanced = (str) => {
    const map = new Map([
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ])

    let stack = [];

    for(let i = 0 ; i < str.length; ++i){
        let node = str[i]

        if (map.has(node)){
            stack.push(node)    
        }
        else if ([...map.values()].includes(node)){
            if (stack[stack.length - 1] !== 
                                [...map.entries()].filter(el=>el[1]===node).pop().shift()
                         ){
                return false
            }
            stack.splice(stack.length-1, 1)
        } 
    }    

    return stack.length === 0
}

很喜欢ES6的这些扩充,和这些新的数据结构。使得撰写Javascript有一种特别愉悦的体验。

让我们再次扩充一下这道题目。要求严格限制括号的顺序,即中括号外围只能是大括号,内部只能是小括号。也即:括号只能以大括号、中括号、小括号的顺序只能前面的包含后面的,不能后面的包含前面的,用代码来表示一下:

isStrictBalanced('foo { bar (baz) [boo] }')  // true
isStrictBalanced('(foo { bar (baz) [boo] })')  // false

这样的需求怎么解决呢?

聪明的你可能已经想出了答案,其实就是在入栈时候判断一下当前的优先级就好了。这里可能需要判断一下当前的字符和栈顶元素的关系。这里,我们如果用Array的pop API,则会破坏stack的结构。上例中,我们用length来取得,这里我们用...语法来实现数组的复制,同时在上例基础上,进行一些重构,得到下列代码:

const isStrictBalanced = (str) => {
    const map = new Map([
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ])
    let stack = [], keys = [...map.keys()], values = [...map.values()];
    for(let i = 0 ; i < str.length; ++i){
        let node = str[i]
        if (map.has(node)){
            if (stack.length){
                let arr = [node, [...stack].pop()]
                    .map(el => keys.indexOf(el))

                if (arr[0] < arr[1]){
                    return false
                }
            }
            stack.push(node)    
        }
        else if (values.includes(node)){
            let needKey = [...map.entries()].filter(el=>el[1]===node).pop().shift()
            if ([...stack].pop() !== needKey){
                return false
            }
            stack.pop()
        } 
    }    
    return stack.length === 0
}

从括号匹配的最简单情形,我们已经扩展到了比较复杂的括号匹配情形。至此,在逐步迭代的需求中给大家展示了栈的实际应用以及ES6中Map和扩展语法的使用。在面试的实际,即时反应很重要,也许并不拘泥于一种解法,从一点开始,逐步深入,慢慢延展,方能达到融会贯通,活学活用,这恐怕是面试官比较看中的。

阅读全文 »

Leon82 发布于 10月01, 2018

给你的动画加点料:谈谈AnimationWorklet

残阳西入崦,茅屋访孤僧。落叶人何在,寒云路几层。 独敲初夜磬,闲倚一枝藤。世界微尘里,吾宁爱与憎。 ——唐.李商隐 《北青萝》

引子

浏览器接收到 HTML 代码后,需要将其显示到屏幕上。不同的浏览器的处理方案有一定的差异。对此,谷歌工程师 Paul Lewis 给出了一个简明的 Rendering Pipeline 模型来说明浏览器解析流程。 据此,Paul 在这篇文章中, 详细地分析了浏览器每一步的职责以及几种常见的运行模式。

做为一个立志给用户最好的浏览体验的 Web 工程师,肯定希望在上述必要的步骤中和浏览器配合,作出必要的优化。

开发者可以通过 JavaScript 或者 CSS 来控制 DOM 和 CSSOM。对应于上图,也就是前两个阶段。在这两个阶段,开发者几乎可以随心所欲地控制 DOM 和 CSSOM。但是,后三个阶段,对 Web 开发者是黑盒。它们的行为,是由浏览器自动完成的,开发者只能通过前两个阶段的操作,间接地影响后面的阶段。

这不是我们想要的结果。

其实,各大浏览器厂商也意识到了这个问题的存在。因此,W3C 的 CSS 工作组,有一个活跃的工作小组,称为 CSS Houdini。CSS Houdini 是由一群来自 Mozilla、Apple、Opera、Microsoft、HP、Intel、IBM、Adobe 与 Google 等公司的工程师所组成的工作小组。这个小组希望指定一系列 API。通过这些 API,Web 开发者也能介入到浏览器的 CSS 解析引擎,从而和浏览器更好的配合,来解决性能、一致性等问题。这些 API 实现后,也许 CSS 的某些新标准也可以借助 Polyfill 在尚未实现的浏览器中体验了。

来自爱尔兰都柏林的资深工程师、开源软件组织成员Serg Hospodarets在 2018 年 3 月进行了一个关于 CSS Houdini 的演讲。演讲文稿中,对 Houdini 的任务模块做了如下的划分:

在这张图中,读者可以看到,浏览器渲染引擎的“白盒化”的标准,大致分了几个类:有对自定义属性的规范,有给 CSSOM 引入类型系统的规范,有解析 CSS 的规范,有字体属性与测量的规范,也有对 Layout、paint 和 Composite 的接口规范。

这里 Layout、Paint 和 Composite 的接口主要是通过 Web Worker 的机制实现的。关于 Layout、Paint 的 API,奇舞周刊后面的文章会给读者做详细的介绍,这篇文章,笔者将和读者主要谈一谈 Composite 相关的 CSS Animation Worklet API。

终于,本文的主角要登场了。没错,就是它,Animation Worklet。

什么是 Animation Worklet

那么 Animation Worklet 到底是做什么的呢?

首先,大家来回想下,一般是怎么制作动画效果的?

最简单的做法,我们使用 setTimeout 和 setInterval 来做动画,动画以时间间隔做为变化的依据。

但很快,人们发现当帧率过快时候,比如小于 16.7ms 一帧的时候,动画会发生丢帧的情况。张鑫旭老师的这篇文章非常形象和趣味地解释了这个问题。

随后,浏览器厂商开放了一个更有效率的 API:requestAnimationFrame。之所以更有效率,在于这实际上是一个基于动画帧发生的回调。这种设计的高明之处在于把帧与时间解耦,从而不再受制于显示器的刷新率。

这个结果似乎已经很完美了,不过这里还有一点问题。以 Chrome 浏览器为例,渲染引擎分主线程和 Compositor 线程。Layout 完成之后,主线程维护了一份 Layer 树,Compositor 维护了一个副本。如果我是主线程,我一定会对这个分担我的运行压力,并且定期和我同步、听候我同步指令的线程兄弟,欣赏有加的。然而,requestAnimationFrame是运行在主线程的。一旦主线程非常繁忙,动画的效率会大打折扣。

怎么才能让 Compositor 线程分担主线程的压力呢?读者不妨参考https://csstriggers.com/ 。只要不触发 Layout 或 Paint 的属性改变,就可以让 Compositor 线程独立工作,并择机同步给主线程。CSS3 动画引入的 transform 就是这个原理。使用了 transform 的动画,往往会比 JavaScript 动画更流畅。对啊,那可是多人干活啊。

好了,如果是这样,我们就尽量研习规范,把工作丢给 Compositor 线程就好了呗。但是,理想总是美好的,现实总是骨感的:能确定丢给 Compositor 线程独立工作,主线程无需参与的,现在还不太丰富。

Animation Worklet 的出现,就是为了满足这个痛点出现的。一方面,可以借助 JavaScript 的威力制作强大而精确控制的动画,一方面可以在 Compositor 线程独立工作。它的目标旨在兼容目前的 Web Animations 标准,尽可能的使用现有的结构,以其使用这组 API 能平衡动画的性能、丰富性和合理性。

Animation Worklet 最早曾经被称为 Compositor Worklet。顾名思义,之前的这些代码是可以直接执行在 Compositor 线程的。直到 2016 年,Houdini 小组在 TPAC 会议(TPAC 会议,W3C 的年度重要技术会议之一。参会者在五天的时间里,共同协调未来开放 Web 平台的技术方向,讨论 W3C 的组织策略)期间,提出对 API 的新建议,主要的变化是不再允许用户代码直接运行在 Compositor 线程。这种变化的原因在于,游览器厂商担心如果有大量的或者低效的用户代码阻塞住了 Compositor 线程,页面将没有反应,动画也将停滞。

新的 API 被重新命名为:Animation Worklet。在 Animation Worklet 里,用户代码不会直接运行在 Compositor 线程。Compositor 线程会尽力和这个线程保持同步。所以,如果代码效率实在“惨不忍睹”,Compositor 线程可能会对其有一定的弱化处理。

为了说明 Animation Worklet,下面先简单讲讲 Worklet 和 Web Animations。

什么是 Worklet

前面提到过,Layout、Paint 和 Composite 的接口主要是通过 Web Worker 的机制实现的。而 Worklet 的接口是一个轻量版的 Web Worker 的版本。借助这个接口,Web 开发者可以获得渲染引擎的底层部分的访问权限,从而使得高性能的诉求称为可能。

Worklet 被严格限制用途,不能随意创建。目前有四类 Worklet:PaintWorklet、AudioWorklet、AnimationWorklet 和 LayoutWorklet。

各种 let 只有一个公用的方法:addModule,返回一个 Promise。Promise 的 resolve 时,已将 URL 所示的脚本模块加载到了当前的 Worklet。

interface Worklet {
    [NewObject] Promise<void> addModule(USVString moduleURL, optional WorkletOptions options);
};

dictionary WorkletOptions {
    RequestCredentials credentials = "same-origin";
};

上面的接口定义了一个标准的 Worklet 接口,读者可在这里 阅读 Worklet 标准的所有细节。

什么是 Web Animations

那么,做为动画本身也有自己的标准,就是:Web Animations。Web Animations 本身又被分为 CSS Animations 和 CSS Transitions 两类。未来这些 API 会在这些接口的基础上丰富更多的功能。使用它们,不需要再去针对不同浏览器作出 hack,也不需要额外的 requestAnimationFrame。理想的目标是,调用这些接口,浏览器可以使用内部的优化机制,达到最优的体验。

Web Animations 定义了 Animation、KeyframeEffect、AnimationTimeline、AnimationEvent、DocumentTimeline、EffectTiming 等接口。是 Animation Worklet 运行动画的基础。

MDN 的这篇文章详细地描述了标准的分类、对象、主要方法,并给出了几个有意思的例子。

细看 Animation Worklet

除了主线程、Compositor 线程,我们又引入了 Animation Worklet 线程。在 Animation Worklet 中,动画运行在 Animation Worklet 线程上下文中,并且,暴露 Web Animations 标准中定义的接口。为了显示的最终一致性,这里存在一个主线程、Compositor 线程和 Animation Worklet 线程同步的流程。

上面这幅图简单的说明了,独立的 Animation Worklet 线程、Compositor 线程是如何与主线程同步的流程。这里以 Chrome 的实现的数据流向为例:从宏观上,主线程发送动画的状态如创建、删除、当前时间偏移等等信息,最终到达 Animation Worklet 线程。Animation Worklet 线程将实际起作用的动画时间回传给主线程。

这里为了不互相阻塞,主线程和 Animation Worklet 线程中间存在一个 Compositor 线程。主线程和 Compositor 线程,Compositor 线程和 Animation Worklet 线程之间于是存在有有两类的数据同步流程:

第一、Animation Worklet 接收到更新可视元素的信号,在动画播放器上运行每一个动画。所有动画播放完毕,记录下每一个动画时间,发给 Compositor 线程。Compositor 线程接收到消息后按时序生成一个新的状态记录。 第二、主线程在文档生命周期中每一次运行动画帧之前,需要和 Compositor 线程进行一次动画属性状态的同步。

Animation Worklet 给出的接口定义如下:

[Exposed=Window]
partial namespace CSS {
    [SameObject] readonly attribute Worklet animationWorklet;
};

最终实现的接口挂在全局的 CSS 对象上。

[Exposed=AnimationWorklet, Global=AnimationWorklet]
interface AnimationWorkletGlobalScope : WorkletGlobalScope {
    void registerAnimator(DOMString name, VoidFunction animatorCtor);
};

方法registerAnimator定义一个动画执行器,动画执行器的名称可以为一个字符串;animatorCtor为一个动画类。

前文曾经提到的数据同步图,每一个黑色的实心原点代表一个动画类实例。将它展开,就是下面这个图。每个动画类都包含一个 animate 方法,这个方法是动画执行的入口,Worklet 通过这个入口控制动画的执行。

在 Chrome 的实现下,上面的两图可以合成如下一幅图:

Animation Worklet 的使用场景都有哪些呢?

  1. 大多数的滚动相关的场景的动画和效果都是 Animation Worklet 的用武之地,比如滚动条、滚动过程中的快照、平滑的滚动动画、固定滚动栏等等。
  2. 可以使用硬件加速的 CSS 属性变化动画,也即单独使用 Compositor 线程可以完成的动画。如:opacity 的变化。

演示例子

让我们来看一个形象的例子。

这个例子里,我们要实现的是:随着滚动条的滚动,页面上部的进度条不断前进;同时,在第一屏以下,逐渐显示订阅按钮,按钮逐渐变大,opacity 值越来越高。

打开 devtools 我们可以看到,动画的发生,不会触发 Layout 和 Paint,这是我们能够顺利使用 Animation Worklet 的前提。同时,我们需要实时获取滚动的进度。Animation Worklet 的 API 正好可以满足我们的需求。

在主页面上,我们引入 Polyfill 和动画文件:

<!-- HTML (scripts) -->
<!-- Polyfill checks and loads (if needed)
    both Web Animation API and Animation Worklet polyfills -->
<script src="polyfill/anim-worklet.js"></script>

<script src="animator.js"></script>

这里animator.js的代码如下:

/* animator.js (load a Worklet module) */
window.animationWorkletPolyfill.addModule('worklet.js')
    .then(()=> {
        onWorkletLoaded()
    }).catch(console.error);

这里说明一下,目前 Polyfill 的实现,会挂载在 window 的animationWorkletPolyfill对象上,如果今后原生支持了,对象一般会通过 CSS.animationWorklet 访问。这段的实现代码大致是:

...
    // Returns true if AnimationWorklet is natively supported.
    function hasNativeSupport() {
      for (var namespace of [scope, scope.CSS]) {
        if (namespace.animationWorklet && namespace.animationWorklet.addModule)
          return true;
      }
      return false;
    }
...
  // Create scope.CSS if it does not exist.
  scope.CSS = scope.CSS || {};

  // Create a polyfill instance but don't export any of its symbols.
  scope.animationWorkletPolyfill = MainThreadAnimationWorklet();

  scope.animationWorkletPolyfill.load();

接下来我们来看看,worklet 里面的写法。

/* worklet.js - register and apply animations */
// Animators are classes registered in the worklet execution context
registerAnimator(
    'scroll-position-animator',// animator name
    class { // extends Web Animation
        constructor(options) {
            this.options = options;
        }

        // currentTime, KeyframeEffect and localTime concepts
        // from Web Animation API
        // animate function with animation frame logic
        animate(currentTime, effect) {
            // scroll position can be taken from option params
            // const scrollPos = currentTime * this.options.scrollRange;

            // each effect will apply the animation options
            // from 0 to 100% scroll position in the scroll source
            effect.children.forEach((children) => {
                // currentTime is a Number,
                // which represent the vertical scroll position
                children.localTime = currentTime * 100;
            });
        }
});

定义完 worklet,我们回过头来补充 animate.js

/* animator.js (onWorkletLoaded() ) */
const scrollPositionAnimation = // animator instance
    new WorkletAnimation(
        'scroll-position-animator', // animation animator name
        [ // animation effects
            new KeyframeEffect(scrollPositionElement, [ // scroll position
                {'transform': 'translateX(-100%)'}, // from
                {'transform': 'translateX(0%)'} // to
                ],
                {duration: 100, iterations: 1, fill: 'both'} // options
            ),
            new KeyframeEffect(subscribeElement, [ // size and opacity
                {'transform': 'scale(0.5)', 'opacity': 0}, // from
                {'transform': 'scale(1)','opacity': 1} // to
                ],
                {duration: 100, iterations: 1, fill: 'both'}) // options
        ],
        new ScrollTimeline({ // animation timeline
            scrollSource: document.querySelector('.page-wrapper'),
            orientation: 'vertical'
        })
    );
scrollPositionAnimation.play(); // start and apply the animation

现在的状态

目前,各大主流浏览器都在努力对 Houdini 标准进行实现,这里面 Chrome 浏览器是最积极的一个。下面这张图,说明了各个浏览器对 API 的支持情况,以及 W3C 组织在各组 API 上的进展。

我们来仔细看下这张图。绿色的是已经有完整的实现;浅黄色的是有部分的实现;深黄色是正在开发中;紫色的是已经有实现的意愿;红色的是暂时还没有进度。

具体到 Animation Worklet,目前标准正在非常活跃的迭代中,直到本文撰写时刻(2018 年 9 月),已经有 6 次工作草案的发布,最近一次为 2018 年 9 月 6 日,这里是最新的标准版本。

在实现方面,暂时还只有 Chrome 有部分支持,不过不要紧,已经有比较成熟的Polyfill来实现这些既有的标准特性,而且,主流的现代浏览器都可以比较完美的支持。

做为 Animation Worklet 标准的活跃推进者,Chrome 已经计划在未来支持除了 CSS 加速属性之外的所有 CSS 属性动画,如果实现,这将是更加令人兴奋的特性。未来会怎样?让我们拭目以待。

Houdini 的官方给出了一些例子供大家感受,读者可以尝试这些例子。这里需要注意一点,这些 API 只对 localhost 域名和 https 的域名可见。这两种情况之外,这些 API 不可用。目前有 Polyfill 可用。如果需要一些原生的支持,目前(2018 年 10 月)请下载最新的 Canary 版 Chrome,同时,打开--enable-experimental-web-platform-features选项。

Web 工程师现在能做什么

包括 Animation Worklet 在内的 Houdini 标准还是在扫雷和实验阶段。虽然已经达成了广泛的共识,并且有主流的浏览器和 W3C 不断的推进,但是对于大规模生产实践应用,还尚有距离,标准不排除会有变化的可能。

笔者衷心感谢可以坚持读到这里的读者。大家是真心关心 Web 标准并对技术有着前瞻性了解希望的,这也是标准得以发展并推进的原始动力之一。包括 Houdini 在内的 Web 标准发展需要广大 Web 开发者、浏览器供应商的共同努力。

笔者认为,大家可以在以下几个方向给 Houdini 的发展作出贡献。

  1. 关心 Houdini 的发展,可以在他们的 github 上提出 issue、PR 等等帮助他们提出问题、解决分歧。也可以开发一些真实可用案例,去实现一些在如今难以被实现的样式或布局。
  2. 同一般软件产品不一样,浏览器的用户分为最终用户和 Web 工程师两类。浏览器厂商需要倾听 Web 工程师方面的痛点、需求。集中反馈的需求可能得到优先的响应。
  3. 关注官方提供的一些例子。为将来到来的 Houdini 特性做好准备,当然也可以帮助他们纠错:-)

致谢

设计师王旋 mm,为本文设计的精美题图,在此表示诚挚的谢意。

参考资料

  1. https://developers.google.com/web/fundamentals/performance/rendering/
  2. https://blog.csdn.net/Joel_h/article/details/72400100
  3. https://github.com/w3c/css-houdini-drafts/tree/master/css-animationworklet
  4. https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/
  5. https://javascript.ruanyifeng.com/htmlapi/requestanimationframe.html
  6. https://segmentfault.com/q/1010000000645415
  7. https://css-tricks.com/myth-busting-css-animations-vs-javascript/
  8. http://www.chinaw3c.org/member-meetings.html
  9. https://developer.mozilla.org/en-US/docs/Web/API/Worklet
  10. https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API
  11. https://drafts.css-houdini.org/css-animationworklet/
  12. http://dassur.ma/things/animworklet/
  13. https://css-houdini.rocks/
  14. https://googlechromelabs.github.io/houdini-samples/
  15. https://docs.google.com/document/d/1MdpvGtnK_A2kTzLeTd07NUevMON2WBRn5wirxWEFd2w/edit#heading=h.w09fmb4pxgin
  16. https://slides.com/malyw/houdini#/46
  17. https://ishoudinireadyyet.com/
  18. https://chromium.googlesource.com/chromium/src/third_party/+/master/blink/renderer/modules/animationworklet/README.md
  19. https://qiita.com/taichitk/items/010c154c407a7b22dfc4
  20. http://www.w3cplus.com/css/css-houdini.html

阅读全文 »