Kyrie On.

今天被问到一个问题:Vuex 中的数据如何用 v-model 绑定到组件?

Vuex 的基本原则

我们知道,Vuex 的规则是:

对于 state 的变更,同步用 commit / mutations,异步用 dispatch / actions

对于 v-model 常用的场景,就是表单的输入输出,通常可以定义为一种同步的数据变更,即采用 commit / mutations 的方式。

v-model 是什么?

v-model 是语法糖。

  1. 通过 v-bind 绑定数据
  2. 通过 input 或 change 事件传递变更的数据

Solution

经过上面两部分的梳理,就很容易把两者相结合起来。

<input :value="message" @input="updateMessage" />
computed: {
    ...mapState({
        message: state => state.message
    })
},
methods: {
    updateMessage (ev) {
        this.$store.commit('updateMessage', ev.target.value);
    }
}
mutations: {
    updateMessage (state, message) {
        state.message = message;
    }
}

Better Solution

<input v-model="message" />
computed : {
    message: {
        get() {
            return this.$store.state.message;
        },
        set(value) {
            this.$store.commit('updateMessage', value);
        }
    }
}

The Last and the Most Important

对于没有第一时间答上来这个问题,事后想想还是很羞耻的,毕竟曾经在工作中解决过这类问题。暴露出来的事情是:缺乏总结,对于一种或一类问题,可以对它的解决方案进行提炼和思考,以至于下一次再碰到的时候不会出现这种似曾相识但又很陌生的尴尬感。
搜索引擎是个好产品,它可以帮助人们解决各种各样的问题。但如果太依赖它的话,人会逐渐失去独立思考和解决问题的能力。
如果要成为一个好的工程师,在解决问题的同时,要定期复盘,这样才能得到真正的成长。

阅读此文
post @ 2018-12-13

什么是懒加载

对于一些资源文件(图片等),只有真正用到的时候才去加载它(发请求),在这之前用体积更小的占位图替代。这么一来就为用户节省了不必要的流量开销。

实现思路

就图片而言,只需要在用户看到或者即将看到的时候。把它 load 出来就可以了。所以这里需要判断图片容器是否进入了页面的可视区域 viewport

两个很关键的属性

为了达到上面那一 part 黑体字的目的,我们需要知道两个东西:

  1. 用户设备 viewport 的高度
  2. images 和 viewport 的相对距离

window.innerHeight

获取用户设备 viewport 的高度,如果需要兼容 IE,那么可以用 document.documentElement.clientHeight 作为备选。

getBoundingClientRect

这个方法非常有用,会返回一组“坐标”数据,MDN 给的定义是:

该方法的返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合, 即:是与该元素相关的 CSS 边框集合。DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right 和 bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。

需要注意的是,这里的 top / right / bottom / left 的定义如下图:

DOMRect

所以,只需要判断 前者 - 后者 >= 0 就能说明相应的元素进入了屏幕的可视区域。

撸起袖子

首先,我们的 DOM 结构是这样子的

<style>
    .lazyload-img {
      width: 200px;
      height:200px;
      background-size: cover;
      background: url("https://kyrieliu.cn/placeholder.png");
    }
</style>
...
<div class="lazyload-img" data-src="http://kyrieliu.cn/head.png"></div>

第一步,获取屏幕的高度

const screenHeight = window.innerHeight || document.documentElement.clientHeight;

第二步,获取相应图片容器元素距离 viewport 顶部的距离(假设只有一个图片),满足条件时开始加载真正的图片。

const img = document.querySelector('.lazyload-img');

function lazyload(){
    let distance = screenHeight - img.getBoundingClientRect().top;
    if (distance >= 0){
        let src = img.getAttribute('data-src');
        img.style.background = `url("${src}")`;
    }
}

最后,添加滚动事件

window.addEventListener('scroll', lazyload, false);

一个小 demo

Done.

阅读此文

最近发现了一个由局部变量引起的Bug,在低版本的浏览器上几乎是必现。
先看一下这段代码:

1
2
3
4
function report(src) {
let reporter = new Image();
reporter.src = src;
}

这是一段常见的代码:在前端通常使用 image 对象进行数据的上报。
但是,这段代码在低版本的浏览器上,并不是每次都会发起 HTTP 的请求,进而导致数据的丢失。

丢失的原因

在上面那段代码中,reporter 是 report 函数中的局部变量,当函数调用完成后,局部变量随即被销毁,而此时浏览器很有可能还没有来得及发出请求。

