浏览器跨tab通信的几种办法

什么场景下需要跨tab通信? 有一些日常中很常见的场景,如果支持跨tab通信,可能能给用户更好的体验。 博客Dark Mode切换 想象你在浏览 Dan Abramov的博客 ,你同时打开了好几个tab学习一系列知识。夜晚来临,你将一个页面切换成了Dark Mode。当你阅读完这一篇去阅读下一篇时,页面却不是以Dark Mode呈现的。你不得不按下F5刷新页面来寻求一致的阅读体验。 需要频繁登录的CMS系统 想象你在网店管理系统中管理你网店中的商品,你打开了好几个tab,分别管理商品类型,商品详情以及商品库存。你正在管理你的商品详情,突然你的登录session过期,你点击重新登录。但是当你切换到商品库存页面时,你发现你依旧无法操作,需要再次登录。你不得已点击了登录按钮,和你辛苦编辑了10分钟但没有保存的商品库存说了拜拜。 以上场景其实无伤大雅,你的系统依旧运转,核心功能依旧正常。但是借助跨tab通信,你能给用户提供更好更顺滑的体验。 Cookie & IndexedDB 要实现数据的一致,很容易想到的就是统一的数据存储。在有后端加入的时候,后端其实就是前端的统一数据存储。在纯前端的环境下,比较常见的数据存储包括 cookie localStorage sessionStorage IndexedDB sessionStorage 由于只存活于单个tab session中,所以不能用于跨tab通信。localStorage 有其他的应用方式,下面会深入分析。所以在这个章节我们主要关注cookie和IndexedDB。 实现思路 实现思路其实非常简单,就是通过轮询数据源,来确保每个tab的数据都是最新的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // Cookie Way // Producer Tab, 当然你也可以用js-cookie这种库更高效的操作cookie document.cookie = "blabla" // Consumer Tab setInterval(() => { const cookie = document.cookie; //Use Cookie to do something doSomething(cookie) }, 1000) // IndexedDB way // Producer Tab const transaction = db.transaction(["customers"], "readwrite"); const objectStore = transaction.objectStore("customers"); customerData.forEach(function(customer) { const request = objectStore.add(customer); request.onsuccess = function(event) { // do some callback }; }); // Consumer Tab setInterval(() => { const transaction = db.transaction(["customers"]); const objectStore = transaction.objectStore("customers"); const request = objectStore.get("444-44-4444"); request.onsuccess = function(event) { doSomething(request.result.name); }; }, 1000) 需要注意的是,cookie的读写是同步的,在cookie比较大的时候可能会阻塞主线程造成页面卡顿。而IndexedDB的API都是异步的,所以不会有cookie同步读写的性能问题。 ...

2021年10月14日 · 2 分钟 · hateonion

层叠上下文和层叠顺序