解决方案

既然定位到了问题是由被销毁的局部变量导致的,那么很容易想到利用闭包解决这个问题。

1
2
3
4
5
6
7
8
let report = (function(){
let reporters = [];
return function(src) {
let reporter = new Image();
reporters.push(reporter);
reporter.src = src;
}
})();
阅读此文
post @ 2018-11-06

当我们谈论前端跨域时,我们到底在说什么?

iframe 跨域

在使用上来说,iframe 跨域是比较麻烦的一种(创建新元素 -> 处理跨域交互),但是伟大的邓小平同志说过:

不管是黑猫还是白猫,只要能抓住老鼠,就是好猫。

所以,作为一种经典的跨域方式,还是要把它单独提溜出来念叨念叨。
根据使用场景和方式的不同,iframe 跨域分为以下几种:

  • document.domain
  • location.hash
  • postMessage

document.domain

适用场景:不同子域,相同主域
举个栗子,有两个 url,分别是:https://kyrieliu.cn/a.htmlhttp://www.kyrieliu.cn/b.html(主域是相同的)。
这个时候,b 页面通过 iframe 内嵌在 a 页面中,iframe 的 onload 事件是由 a 中的脚本制定的函数,用以获取 b 中的某个全局变量。
需要将二者的 document.domian 同时设置为 kyrieliu.cn 即可完成这一次的跨域。

location.hash

一个页面和从属于它的 iframe 之间可以互相读取和修改 URL,但还是有一定的前提:父窗口对子窗口进行 url 的读写时,随意;子窗口对父窗口的 url 进行读写时,受到同源策略的限制。所以在这种情况下,子窗口需要借助一个“代理窗口”去修改父窗口的 url。
通信的交互过程如上。
接下来再讨论为什么 hash 可以实现传递数据的需求。
对于每一个 url 来说,hash 就是 # 后面的部分,这一部分通常用来做当前页面的锚点定位,所以服务器(后端)是不会关心这一部分的,从而可以交给前端来搞一些“骚操作”。但也有一个不可避免的缺点:会造成一些不必要的 history 记录。

大致实现

比如在 url 为 kyrieliu.cn/index.html 的页面 A 内 append 了一个 src 为 google.com/index.html 的 iframe。

  • A 向 iframe 传递数据
    1. A 中直接修改 iframe 的 src 为 google.com/index.html#name=kyrieliu
    2. iframe 中通过 hashchange 事件即可拿到数据
  • iframe 向 A 传递数据
    1. iframe 中再创建一个 iframe,后者的 src 和 A 同源并且通过 hash 带上想要传递的数据,如:kyrieliu.cn/proxy.html#age=23
    2. 子 iframe 通过 parent.parent.location.hash = self.location.hash.substring(1)将数据传递给 A
    3. A 通过 hashchange 事件拿到对应的数据

postMessage

这个方法就比较简洁明了:父窗口和子窗口都可以作为数据的发送方和接收方,且不需要考虑是否同源。还是用上面的栗子,比如现在需要从父窗口向子窗口发送数据:

// A.js
const iframe = document.querySelector('#iframe');
const targetOrigin = 'google.com';
iframe.contentWindow.postMessage('hello', targetOrigin);

// iframe.js
window.addEventListener('message', function(ev){
    // 验证发送方
    if (ev.origin === 'kyrieliu.cn') {
        console.log(ev.data);
    }
}, false);

这里注意 postMessage 的调用方式: whichWindow.postMessage(message, targetOrigin);

JSONP 跨域

这是一种古老且稳定的跨域方式,兼容性好,但只支持获取数据(GET)。它的原理是:

  1. 前端通过 <script> 的形式向后端发起请求,并在参数中告知将用于处理数据的函数名,同时在前端定义这个函数。
  2. 后端返回的不是纯数据,而是用前端告知的函数名包裹数据,传递到前端以后也就变成了一段可执行的 js 代码

CORS 跨域

CORS(Cross Origin Resource Sharing)的中心思想是:通过自定义的 HTTP Header 让浏览器和服务器进行“沟通”,决定本次请求的成功与否。
一般情况下,前端开发人员需要做的就是:

  1. 找到对接的后端大佬
  2. 对他说:“老哥,帮我设置一下跨域头(Access-Control-Allow-Origin)”
阅读此文
post @ 2018-09-23

所谓好的代码啊……

是高内聚低耦合的设计,处处体现出的优雅感

是查看方法时时感受到的,从始至终清晰而流畅的思路

是每一处编码的干净整洁,伴随而来的身心愉悦

是伴随着代码规模增大时,归纳出的统一的抽象

是面对需求与排期矛盾时,做出恰到好处的取舍

是面对未来的不确定性时,依靠直觉的准确预测

是在无人办公室敲击键盘时的行云流水

是在面对系统复杂性时的庖丁解牛

是面对挑战时的热情,是面对挫折后的执着

是灵感,是创造,是技巧

是每个 coder 深思熟虑之后记录下来的思想

是一个正在挑战世界的十六岁少年,而绝不会是四十岁中年人一样浑浑噩噩过完一天算一天的代码

Repost from Weibo: 蛋疼的axb

阅读此文
post @ 2018-09-02

我是在说 Event-loop 啊喂!

阅读此文
post @ 2018-08-29

今天,哦不,昨天,是让人惊喜的一天。这惊喜当然不是来自于日常工作,而是来自于开源社区。
移动端调试在几年前可能还是前端开发的一个痛点,不过随着开发工具的不断完善以及开源社区的大佬们无私的奉献,这一“难题”也变得不那么难。但是今天偶遇到的解决方案,却可以说是惊艳到我了。

我们为什么需要移动端调试

随着移动浪潮的到来,越来越多的页面需要呈现在用户的手机上,前端的产品形态重心也慢慢从 PC 转向 mobile,就我个人来说,入职一年多,绝大多是都在开发移动端的网页(公司使用 Hybrid 的开发模式)。在开发 PC 页面时,Chrome 和 Firefox 提供了很好的开发者工具(aka:控制台),其中涵盖了一个前端工程师所需要的全部工具,话虽这么说,站在我的角度上来讲,开发一般的页面,Console 和 Network 就足够了。
以上是传统的 PC 页面,概括的来讲,开发之前按下 F12,一切都是那么的自然。
到了移动端,手机上可没有浏览器给你玩,怎么办?
凉拌。
常规操作,毕竟是在电脑上进行开发,所以通用的解决办法是:

  1. 打开 Chrome!
  2. F12!
  3. Toggle device toolbar!

Chrome浏览器自带了移动设备模拟功能,所以只需点击控制台左上角那个带有手机/pad icon 的图标,即可进入移动设备模式,并且伴有主流设备的选项可供选择,几乎解决了移动端样式调试的需求。
这个方法没毛病,但还是存在一定的局限性:比如无法定位不同设备(真机)由于浏览器内核的不同出现的差异性。再比如,通过 js 调用了一些 native 的接口,是否调用成功以及如果失败了会有什么错误信息?
在电脑浏览器上模拟设备尺寸的调试并不是真正的移动端调试。
所以,我们需要一个真正意义上的移动端调试的方法,可以脱离电脑的束缚,在移动设备上获得页面的一切信息。

vConsole

秉着 ios/android 无差别共用的精神,这里直接略过了一些只针对单个操作系统的 hack,比如 chrome 的 Remote devices + 数据线的调试方式(既然不是通用方案,我就不关心了)。
理想中的方案就是在需要调试的页面调出控制台。
已经有很多开源项目做到了这件事情,vConsole就是其中之一(微信出品,必属精品)。
通过对浏览器控制台的“模拟”,vConsole 实现了:

  • 查看打印日志
  • 查看网络请求
  • 查看页面元素
  • 查看 cookie 和 localStorage
  • 在控制台执行 js
  • 自定义插件

但是就算有了这样的工具,在开发甚至是上线的过程中,还是会有一些问题无法覆盖到,比如:

  • 开发人员鲁莽上线,直接把 vConsole 带到生产环境
  • 开发人员再次鲁莽上线,js 运行时错误,影响交互(致命)

AlloyLever

抛了这么多砖(说了这么多废话),终于该引玉了,也就是我在文章一开始说的惊艳到我的东西。
AlloyLever 是鹅厂的 dntzhang 结合了 vConsole 的强大之后开发出的一款堪称大杀器的移动端调试工具。换句话说,真正的调试功能还是借助 vConsole 完成的,但是对使用场景和调用方式进行了拓展:

  • 支持错误监控和上报
  • 支持预埋机关(DOM/Url)唤起 vConsole

不得不说,高水平的前端工程师是有魔法的。
看了源码之后,发现实现原理并不复杂,而且很像我最近在做一个坑爹需求时的实现方式:异步加载CDN 上的 js。但我却没有更进一步的思考:这种实现方式是否可以继续拓展,得以更好的解决其他类型的问题。
至于预埋机关唤起调试模式,可以说是相当骚了,完美解决了如何在用户侧定位异常的问题,在下五体投地。