为什么 z-index 不工作了? 在某些场景下,我们经常会发现z-index不知为何就不工作了,比如下面的场景。(demo:stackblitz demo ) 1 2 3 <div class="block-a">block-a z-index 1</div> <div class="block-b">block-b z-index 2</div> <div class="block-c">block-c z-index 3</div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 .block-a { background: #ffb3a7; z-index: 1; position: relative; top: 60px; opacity: .8; width: 500px; height: 90px } .block-b { background: #fff143; z-index: 2; opacity: .8; height: 100px; width: 320px; position: fixed; } .block-c { background:#bddd22; opacity: .8; z-index: 3; width: 200px; padding-top: 300px; } div { color: #50616d; font-size: 24px; text-align: center; padding-top: 40px; border: #50616d 1px dashed; } block-a的 z-index 是 1, block-b 的 z-index 是 2, block-c 的 z-index 是3。 按照之前的理解,覆盖关系应该是block-c覆盖block-b覆盖block-a。但是实际结果却如下图。 ...

2019年10月11日 · 5 分钟 · hateonion

重学前端学习笔记(1)--Js类型

基础类型 Undefined Null Boolean String Number Symbol Object boolean这个类型比较简单,不单独作解释。Symbol这个类型太复杂,后面单独总结。 Undefined 和 Null undefined并不是一个关键字。我们日常访问的undefined其实是访问的global级别的undefined变量,undefined变量的值是基础类型undefined。但是在现代浏览器中,undefined都不能被覆写。 undefined代表一个变量初始化了但是没有赋值,任何变量初始化前值都是undefined。 null是JavaScript的关键字,表示的是一个空值。 所有变量定义都不要赋undefined值,这样会让代码难以理解(到底是代码没有赋值还是?) String string的最大长度是2^53-1,但是长度并不是指代字符数,而是字符串的UTF16编码的和,比如需要计算一个emoji的长度,你会发现是2。 字符串方法比如length和charAt都是基于字符串编码和的。 字符串一旦构造便不可更改,属于值类型。 Number Js中除开规定有理数,还规定了NaN,Infinity,-Infinity Js中有+0和-0,在加减法中没有问题,在乘除法中,如果是 x * -0 会得到 -0,x/-0 会得到 -Infinity。 Js中的Number类型范围是-2^53-2^53,如果不使用BigInt,只能表示这个范围之内的整数 非整数类型的Number无法使用 == 或者===来进行比较,0.2 + 0.1 == = 0.3 会返回false,如果想要得到正确的匹配结果,请使用 Math.abs(0.1 + 0.2 -0.3) <= Number.EPSILON Object Js中对象是属性的集合。属性分为数据属性和访问器属性,以key-value结构进行存储,key可以是字符串或者是个Symbol类型。 Js中的类和Java中的类有一些不同,Java中的类代表的是真正的类型,但是Js中的类只是运行时对象的一个私有属性。 Number/String/Boolean和Symbol这四个基本类型都自带对应的构造器,如果使用 new 操作符,会返回一个对象类型而不是基本类型。 Js中的基本类型和对象的边界很模糊,可以直接在基础类型上调用方法,比如"abc".charAt(0),也可以通过给原型添加方法给基础类型增加方法。 为什么基础类型可以调用对象方法?本质上是 . 运算符提供了装箱操作 ,这个运算符会根据基础类型构造一个临时对象。 类型转换 值转换表如下 StringToNumber String到Number的类型转换支持十进制/二进制/八进制和十六进制 30 0b111 0o13 0xFF 同时还支持科学计数法 ...

2019年8月26日 · 1 分钟 · hateonion

RxJS入门指南

Prerequisite(先导知识) 本文是RxJS的入门知识,在文章中会涉及到大量的JavaScript示例代码,想要更好的理解本文,你需要具备以下知识。 理解基本的ES6和TypeScript语法 了解过观察者模式 Observable? Observer? 在RxJSObservable和Observer。要想很好的理解两个概念,我需要借用一个生活中很常见的例子,买房。 在这个图中,我们的购房者一直在密切的关注我们的房价。房价随着时间波动,购房者可能会根据波动的房价而采取一系列的行动,比如购入或者继续观望。购房者与房价的这样一种关系其实就构成了一种观察者关系。套用到观察者模式中, 房价 – Observable 购房者 – Observer 购房者观察房价 – Subscribe(订阅) 再结合买房的例子,我们可以很学术的描述Observable和Observer的行为。 Obserable 可被观察(房价被购房者关注),并且随时间变化发出(emit)不同值(房价波动)。 Observer 观察Observable(购房者关注房价),并在Observable(房价)发出不同值(房价波动)时做出响应(买房或者观望)。 Observable和Observer之间通过订阅(Subscription)来建立观察关系。 当Observable没有Observer的时候,即使发出了值,也不会产生任何影响(无购房意愿者不会响应房价波动) RxJS中的Observable和Observer 有了基本的Observable和Observer的概念,我们再来看看RxJS中的Observable和Observer。 创建一个Observable 我们可以调用 Observable.create 方法来创建一个Observable,这个方法接受一个函数作为参数,这个函数叫做 producer 函数, 用来生成 Observable的值。该函数的参数是observer,调用observer.next()就可以生成一个有一系列值的Observable。 1 2 3 4 5 6 import { Observable } from 'rxjs'; const observable = Observable.create(observer => { observer.next('foo'); observer.next('bar'); }) 但是运行这段代码后并不会发生任何事情,我们需要一个Observer去Subscribe这个Observable,然后Observer基于Observable发出的值做出响应。 Subscribe一个Observerable 我们通过下面的这段代码去Subscribe一个Observable 1 2 3 4 5 6 7 8 import { Observable } from 'rxjs'; const observable = Observable.create(observer => { observer.next('foo'); observer.next('bar'); }) observable.subscribe(console.log) 运行代码,console中就会依次打印 foo / bar 了 ...

2019年7月17日 · 5 分钟 · hateonion

图片懒加载从简单到复杂

为什么要做图片的懒加载 假设在用户访问某个页面时就加载这个页面全部的图片(即使这些图片并不处在用户的当前的视窗中),在弱网环境或者网速较慢的环境下,这些“冗余”图片的下载会占用用户本来就非常有限的带宽,伤害用户体验(比如影响其他资源的下载)。所以对于网站的图片,理想的做法是懒加载(按需加载)。 图片懒加载的原理 在浏览器内部对于各种资源有着一套自己的优先级定义 ,浏览器会优先加载优先级高的资源 如果我们不去进行图片的懒加载,默认情况下,资源的priority如下。 这些优先级标记为high的图片会占用其他资源的下载带宽,可能会造成某些比较关键的资源(比如xhr call)加载缓慢,拖慢页面速度。 图片懒加载的简单实现 图片懒加载的思路一般时当页面加载时加载一个尺寸很小的占位图片(1kb以下),然后再通过js选择性的去加载真正的图片。 一个最简单的的实现如下 1 2 <!-- index.html --> <img src="placeholder.jpg" data-src="real_image.jpt" /> 1 2 3 4 5 6 7 8 9 10 // index.css img[data-src] { filter: blur(0.2em); } img { filter: blur(0em); transition: filter 0.5s; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // index.js (function lazyLoad(){ const imageToLazy = document.querySelectorAll('img[data-src]'); const loadImage = function (image) { image.setAttribute('src', image.getAttribute('data-src')); image.addEventListener('load', function() { image.removeAttribute("data-src"); }) } imageToLazy.forEach(function(image){ loadImage(image); }) })() 通过懒加载之后,资源优先级如下。 ...

2019年1月30日 · 2 分钟 · hateonion

Deep In React(五)setState中的黑魔法

以下代码全部基于React15(React16代码太复杂了看不懂哇- -)。 setState不一定是同步的 在React官方文档中有这么一句话state-updates-may-be-asynchronous 。 下面这两个很经典也是新人很容易糊涂的场景就是由上面这句模棱两可的话带来的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Demo extends Component { state = { count: 1 } onClickHandler = () => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 } render() { const { count } = this.state; return ( <button onClick={this.onClickHandler}>{count}</button> ); } } class SetTimeoutDemo extends Component { state = { count: 1 } onClickSetTimeoutHandler = () => { setTimeout(() => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 2 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 3 }, 0); } render() { const { count } = this.state; return ( <button onClick={this.onClickSetTimeoutHandler}>{count}</button> ); } } stackBlitz的demo在这 Clicker 和 SetTimeoutClicker ...