最后

在了解了实现机制之后,争取自己实现一遍(立个 flag):不求源码一致,但求上手顺滑。

阅读此文

两个月以前在公众号发过一个图片消息,标题是 How to compare two objects in JavaScript,有一个关注了我的同事第二天告诉我说看不懂。看不懂是结果,而为什么看不懂则是导致这一结果的过程。我试着揣测了她看不懂的过程,大概有这些原因:

  • 只有代码没有注释,阅读时心理抵触
  • 阅读时心理状态较为浮躁(这也是现在公众号读者普遍的难关)
  • 对 JavaScript Object 没有充分的理解

这是站在读者角度的分析。若是站在笔者角度,最大的问题就是:只有代码没有注释。当然了,这个锅我是不背的,毕竟这类消息的目标用户从不是包含着上述三个特征的读者。
而现在我把这个问题又拎了出来,强化一下记忆。

如何比较?

说了这么多废话,到底如何比较呢?

===大法好

能想到的第一个方法必然是全等比较,如果obj_1 === obj_2这条表达式返回的结果是 true 的话,则说明两个对象的内存地址相同,即:本就是一个对象。在 JavaScript 中,只要不是NaN,一个变量总是和自身相等的。

如果不全等呢?接下来就要凭借着对 Object 对象的了解,手动比较了。

函数比较

在 JavaScript 中,函数也是对象的一种,所以我们先考虑一下,如果要比较的是两个函数该怎么办。
回忆一下你是如何区分两个函数的。
看函数名,看参数,看函数中的语句。如果我们能把函数转换成所有内容组成的字符串,是不是就很直观了?
所以在这里,我们只需要调用toString方法,将结果进行比较即可。

时间对象的比较

除了函数之外,同样符合object身份的Date对象也需要用特殊的办法进行比较。
这个倒也简单,将两者用getTime方法转换成时间戳,再进行比较,即可。

原型比较

这是个老命题了。
因为对象的可继承属性,决定了一个对象不止有自己内部定义的key-value对,如果需要的话,还要考虑到对象原形链上可访问到的属性。
可以用Object.getPrototypeOf方法获得一个对象的原型(这里说“原型”是翻译自函数名,但不太准确,“父对象”更为合理,二者是继承关系),再将获得的对象进行比较。

键值对比较

这也是我们要做的最后一步:看对象的每一个键值对是否相等。

  1. 获取对象内所有的key;
  2. 比较每个key对应的value是否相等;
  3. 如果某个key对应的value是对象,递归

完整Demo

function isDeepEqual(obj1, obj2, testPrototypes = false) {
    if (obj1 === obj2) {
        return true;
    }

    if (typeof obj1 === 'function' && typeof obj2 === 'function') {
        return obj1.toString() === obj2.toString();
    }

    if (obj1 instanceof Date && obj2 instanceof Date) {
        return obj1.getTime() && obj2.getTime();
    }

    const isPrototypeEqual = testPrototypes
    ? isDeepEqual(
        Object.getPrototypeOf(obj1),
        Object.getPrototypeOf(obj2),
        true
    )
    : true;

    const obj1Props = Object.getOwnPropertyNames(obj1);
    const obj2Props = Object.getOwnPropertyNames(obj2);

    return (
        obj1Props.length === obj2Props.length &&
        isPrototypeEqual &&
        obj1Props.every(prop => isDeepEqual(obj1[prop], obj2[prop]))
    );
}
阅读此文
post @ 2018-08-19

什么是浏览器缓存?

Browser Caching 是浏览器将网络资源存储在本地的一种行为。
优点有:

  1. 减少冗余数据的传输
  2. 减轻服务器的压力
  3. 缩短网页的加载速度

可以看出,浏览器缓存与性能优化有着千丝万缕的联系。

浏览器缓存的分类

  1. 协商缓存
  2. 强缓存

听名字可以看出来,这两类缓存,一个属于细心的暖男,另一个则是强硬的霸道总裁。
好,参照这篇文章,我们先来了解一下浏览器缓存的过程:

  1. 首先,浏览器会去检查该资源有关缓存的HTTP Header。一个expires,一个cache-control,看是否命中强缓存。如果命中则直接从缓存中得到该资源(这次请求不会和服务端进行通信)。
  2. 如果走到了这一步,说明我们的霸道总裁没有被翻牌子,那么协商缓存这个暖男就该出动了。浏览器会发送一个请求到服务端,这个请求会携带一些东西上一次请求返回的有关缓存的header字段(Last-Modified/If-Modified-Since、Etag/IF-None-Match),服务端拿到这些信息后,对比结果看是否命中协商缓存。如果命中,服务端只返回新的响应header更新缓存中的对应header信息(不返回对应资源),浏览器get到以后直接从缓存中取得该资源;若没有命中,服务端返回最新的资源内容。

强缓存

先来看看这个霸道总裁好了。
强缓存主要利用Http Response Header中的两个字段来控制,分别是ExpiresCache-Control

Expires

Expires的值是一个绝对时间的字符串,GMT格式,比如:Expires:Thu, 26 Jan 2017 23:39:02 GMT。这个时间表示的是该资源的有效期限,当请求处于这个时间之前时,均视为命中缓存。
缺点:因为这个时间表示的是资源过期的时间,所以当客户端和服务端有较大的时间偏差时,咳咳,情况就不妙了。

Cache-Control

Cache-Control是HTTP1.1时出现的头信息,主要利用其max-age的值来进行判断,Cache-Control:max-age=1800,表明这个资源的有效期是1800秒,不难看出,和Expires不同,这是一个相对时间。

协商缓存

所谓协商缓存,就是协商嘛,这种情况下客户端和服务端地位是平等的,不像之前说的强缓存,服务端一个header字段,浏览器就要唯命是从。
准确的讲,

协商缓存是由服务端来确定缓存资源是否可用,这个情况下,需要客户端与服务端通过某种标识来进行通信,从而让服务端判断请求的资源是否可以通过缓存来访问。

这个过程涉及到两组字段(成对出现的字段):

  1. Last-Modified and If-Modified-Since
  2. Etag and If-None-Match

上述1和2中的前者都是在第一次请求的响应头中存在的字段,后者则均存在于后续请求的请求Header中。
成对出现的表现在于:如果响应头中没有前者,则后续的请求头不会有后者

Last Modified/If-Modified-Since

当服务端接收到来自浏览器的第一个请求时,服务端在响应的头中会带上Last-Modified,是一个GMT的时间字符串,代表着该资源的最后修改时间,例如Last-Modified: Wed, 26 Jan 2017 00:35:11 GMT。
当浏览器再次请求相同的资源时,请求的头中会包含一个If-Modified-Since,这个值就是第一次请求该资源时返回的Last-Modified。服务端会拿这个字段的值和资源上次修改的时间进行对比,如果相同,则说明资源没有改变,即命中缓存,此时返回304,不会返回相应的资源和Last-Modified。

Etag/If-None-Match

和上一对不同的是,这一对字段的值不是时间,而是一个校验码。Etag保证每个网络资源都是唯一的,资源变化都会导致Etag的变化。当服务端再次收到请求时,根据客户端上送的If-None-Match来进行判断,看是否命中协商缓存(过程与Last-Modified/If-Modified-Since相似)。
当两对字段一起使用时,Etag的优先级大于Last-Modified。

最后

这是2017年写的一片文章,但最近发现,这真的是一道高频出现的面试题目,所以搬到这个博客上来,算是记录一下吧。

阅读此文
post @ 2016-08-20

在刚搭好这个博客的时候,会有一篇名为 Hello World 的初始文章,算是系统自动生成的。但是我想了一下,这不是我第一次搭博客了:上一次要追溯到大学时代,用腾讯云的校园优惠券,买了一台服务器,并且开始了我生命中的第一段博客之旅。既得当时还申请了 kyrieliu.cn 的域名,那时觉得这是一件很酷的事情。
不知不觉中 2018 年已经过了一大半,自从毕业后就放弃了继续养着服务器的想法,改用了 Github Pages 和微信公众号为自己提供可以写字的地方。但还是有些社交的意味在里面。公众号天然的关注/粉丝机制,让我越来越苦恼应该写什么内容,这么说来我也算是有一点取悦型人格了,我觉得这样不好,但也没办法全然不顾他人的看法。
于是乎,为了能有一个地方可以随心所欲的抒发一些东西,我决定再次捡起博客这种看似有些古老的写作方式。安静就好。
我可能不会费尽心机的把我在其他平台上的粉丝引到这里来,那样会导致这里失去本身存在的意义。
所以这篇命名为 Hello World Again

阅读此文
关注我
⬆︎TOP