2019年1月14日 · 3 分钟 · hateonion

Js中的防抖与节流

为什么需要防抖和节流? 日常与浏览器打交道比较多的前端开发者对于浏览器的各种事件想必都不会陌生,我们会针对某一个特定的事件绑定对应的响应函数,在事件被触发时让浏览器自动调用该函数。 1 2 3 4 5 function onResizeHandler() { // do something on resize } window.addEventListener('resize', onResizeHandler); 如果只是一些触发频率很低的事件,那么上面的代码并没有什么问题。但是如果像resize这样可能在短时间内被频繁触发的事件(比如click/keydown/touchStart等),我们不去做任何的处理的话,可能导致事件的响应函数在短时间内被大量的触发,造成浏览器卡顿,伤害用户体验。 而节流和防抖一个很经典的应用场景就是去控制我们的事件的触发,节省浏览器开销。 什么是防抖 JavaScript中的防抖指的是只有在x ms内没有调用该函数,才会真正调用该函数。如下图 该图上方是事件触发的次数,下方是响应函数触发的次数,可以发现在事件大量被触发时(色块密集),响应函数并没有被触发。当事件停止触发一段事件后(三个色块的时间间隔)后,响应函数才会被真正的触发。 如果你想自己试试上面的例子,可以访问这个 codePen 。 防抖的简单实现 防抖函数其实是一个高阶函数,接收一个函数和防抖事件作为参数,返回值为防抖处理的函数。 的实现其实很简单,我们需要的是是一个定时器,如果在定时器的定时结束前,响应函数被触发的话则重置这个定时器的时间。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function debounce(fn, wait, leading=false) { /** @type {number} */ let timer; /** @type {number} */ let lastCallTime; /** @type {boolean} */ let isInvoked = false; return function debounced(...args) { const context = this; const thisCallTime = Date.now(); if(leading) { if(!isInvoked) { fn.apply(context, args); isInvoked = true; } if(thisCallTime - lastCallTime >= wait) { fn.apply(context, args); } lastCallTime = Date.now(); return; } clearTimeout(timer); timer = setTimeout(() => fn.apply(context, args), wait); } } 需要注意的是,上面的函数添加了leading参数。传统的防抖函数响应函数第一次触发也需要等待x ms,使用该参数可让响应函数立即进行触发。 ...

2019年1月2日 · 2 分钟 · hateonion

Deep In React (四) stack reconciliation

前提 本篇文章中所有的Host Component均以DOMComponent为例。 Internal Instance更新 在之前我们建立了Internal Instance的mount和unmount方法用来处理Internal Instance的挂载和卸载。为了完成更新功能,我们需要建立一个叫receive的方法。 Composite Component更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CompositeComponent { receive(nextElement) { const previousElement = this.currentElement; const previousProps = previousElement.props; const publicInstance = this.publicInstance; const previousRenderedComponent = this.renderedComponent; const previousRenderedElement = previousRenderedComponent.currentElement; // 更新 this.currentElement = nextElement; const type = nextElement.type; const nextProps = nextElement.props; let nextRenderedElement; if (isClass(type)) { if (publicInstance.componentWillUpdate) { publicInstance.componentWillUpdate(nextProps); } publicInstance.props = nextProps; nextRenderedComponent = publicInstance.render(); } else if (typeof type === "function") { nextRenderedComponent = type(nextProps); } } } 上面的这个render方法对应的是当Composite Component的type没有发生改变的时候,我们只对对应的component进行更新,而不是每次有任何更新都进行重新的mount。而这一个过程同样也是一个递归的过程。 ...

2018年11月18日 · 3 分钟 · hateonion

Deep In React (三)Internal Instance

从一次挂载说起 如果你写过React代码,那么ReactDOM.render(<App />, container) 可能是你最熟悉的一段代码了。<App />是一个React Element,container是一个node节点。这个方法是把React element映射到真正的DOM结构的一个触发器,当你调用这个方法之后,会把React Element渲染成virtual DOM Tree。 首先有必要提一下Element和virtual DOM Tree这两个概念。 React Element可能会存在type为Component Element的节点。而Virtual DOM Tree指代完全映射为真实DOM结构的树,所有节点的type都是string类型。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { // React Element Tree type: App, props: { children:[{ type: 'div', ... }, { type: Button, ... }] } } // 最终渲染成的virtual DOM Tree { // Virtual DOM Tree type: 'div', props: { children:[{ type: 'div', ... }, { type: 'button', ... }] } } } 渲染是怎么发生的? React在接受<App />这个Element的时候,其实是不知道这个Element的深度是多少,每个节点对应的DOM元素是什么的,<App />这个Element下,可能只是一个简单的<div />,也可能是许多复杂组件的组合。因此,React需要自顶向下进行递归的渲染,最终得到一个对应到真实DOM结构的Virtual DOM树。 ...

2018年11月12日 · 5 分钟 · hateonion

Deep In React (二) Component,Element和渲染

先导知识: React Component和React Element 在React中,有着组件(component)和元素(element)两个概念。我们日常可能听到过很多次Component这个名词,但是对于Element这个名词在React中似乎聊得并不多。今天就来给大家讲讲什么是Element,什么是Component,以及他们和React渲染的关系。 React Element React Element从数据结构上来讲就是简单的plain object, 这个plain object会提供对于整个React组件树的描述。 1 2 3 4 5 6 7 8 9 10 11 { type: 'button', props: { classNames: 'foo', children: { type: 'span', props: { children: 'click' } } } 讲这个Element翻译成HTML就是如下结构 1 2 3 <button class="foo"> <span>click</span> </button> 需要注意的是type这个属性,在上面的例子中,type是一个string,而type是string的element React将其定义为HostElement。Host的意思是这个元素会根据不同的平台对应不同的Native View。比如ReactDOM会对应成相应的HTML Tag,而React-Native则会对应成Native View。 而除开HostElement, React还支持Component类型的element。我们在React代码中经常会这样组合我们的组件。 1 2 3 4 5 6 <Container> <Header /> <Sidebar /> <div>content</div> <Footer /> </Container> 这段代码对应生成的element的描述如下 ...

2018年11月1日 · 3 分钟 · hateonion