teobler

teobler 查看完整档案

成都编辑东北大学  |  计算机科学与技术 编辑teobler  |  前端开发 编辑 teobler.com 编辑
编辑

失眠多梦 bug体质

个人动态

teobler 发布了文章 · 6月1日

SSO里面的SAML和OIDC到底讲了啥

本文首发于我的博客 https://teobler.com 转载请注明出处

SSO是什么

在了解SSO是什么之前,我们需要搞清楚两个概念: Authentication & Authorization

Authentication(又被称为AuthN,身份验证),它指的是 the process of verifying that "you are who you say you are",也就是说这个过程是为了证明你是你。通常来说有这么几个方式:

  • Single-factor - 也就是可以通过单一的因素证明”你是你“,比如密码、PIN码
  • Multi-factor - 有时候 Single-factor 没有办法保证”你是你“,就会需要一些多重验证的手段,比如动态口令、生物识别等
  • 上面提到的是两种用的比较多的手段,还有一些其他的,比如安全问题,短信,email认证等等

Authorization(又被称为AuthZ,权限验证),他指的是 the process of verifying that "you are permitted to do what you are trying to do",也就是说这个过程是为了证明你是否拥有做这件事的权限,比如修改某个表格等等,如果没有权限的话通常会返回 403 错误码。

SSO 出现之前,用户在不同的系统登录就需要在不同的系统注册多个账号,然后需要自己记住多个用户名和密码,而如果这些系统是同一个平台的话,其实该平台还需要维护多套几乎一模一样的登录系统,给用户和平台都带来了负担。

SSO 就是一种authentication scheme(身份验证方案),SSO 允许用户使用同一个账户登录不同的系统,很好地解决了上述的问题。它不但可以实现单一平台的登录,假如某个平台之外的系统是信任该平台的,那么外部系统也可以集成这个平台的 SSO, 比如现在的大多数网站都提供了 Google 账号登录的功能。

要实现 SSO,首先需要你正在开发的系统信任这个第三方登录系统所提供的用户信息,然后你需要按照一定的标准和协议去与其对接,接下来我们会介绍两个比较常用的 SSO 协议 -- SAML 2.0 和 OIDC。

SAML 2.0

SAML 2.0是什么

SAMLSecurity Assertion Markup Language 的简称,是一种基于XML的开放标准协议,用于在身份提供者(Identity Provider简称IDP)和服务提供商(Service Provider简称SP)之间交换认证和授权数据。

SAML 2.0是该协议的最新版本,于2005年被结构化信息标准组织(OASIS)批准实行。

流程

SAML flow

  1. 用户要访问SP上面的资源,但是SP要求提供用户身份信息
  2. SP 会将用户跳转到 IDP,IDP 返回一个登陆页面
  3. 用户成功登陆后IDP会提供一个 SAML Assertion,通过用户端传递给SP
  4. 此时SP会验证这个 SAML Assertion,没有问题的话就会返回相应的资源
  5. 如果后面用户又要访问该平台的另外一个系统,由于该用户已经在IDP那边登录过了,所以此次访问IDP就能够直接将用户的 SAML Assertion 传递给这个新的SP,便可以实现不登录直接访问资源

SAML Assertion

那么这个 SAML Assertion 又是个啥呢?

首先用户为什么可以访问SP上的资源呢?肯定是因为其实SP也有一份用户的资料,这个资料里面可能有用户的账户信息,权限等等,但是现在用户的信息是由IDP提供的,所以现在需要做的就是讲两份用户信息映射起来,好让SP知道IDP提供的用户到底是哪一个。

而这个映射的约定,就是SP和IDP进行集成的时候的一个配置,这个配置叫做 metadata。这个配置有两份,两边各一份,里面约定了应该怎么去映射用户信息,签名的证书等。IDP和SP会通过别的方式去交换这两份 metadata

所以其实 SAML Assertion 里面包含了用户的唯一标识,能够证明该用户是谁。在SP拿到这份信息后就会按照一些规则去验证里面的信息是否是合法的用户。

那么问题来了,如果中间人知道了我们之间的规则随便塞了一份信息进来咋办?所以其实 SAML Assertion 里面除了用户信息其实还有IDP的签名,只有SP先解析了里面的签名确认无误之后才会信任这份信息。

知道了 SAML Assertion 是个啥以后,我们还需要弄清楚它是怎么发送出去的。要弄清楚它们是怎么发送出去的我们需要知道一个东西叫做 binding method

SAML 2.0 有许多不同的 binding,它们其实就是 SAML Assertion 的交互方式:

  • HTTP redirect binding
  • HTTP POST binding
  • HTTP artifact binding
  • SAML URI binding
  • SAML SOAP binding(based on SOAP 1.1)
  • reverse SOAP(PAOS) binding

其中现在用的比较多的是前三种,它们都是基于HTTP协议来实现的。

  • redirect binding是在SP redirect到IDP的时候会在URL中带上请求信息,比如id,谁发出来的等等
  • POST binding是为了解决redirect方式在使用过程中的一些问题而产生的,比如URL过长,response不安全等等
  • artifact可以看做一个引用,这个引用会通过浏览器带到SP那边,SP拿到之后再通过里面的信息去IDP拿相应的 SAML Assertion

metadata

上面提到过 metadata 是为了让IDP和SP明白彼此交流的信息,并且有一些安全考虑,里面主要的信息有:

  • NameFormat -- 约定用户ID的格式,比如 email address, transient等等
  • Certificate -- 解析签名,加密assertion
  • Entity identifier -- 该metadata的唯一标识符
  • Binding -- 使用何种方式通信

其中有一个字段 md:KeyDescriptor 在SP中有一个 encryption,在SP和IDP进行通信建立信任的时候,IDP就会拿到SP加密的key,在用户登录成功后,IDP就会用这个key加密 SAML Assertion,SP拿到后通过自己的私钥进行解密。另一个叫 signing 的字段会被用来解析对方的签名,用来辨别这个 Assertion 是不是我想要的人发过来的。

OIDC

OIDC是什么

OpenID Connect(OIDC) 是建立在 OAuth 2.0 协议之上的一个简单的身份层,它允许计算客户端根据授权服务器执行的认证,以 JSON 作为数据格式,验证终端用户的身份。它是 OpenID 的第三代规范,前面分别有 OpenID 和 OpenID 2.0。它在OAuth 2.0 的基础上增加了 ID Token 来解决第三方客户端标识用户身份认证的问题。

它的结构如图所示:

image-20200601085202978

从它的结构图中可以看出,除了核心实现外,OIDC 还提供了一系列可选的扩展功能。比如:

  • Discovery:发现服务,使客户端可以动态的获取 OIDC 服务相关的元数据描述信息(比如支持那些规范,接口地址是什么等等)
  • Dynamic Registration :可选。动态注册服务,使客户端可以动态的注册到OIDC的OP
  • Session Management :Session管理,用于规范OIDC服务如何管理Session信息
  • OAuth 2.0 Form Post Response Mode:针对 OAuth2 的扩展,OAuth2 回传信息给客户端是通过URL的 querystring 和 fragment 这两种方式,这个扩展标准提供了一基于 form 表单的形式把数据 post 给客户端的机制

由于图片距今已经有些年限了,其实现在OIDC还提供了许多可选的扩展,具体可到官网查看。

流程

由于 OIDC 是基于 OAuth 2.0 的,所以 OIDC 也拥有多种 flow。由于篇幅所限我这里会相对详细地解释 Authorization Code Flow,在开始前我们需要弄清楚几个名称:

  • EU:End User,指使用终端(浏览器等)访问服务器资源的用户
  • RP:Relying Party,用来代指 OAuth2 中的受信任的客户端,身份认证和授权信息的消费方,相当于 SAML 中的 SP
  • OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息,相当于 SAML 中的 IDP
  • ID Token:JWT格式的数据,包含 EU 身份认证的信息。ID Token 由 JWS 进行签名和 JWE 加密,从而提供认证的完整性、不可否认性以及可选的保密性。里面可能会有很多字段,详细可以看这里,其中这几个字段是一定包含其中的

    • iss - Issuer Identifier:提供认证信息者的唯一标识,通常是一个 HTTPS 的 URL
    • sub - Subject Identifier:iss 提供的 EU 的标识,在 iss 范围内唯一,它会被 RP 用来标识唯一的用户,最长为255个ASCII个字符
    • aud - Audience(s):标识ID Token的受众,必须包含 OAuth 2.0 的client_id
    • exp - Expiration time:过期时间,超过此时间的 ID Token 会作废不再被验证通过
    • iat - Issued At Time:JWT的构建的时间
  • UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPS
  • APP Token:通常来说 OP 提供的用户信息和 Access Token 中包含的信息不带有用户在 RP 中的权限,RP 通常会自己生成一个 token 给 EU 作为后续访问资源的用户证明

Authorization Code flow

Authorization Code flow

  1. EU 访问 RP 的资源但是没有进行身份认证
  2. RP 将 EU redirect 到 OP 端,并带上一些参数,这里列举一些必选参数,还有许多可选参数可以看这里

    • client_id:唯一标识
    • scope:请求权限范围,OIDC的请求必须包含值为“openid”的scope的参数
    • response_type:要求 OP 的返回值,值为 codetokenid_tokennone 中的一个或几个,在当前 flow 值为 code
    • redirect URL:认证完成后的跳转URL
    • state:当前登录认证操作的一个随机 query,用于防止 CSRF 或 XSRF 攻击
  3. 然后 OP 会验证 EU 的身份信息,通常会询问用户是否将自己的信息提供给 RP,确认后进行登录操作
  4. 登陆成功后 OP 会将 EU redirect 到刚刚 RP 提供的 URL,同时会在 URL 中带上一个 Authorization Code 和刚刚的 state 参数
  5. 之后 RP 拿到 code 和 state,先确认是不是相同的 state 保证这次通信是有效的,之后再通过 POST 请求从 OP 获取 token,里面包含 ID Token,Access Token,Refresh Token,Token Type,Expired In 等信息
  6. 之后 RP 会验证 ID Token验证 Access Token 确保它们没有问题
  7. 然后 RP 通过 Access token 通过 OP 提供的 UserInfo Endpoint 获取用户信息,拿到用户信息后与自己的用户信息进行比对
  8. 最后返回一个 APP Token 到 EU

Implicit Flow

Implicit Flow 是在 OP redirect EU 到 RP 的时候会带上 ID Token 和 Access Token(如果必要) 而不是 Authorization Code,同时在发送请求的时候也会有一些不同,需要带上一些别的参数,这里就不细讲了,总的流程是差不多的,详情可以查看这里

Hybrid Flow

Hybrid Flow 可以理解为上面两个 flow 的结合,OP redirect EU 到 RP 的时候会带上 Authorization Code,同时根据发送请求时候 Response Type 参数的不同还会带上一些别的参数,具体流程可以参考这里


非常感谢你能看到这里,如果你觉得有帮助到你,可以关注我的微信公众号
gongzhonghao.jpeg

查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 5月23日

(2)你真的会用Chrome devtool吗?

本文首发于我的个人博客: https://teobler.com

Performance

Performance API

有的时候我们可能会想测试一下用户的某一个操作要消耗多少时间,而通常一般人会这么做:

const start = new Date().getTime();// do your workconst end = new Date().getTime();console.log(end - start);

使用 performance API,我们可以这么做:

performance.mark("start");// do your workperformance.mark("end");performance.measure("your work name", "start", "end");console.log(performance.getEntriesByType("measure"));

之后在控制台里我们可以看到这样的 console 信息

image-20200522145519948

同时在 chrome 的 performance tab 里面,如果你进行相应的记录,会产生一个在 User Timing 标题下的可视化的图表

image-20200522145610966

Networking

network面板

首先老规矩我们来介绍下面板:

image-20200522145644585

  • 中间带有毫秒数的部分是一个瀑布流,它显示了该网页中每一个资源加载花费了多长时间,不同的颜色标识了不同的加载阶段
  • 下面的表格详细展示了每一个资源的信息

    • 第一栏是该资源的名字
    • 第二栏是该资源的加载状况,也就是网络请求的状态码
    • 第三栏是类型,标识该资源是document / stylesheet / script...
    • 第四栏是该资源的调用者,这里的index指的就是index.html调用的改资源,将鼠标放在某个资源上然后按住shift,devtool会用绿色为你标识出该资源的调用者,红色标识出该资源又调用了哪些其他的资源
    • 第五栏是大小,但是注意这里有两个大小,第一个是该资源的实际大小,第二个是该资源压缩后的大小,如果该资源来自于cache,这里就会显示cache
    • 第六栏是加载该资源花费的时间,同样对应了压缩前和压缩后的时间
    • 最后一栏显示了该资源的详细加载的过程

其中最后一栏中有不同的颜色,它们代表了不同的意义:

  • 白色 - 该资源在队列中,这通常表明:

    • 这个资源被渲染引擎推迟加载以把时间让给更重要的资源(比如styles和script),通常图片的渲染会被延迟
    • 端口被占用在排队
    • 浏览器的TCP连接满了,所以在排队(在HTTP1中浏览器只能一次同时建立6个连接)
    • 创建磁盘缓存所花费的时间也会被标记为队列等待
  • 灰色 - 阻塞:

    • 你的请求还没发出去,可能发生在代理查找或是队列由于不知道啥原因阻塞了
    • 与代理服务器连接所花费的时间
  • 深绿色 - DNS查找: 通常在你访问一个你从来没有访问过的域时会稍微长些,因为没有缓存
  • 橙色 - 连接初始化 / 正在连接: 建立连接的时间,比如TCP连接的三次握手 / 四次挥手
  • 深橙色 - SSL连接正在建立
  • 绿色 - 请求已经发送 / 正在发送 / 等待: 这段时间是请求发送的一瞬间到我们接收到第一个字节的间隙,如果这段时间不同寻常的长的话,通常我们的服务器网络配置或者服务器本身有问题,比如一段sql查询很慢,那么绿色的这段时间就会很长
  • 蓝色 - 资源下载: 指的是资源开始下载到下载完成的这一段时间

实例

接下来我们来看几个例子,看看这些颜色到底有什么用:

image-20200522145708160

这张图很明显是下载的文件太大了

image-20200522145721320

这张图后端可能出问题了

image-20200522145734992

这张图的总耗时并不长,但是可以看到各种时间都挺长,从DNS查找的时长来看可能是第一次访问一个新的域名,导致所有的连接时间都比较长

image-20200522145750029

这张图表明你一次性下载的资源过多,导致队列等待时间过长

截图

在network面板最上方有一个消摄像机的图标,点击后刷新页面devtool会记录下加载网页过程中的每一次重绘的时间点并截图,你可以从中看到整个网页加载的过程,你可以使用这个功能来测试你的网站如果在网络环境比较差的情况下如何加载的,用户体验是否友好等等。

在截图完成后你可以双击某一个图片放大,然后使用左右箭头来预览不同时刻你的网页加载情况。

同时你可以用这个功能来提升一些微小的性能问题,比如加载太多的图片时如何加载比较好等等。比如你在加载一个比较大的资源的时候如果阻塞了其他所有资源的加载的话是否可以考虑将这个资源延迟加载会比较好,不然用户将没有办法看到网页上的数据。

Auditing

auditing可以判断你网页的一些问题,比如加载时长,SEO,用户体验等等。

在auditing面板中google集成了Lighthouse,这是google开发的专门用于分析网页问题(比如SEO,性能,最佳实践等等)的一个工具,它的使用很简单,打开audit面板,进入你想测试的网页,勾上想要测试的内容,点击generate report按钮就可以了,之后一段时间的分析,Chrome将为你的网站进行一系列的分析和评分,在有问题的地方还能够给出一些建议,帮助你解决网站的各种问题。

image-20200522145802055

在报告的最上面可以充看到报告的一个整体情况,点击某一个分数后就能够跳转到相应的板块,我们以performance为例

image-20200522145813872

在图中我们可以看到Chrome认为有问题的项目就会被标红,点击右边的按钮可以得到更多的信息,然后点击learn more可以看到一些google的建议,总的来说这个面板对于提升我们的网站体验有着不错的效果。你可以通过这个工具更加熟悉你的网站,可以看到那些地方是还可以改进的,哪些地方是做的不错的。

在这里可以介绍一个差不多的网站叫做sonarwhal,它的功能也跟devtool里面的audit类似

Node.js Profiling

在启动node server的时候如果在命令行中加上--inspect的话,就可以在Chrome中的console中看到一个Node的logo,点击后可以打开node专用的devtool,在这里你可以在sources中进行一切操作,同时这里有一个profile页面,选中相应的server并点击start按钮后,刷新相应的页面,在刷新完成后再回来点击stop,这时Chrome会展示在刚刚的一次刷新中你的server做了什么:

image-20200522145826064

图中的横轴是时间,纵轴是这次刷新页面server端的调用栈。那么这玩意儿是干嘛用的呢?profile是为了能够让开发者直观地看到自己的Node server端的一些调用情况,在出问题时可以马上看到一个调用消耗的时间很长,那么就可以定位到代码中,看是不是相关的调用发生了不可预知的错误,及时修正。在这个profile中点击相应的function将直接跳转到相应的source code。

如果你喜欢我的文章,请帮我点个赞吧,同时也可以关注我的公众号

查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 5月16日

React Concurrent Mode 之 Suspense 实践

本文首发于我的个人博客: https://teobler.com ,转载请注明出处

自从三大框架成型之后,各个框架都为提升开发者的开发效率作出了不少努力,但是看起来技术革新都到了一个瓶颈。除了 React 引入了一次函数式的思想,感觉已经没有当初从DOM时代到数据驱动时代的惊艳感了。于是 React 将精力放在了用户体验上,想让开发者在不过多耗费精力的情况下,用框架自身去提升用户体验。

于是在最近的几个版本中,React 引入了一个叫做 Concurrent Mode 的东西,同时还引入了 Suspense,旨在提升用户的访问体验,React 应用在慢慢变大以后会慢慢变得越来越卡,这次的新功能就是想在应用初期就解决这些问题。

虽然现在这些功能还处在实验阶段,React 团队并不建议在生产环境中使用,不过大部分功能已经完成了,而且他们已经用在了新的网站功能中,所以面对这样一个相对成熟的技术,其实我们还是可以来自己玩一下的,接下来我来带领大家看看这是一个什么样的东西吧。

Concurrent Mode

WHAT

那么这个 Concurrent Mode 是个啥呢?

Concurrent Mode is a set of new features that help React apps stay responsive and gracefully adjust to the user’s device capabilities and network speed.

在官网的解释中, Concurrent Mode 包含了一系列新功能,这些新功能可以根据用户不同的设备性能和不同的网速进行不同的响应,以使得用户在不同的设备和网速的情况下拥有最好的访问体验。那么问题来了,它是怎么做到这一点的呢?

HOW

可停止的rendering (Interruptible Rendering)

在通常状况下,React 在 render 的时候是没有办法被打断的(这其中有创建新的 DOM 节点等等),rendering 的过程会一直占用 JS 线程,导致此时浏览器无法对用户的操作进行实时反馈,造成了一种整个页面很卡的感觉。

而在 Concurrent Mode 下,rendering 是可以被打断的,这意味着 React 可以让出主线程给浏览器用于更紧急的用户操作。

想象这样一个通用的场景:用户在一个输入框中检索一些信息,输入框中的文字改变后页面都将重新渲染以展示最新的结果,但是你会发现每一次输入都会卡顿,因为每一次重新渲染都将阻塞主线程,浏览器就将没有办法相应用户在输入框中的输入。当然现在通用的解决办法是用 debouncing 或者 throtting。但是这个方式存在一些问题,首先是页面没有办法实时反应用户的输入,用户会发现可能输入了好多个字符页面才刷新一次,不会实时更新;第二个问题是在性能比较差的设备上还是会出现卡顿的情况。

如果在页面正在 render 时用户输入了新的字符,React 可以暂停 render 让浏览器优先对用户的输入进行更新,然后 React 会在内存中渲染最新的页面,等到第一次 render 完成后再直接将最新的页面更新出来,保证用户能看到最新的页面。

image-20200516110545540

这个过程有点像 git 的多分支,主分支是用户能够看到的并且是可以被暂停的,React 会新起一个分支来做最新的渲染,当主分支的渲染完成后就将新的分支合并过来,得到最新的视图。

可选的加载顺序(Intentional Loading Sequences)

在页面跳转的时候,为了提升用户体验,我们往往会在新的页面中加上 skeleton,这是为了防止要渲染的数据还没有拿到,用户看到一个空白的页面。

Concurrent Mode 中,我们可以让 React 在第一个页面多停留一会,此时 React 会在内存中用拿到的数据渲染新的页面,等页面渲染完成后再直接跳转到一个已经完成了的页面上,这个行为要更加符合用户直觉。而且需要说明的是,在第一个页面等待的时间里,用户的任何操作都是可以被捕捉到的,也就是说在等待时间内并不会 block 用户的任何操作。

总结

总结一下就是,新的 Concurrent Mode 可以让 React 同时在不同的状态下进行并行处理,在并行状态结束后又将所有改变合并起来。这个功能主要聚焦在两点上:

  • 对于 CPU 来说(比如创建 DOM 节点),这样的并行意味着优先级更高的更新可以打断 rendering
  • 对于 IO 来说(比如从服务端拿数据),这样的并行意味着 React 可以将先拿到的一部分数据用于在内存中构建 DOM,全部构建完成后在进行一次性的渲染,同时不影响当前页面

而对于开发者来说,React 的使用方式并没有太大的变化,你以前怎么写的 React,将来还是怎么写,不会让开发者有断层的感受。下面我们可以通过几个例子来看看具体怎么使用。

Suspense

开始前的准备

与之前的功能不同的是,Concurrent Mode 需要开发者手动开启(只是使用 Suspense 貌似不用开启,但是我为了下一篇文章的代码,现在就先开启了)。为了方(tou)便(lan),我们用 cra 创建一个新的项目,为了使用 Concurrent Mode 我们需要做如下修改:

  • 删除项目中的react版本,该用实验版 npm install react@experimental react-dom@experimental
  • 为了正常使用 TypeScriptreact-app-env.d.ts 文件中加入实验版 React 的 type 引用

    /// <reference types="react-dom/experimental" />
    /// <reference types="react/experimental" />
  • index.tsx 中开启 Concurrent Mode

    ReactDOM.unstable_createRoot(
      document.getElementById("root") as HTMLElement
    ).render(<App />);
  • Suspense 中如果你需要在拿后端数据时”挂起“你的组件,你需要一个按照 React 要求实现的 "Promise Wrapper",在这里我选择的是 swr

    • 从后面的结果来看,目前 swr 对于 Suspense 的实现还没有完成,但是已经有一个 pr 了
本文中所有的代码都可以在我的 github repo 里找到,建议时间充裕的同学 clone 一份和文章一同食用效果更佳。

Data Fetching

React 团队在 16.6 中加入了一个新的组件 SuspenseSuspense 与其说是一个组件,更多的可以说是一种机制。这个组件可以在子节点渲染的时候进行”挂起“,渲染一个你设定好的等待图标之类的组件,等子节点渲染完成后再显示子节点。在 Concurrent Mode 之前,该组件通常用来作懒加载:

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

在最新的 React 版本中,现在 Suspense 组件可以用来”挂起“任何东西,比如可以在从服务端拿数据的时候”挂起“某个组件,比如一个图片,比如一段脚本等等。我们在这里仅仅以从后端拿数据为例。

在传统的数据获取中,往往是组件先 render 然后再去后端获取数据,拿到数据后重新 render 一遍组件,将数据渲染到组件中(Fetch-on-render)。但是这样就会有一些问题,最明显就就是触发瀑布流 -- 第一个组件 render 触发一次网络请求,完了以后 render 第二个组件又触发一次网络请求,但是其实这两个请求可以并发处理。

然后可能有人为了避免这种情况的出现就会来一些技巧,比如我在 render 这两个组件之前先发两次请求,等两次请求都完了我再用数据去 render 组件(Fetch-then-render)。这样的确会解决瀑布流的问题,但是引入了一个新的问题 -- 如果第一个请求需要 2s 而第二个请求只需要 500ms,那第二个请求就算已经拿到了数据,也必须等第一个请求完成后才能 render 组件。

Suspense 解决了这个问题,它采用了 render-as-you-fetch 的方式。Suspense 会先发起请求,在请求发出的几乎同一时刻就开始组件的渲染,并不会等待请求结果的返回。此时组件会拿到一个特殊的数据结构而不是一个 Promise,而这个数据结构由你选择的 "Promise wrapper" 库(在上文提到过,在我的例子里我用的是 swr)来提供。由于所需数据还没有准备好,React 会将此组件”挂起“并暂时跳过,继续渲染其他组件,其他组件完成渲染后 React 会渲染离”挂起“组件最近的 Suspense 组件的 fallback。之后等某一个请求成功后,就会继续重新渲染相对应的组件。

在我的例子中我尝试用 Suspense + swr + axios 来实现。

在 parent 中 fetch data

在第一个版本中我尝试在 parent 组件(PageProfile)中先 fetch data,然后在子组件中渲染:

const App: React.FC = () => (
  <Suspense fallback={<h1>Loading...</h1>}>
    <PageProfile />
  </Suspense>
);

export default App;
export const PageProfile: React.FC = () => {
  const [id, setId] = useState(0);
  const { user, postList } = useData(id);

  return (
    <>
      <button onClick={() => setId(id + 1)}>next</button>
      <Profile user={user} />
      <ProfileTimeline postList={postList} />
    </>
  );
};
export const useData = (id: number) => {
  const { data: user } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-user/${id}`, method: "get" },
    {
      suspense: true,
    },
  );
  const { data: postList } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-list/${id}`, method: "get" },
    {
      suspense: true,
    },
  );

  return {
    user,
    postList,
  };
};
import useSWR, { ConfigInterface, responseInterface } from "swr";
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";

export type GetRequest = AxiosRequestConfig | null;

interface Return<Data, Error>
  extends Pick<responseInterface<AxiosResponse<Data>, AxiosError<Error>>, "isValidating" | "revalidate" | "error"> {
  data: Data | undefined;
  response: AxiosResponse<Data> | undefined;
}

export interface Config<Data = unknown, Error = unknown>
  extends Omit<ConfigInterface<AxiosResponse<Data>, AxiosError<Error>>, "initialData"> {}

export const useRequest = <Data = unknown, Error = unknown>(
  requestConfig: GetRequest,
  { ...config }: Config<Data, Error> = {},
): Return<Data, Error> => {
  const { data: response, error, isValidating, revalidate } = useSWR<AxiosResponse<Data>, AxiosError<Error>>(
    requestConfig && JSON.stringify(requestConfig),
    () => axios(requestConfig!),
    {
      ...config,
    },
  );

  return {
    data: response && response.data,
    response,
    error,
    isValidating,
    revalidate,
  };
};

但是不知道是不是我的写法有问题,有几个问题我死活没弄明白:

  • 为了实现 render-as-you-fetch,文档中有提到过可以尽可能早的 fetch data,从而可以让 render 和 fetch 并行并且缩短拿到 data 的时间(如果我理解没错的话)

    • 我的想法是先在 parent 组件中 fetch data,然后用两个 SuspenseProfile 组件和 ProfileTimeline 组件包起来,然后就能够在拿到相对应的数据(user 和 postList)之后渲染相对应的组件
    • 但是在使用的过程中我发现 ”在哪个组件中 fetch data,就必须用 Suspense 将这个组件包起来,否则就会报错“,所以这里我将整个 PageProfile 包了起来。而这个时候就算我用两个 SuspenseProfile 组件和 ProfileTimeline 组件包起来也没办法实现两条加载信息,只会显示最外层的 loading,也就没有办法实现 render-as-you-fetch
    • swr 在这样的写法下会多发一次莫名其妙的请求,目前还没有找到原因

      • image-20200515111513161
      • 图中第一个第二个请求分别是请求的 user 和 postList 数据,但是在完了之后又请求了一次 user
  • swr 目前还没有实现在 Suspense 模式下避免 waterfall,所以两个请求会依次发出去,等待时间是总和,不过翻看github已经有 pr 在解决这个问题了,目前来看处于codereview的阶段

在当前组件中 fetch data

为了解决上面的问题,我换了一种写法:

const App: React.FC = () => {
  const [id, setId] = useState(0);

  return (
    <>
      <button onClick={() => setId(id + 1)}>next</button>
      <Suspense fallback={<h1>Loading profile...</h1>}>
        <Profile id={id} />
        <Suspense fallback={<h1>Loading posts...</h1>}>
          <ProfileTimeline id={id} />
        </Suspense>
      </Suspense>
    </>
  );
};

export default App
export const Profile: React.FC<{ id: number }> = ({ id }) => {
  const { data: user } = useRequest({ baseURL: BASE_API, url: `/api/fake-user/${id}`, method: "get" }, { suspense: true });

  return <h1>{user.name}</h1>;
};
export const ProfileTimeline: React.FC<{ id: number }> = ({ id }) => {
  const { data: postList } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-list/${id}`, method: "get" },
    { suspense: true },
  );

  return (
    <ul>
      {postList.data.map((listData: { id: number; text: string }) => (
        <li key={listData.id}>{listData.text}</li>
      ))}
    </ul>
  );
};

此时我将对应的请求放在子组件内,这样的写法不管是两个组件 loading 的状态,还是网络请求都是正常的了,但是按照我的理解这样的写法是不符合 React 的初衷的,在文档中 React 提倡在顶层(比如上层组件)先 kick off 网络请求,然后先不管结果,开始组件的渲染:

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

但是目前这种写法很明显是开始渲染子组件了才发的网络请求。关于这个问题我会等 swr merge完最新的 pr 更新下一个版本后再进行实验。

one more thing

除了上面介绍过的以外, Suspense 还给开发者带来了另外一个好处 -- 你不用再写 race condition 了。

在之前的请求方式中,首次渲染组件的时候你是拿不到任何数据的,此时你需要写一个类似于这样的判断:

if (requestStage !== RequestStage.SUCCESS) return null;

而在同一个项目中经不同人的手还会有这样的判断:

if (requestStage === RequestStage.START) return null;

而如果请求挂了,你还得这样:

return requestStage === RequestStage.FAILED ? (
  <SomeComponentYouWantToShow />
) : (
  <YourComponent />
);

这堆东西就是一堆模板代码,有些时候还容易脑抽就忘加了,在 Suspense 下,你再也不用写这些东西了,数据没拿到会直接渲染 Suspense 的 fallback,至于请求错误,在外层加一个 error boundary 就行了,这里就不过多展开了,详见文档

总的来说 Suspense 的初衷是好的,可以提升用户体验,可能现在各个工具包括 React 本身还处于实验阶段,还多多少少会有一些问题,接下来我会尝试去找找怎么解决这些问题再回来更新。下一篇我会接着躺坑 UI 部分。

欢迎关注我的公众号

查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 3月4日

(1)你真的会用Chrome devtool吗?

本文首发于我的个人博客: https://teobler.com 转载请注明出处

这是一个介绍Chrome devtool的系列文章,虽然这是一个前端小伙伴们都很熟悉的东西,但是我相信它的很多还不错的功能其实或许你并不知道,这个系列的文章会涉及到: 代码修改、debug、network、audit、monitor等等。

第一篇我们会介绍如何在devtool中做编辑和debug。

Editing

编辑HTML和CSS

首先是大家熟悉的HTML和Style,在elements的tab下,只需要双击某个元素的内容即可进入编辑模式,这时你可以随意修改其中的内容,同理在其右边的style栏,你可以使用相同的方式修改CSS样式。

保存你自己的调色板

在style栏中,点击任意颜色图标,你就可以打开一个调色板,此时再点击第二行的箭头,你就能打开一个预设的调色板保存界面。

color-palette-popup

里面预先保存了Material UI的配色方案,你也可以保存你自己的配色方案,这对于自己一个人想要开发一个项目却为配色方案捉急的开发者提供了便利,你可以平时收集一套你自己的配色方案进行保存并随时取用,因为这个调色板还提供了当前网站的配色方案,也就是说你可以打开一个你觉得配色不错的网站保存它的配色。

default-color-palette

滚动某个元素到页面内

这个功能对于一些无限滚动的网站开发更有用,比如你在elements tab下找到了你的目标元素,但是你想看看它长得像不像你想象中的样子,没有这个功能的话你就需要一直找,此时你只需要右键那个元素,选择Scroll into view即可。

隐藏和显示元素

你可以在某个元素上右键选择隐藏某个元素,其原理是Chrome为其加了一个visibility为hidden的class,右键又可以显示,当然你还可以选择删除,想让它回来的话ctrl/command z即可。

元素状态的改变

如图所示,选中某个元素后再点击:hov可以打开一个状态面板,这可以帮助你看清楚不同状态下该元素的CSS样式。

various-states

显性样式

在一个复杂的项目中,可能同一个元素上你覆盖了很多个样式,就拿body元素来说,浏览器有一个默认的样式,然后可能你使用了一些reset的样式,同时你在某个文件中还加入了别的样式,如果这个时候你想知道body的样式究竟是哪里来的,如果你用眼睛在styles的tab里面找就会很难。

这个时候我们可以选择第二个tab Computed 在这个tab里是最终显示在页面上的样式,并且如果你点击某个样式右边的按钮,就会跳转回style tab并把这个生效的样式高亮出来,帮助你快速找出准确的class。

computed-styles

查找事件监听

这个功能可以找到当前页面中的所有时间监听函数,并且点击右边的超链接可以跳转到对应的代码,这个功能在你“知道某个bug是因为某个监听引起的,但是不知道这个监听函数在哪”的时候很有用。

event

改变颜色格式

在任意颜色上按住shift + 左键可以改变颜色的格式(rgb, hsl, hex),可以方便的帮助你改变网站的颜色与设计相一致。

color-format

源代码format

通常在prod环境的代码都经过压缩,这会导致我们在看源代码的时候所有代码被压缩到同一行导致不可读,这时我们可以点击format按钮使得代码变回在编辑器中的样子。

pretty-print

DOM断点

有一些代码会影响DOM的渲染,然后如果这些代码产生了相应的bug,比如渲染出来的DOM不是你想的哪样,这时我们可以在DOM上打断点,这时如果DOM发生了改变,Chrome就会跳转的相应的代码的debug界面,是一个方便的debug方式。

可以选择在 子节点改变 / 属性改变 / 节点被删除 时进行断点调试。

DOM-break-on

元素选择历史

在HTML的界面,你选中某一个元素,然后打开console界面,输入$0将输出你刚刚选中的元素,$1则是你之前选中的上一个元素,以此类推,这可以帮助你在console页面对这个DOM节点做一些调试,不用你手动选择,同时需要说明的是Chrome的console页面是内置了一部分jQuery的,比如$选择器。

这里有一个小练习看你能不能用上边的知识完成。

debug

第二个我们用的比较多的功能就是debug了,这篇文章我们会介绍如何使用Chrome devtool来进行debug。

断点调试方法

第一种debug的方式是当你在本地启动了你的dev server,并且在你想要打断点的代码处写上debugger关键字,那么当你打开devtool访问你的页面并刷新页面时你就能停在debugger的地方。

第二种方式是打开devtool的sources tab,找到你想debug的文件(cmd + p / cmd + shift + p),然后在行号上点击,就会加上一个蓝色标记,这时如果你刷新页面也可以进入断点调试。

调试面板结构

debug-panel

  • Watch可以添加变量或者一段代码进行一些运算,这些变量或者运算会随着你的断点的变化而变化进行实时更新,方便进行调试,需要注意的是如果当前断点的上下文中找不到这个变量,会显示undefined
  • Call Stack就是调用栈,可以看到当前断点的调用信息,点击不同的行,可以跳转到不同的调用处
  • Scope显示了当前你可以访问的所有上下文
  • Breakpoints显示了你的所有断点,可以方便的在各个断点处跳转,也可以当做开关来用,可以在此处选择忽略哪些断点,或是直接删除不需要的断点
  • XHR Breakpoints可以添加网络请求的断点
  • DOM Breakpoints就是之前讲过的DOM断点
  • 同理Event Listener Breakpoints可以在触发相应的事件时产生断点进行调试

跳过黑盒代码

有时候我们会想要看看我们的当前的函数调用栈,但是通常来说我们会在我们的app里面引入许多第三方库,比如React,那么在你看调用栈的时候,你就会进入到React的调用栈中,然而我们并不想看React,所以我们可以选择隐藏这些调用栈:

blackbox-script

或者你嫌麻烦的话,可以直接在devtool的设置里将整个库都给隐藏:

blackbox-setting

条件断点和XHR断点

想象一个场景,我们的某个地方用到了我们代码中的一个通用函数,但是用的时候出问题了,我们想打个断点看看,但是由于这是一个通用的函数,我们刷新页面后这个函数会被调用多次,但是我们只关心其中的某一次,这个时候我们就需要跳过我们不关心的调用,除了一直手动点跳过按钮,还能怎么办呢?

condition-breakpoint

右键行号,选择conditional breakpoint然后输入你的条件,比如 user.name === "小明",这时只有当满足相应条件的时候才会断点。

而假如我们将上面的例子运用在网络请求上,当我们向某个特定的url发送请求时,产生一个断点,我们就可以在右边的XHR/Fetch Breakpoints中添加相应的url。

不过需要注意的是,此时的断点可能会断在Chrome的源码中或者别的什么地方,我们需要在Call Stack中找到正确的文件。


欢迎关注我的公众号
shanyuan.jpeg

查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 2月28日

Webpack原理(3) — 核心概念

本文首发于我的个人博客: https://teobler.com 转载请注明出处

Entry

我们先来看一张图

webpack-entry

从这张图可以看到,最上面的文件就是我们整个app的入口,也是这个文件启动了我们整个app,这就是weback的入口,通常这个文件会依赖我们自己app的其他文件,其他文件又会依赖别的第三方库,这些依赖可能是js,也可能是css,当然右边也展示了我们也会依赖app里面的其他文件。

在webpack的config文件中,我们使用entry字段来设置这个入口:

module.exports = {
    ...
    entry: "./main.js",
    ...
}

一句话来总结就是

Entry tells webpack WHAT(files) to load for the browser

Output

webpack-output

图片中入口文件下方的是入口文件的依赖,上方就是bundle之后的输出,同样的,我们在config文件中可以通过output这个字段来设置相应的配置项:

module.exports = {
    ...
    output: {
        path: "./dist",
        filename: "./bundle.js",
    },
    ...
}

这个配置就是在告诉webpack编译后的文件应该放在哪里,文件名应该叫什么,一句话总结

Output tells webpack WHERE and HOW to discribute bundles. It works with Entry.

Loaders & Rules

需要明白的是,loaders都是一些JS的modules,也就是说都是一些JS的方法(functions),他们以你app的module作为输入,返回一个修改后的状态,这些loaders会在webpack建立依赖图的时候对每一个文件进行相应的处理:

webpack-loaders

比如第一个ts-loader就是在说告诉webpack,任何时候你想要把一个ts文件放入依赖图中,就用ts-loader处理一次,处理过后这个文件就被编译成了js文件,当然,可能这个文件也有别的依赖,会按照相同的方式依次进行处理。

通常这个字段接收这些参数:

module.exports = {
    rules: [
        {
            // 告诉编译器要编译哪些文件
            test: regex,
            // loader(s)
            use: (Array | String | Function),
            // 白名单
            include: RegExp[],
            // 黑名单
            exclude: RegExp[],
            // 告诉webpack这条规则是否在其他规则 之前 | 之后 运行
            enforce: "pre" | "post"
        },
    ]
}

Chaining Loaders

在上面的介绍中可以看到use字段是可以传入一个数组的,比如["style", "css", "less"]但是需要指出的是,这三个loaders是按照从右到左的的顺序来执行的,这个规则将使一个less文件编译成一个css文件,再由css编译成js文件,最后编译成一个能够在浏览器中运行的inline style的js文件。

loaders tells webpack HOW to interpret and translate files. Transformed on a per-file basis before adding to dependency graph

Plugin

对于webpack来说,插件是什么:

  • 一个对象,这个对象上有一个apply属性
  • 允许你在编译的生命周期里做一些事情(hook)
  • webpack有各种各样的内置插件

一个简单的例子,编译器用这个插件分发事件:

webpack-plugin

这个插件被作为一个实例传入了webpack,所以它可以hook进不同的事件中(这里的代码是webpack3的,因为这里只讲概念,所以问题不大)。

首先这个插件插入编译器后悔监听done这个事件,这个事件会给这个插件传入一个参数,这里是一个state然后插件在这个state的基础上做出一些反应和处理(这里只是在控制台里输出了一个字符),下面的也是同样的,不过这次这个事件换成了failed

可以看到插件的定义是这样的,所以它可以被实例化,于是在webpack的config文件中我们就会这样去使用它:

const BellOnBundlerErrorPlugin = require("bell-on-error");
const webpack = require("webpack");

module.exports = {
    ...
    plugins: [
            new BellOnBundlerErrorPlugin(),
            new webpack.optimize.CommonsChunkPlugin("vendors"),
    ],
    ...
}

有意思的是,webpack的源代码有80%左右都是由plugins组成的,webpack是一个完全由事件驱动的体系结构,这也就可以使用插件迅速为webpack增添新的功能,并且不会破坏原有的功能,同样的也能够删除一些不必要的功能。

Adds additional functionality to Compilations(optimized bundled modules). More powerful more access to CompilerAPI. Does everything else you'd ever want to in webpack.

plugin与loader的最大不同就是loader是通过去访问一个个文件去做一些事情的,但是plugin可以访问webpack的事件生命周期和运行时,并且可以访问所有bundle文件。

示例

讲了这么多我们总得用概念做点什么

以下代码位于 feature/04010-composing-configs-webpack-merge 分支

读取命令行中的参数

我们可以将package.json中的命令做一个修改,传入一个env变量,带有一个mode属性:

"script" {
    "webpack": "webpack",
    "dev": "npm run webpack -- --env.mode development"
}

然后在webpack.config.js中我们可以这样获取到这个变量:

module.exports = env => {
    console.log(env);
    // { mode: "development" }
    
    return {
        mode: env.mode,
        output: {
            filename: "bundle.js",
        }
    }
}

加插件

我们这里以一个很通用并且很重要的插件html-webpack-plugin为例

首先用yarn add html-webpack-plugin —dev安装插件,然后在config文件中配置:

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = env => {
    console.log(env);
    // { mode: "development" }
    
    return {
        mode: env.mode,
        output: {
            filename: "bundle.js",
        },
        plugins: [new HtmlWebpackPlugin()],
    }
}

之后再运行yarn dev,你会发现bundle里面多了一个index.html文件,文件中用script标签把已经打包好的JS代码进行了引入。

设置本地开发服务

首先安装server插件yarn add webpack-dev-server --dev,然后修改package.json:

"script" {
    "webpack-dev-server": "webpack-dev-server",
    "dev": "npm run webpack-dev-server -- --mode development"
}

之后当你运行yarn dev的时候就会默认在本地8080端口启动一个server,在浏览器访问这个端口你就可以看见刚刚生成的html文件了,而且dev server默认开启了watch模式,可以实时更新代码到server上。

其实大家也能够猜到这个”插件“其实就是启动了一个Express的server,然后webpack在打包的时候直接将生成的文件加载进内存,通过server直接显示在浏览器里。

为不同的环境设置不同的config文件

我们可以像代码库中一样通过简单的配置使得webpack在得到不同参数的情况下去require不同的webpack config文件,以达到区分dev环境和prod环境的目的:

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpackMerge = require("webpack-merge");

const modeConfig = env => require(`./build-utils/webpack.${env}`)(env);

module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
  return webpackMerge(
    {
      mode,
      output: {
        filename: "bundle.js"
      },
      plugins: [new HtmlWebpackPlugin(), new webpack.ProgressPlugin()]
    },
    modeConfig(mode)
  );
};

但是实际的项目往往要复杂得多,一个大型项目往往不止有两个环境,通常来说development mode下应该有CI Dev QA UAT四个环境,production mode对应Prod环境。

由于在开发时不同的环境又对应不同的config,比如在Dev坏境应该去请求后端的Dev坏境,而不应该去请求QA坏境,这时我们又会引入不同的config文件去实现这一点。

首先安装config这个包,,然后在根目录下新建一个config目录,新建你需要的config文件,这些文件都是JSON格式的,记得建一个default.json,当其找不到对应的坏境时就会默认读取default文件,文件可以长这个样子:

// default.json
{
  "API_BASE_URL": "https://dev.your-project.com",
  "AUTH_URL": "https://dev.your-project.com/auth",
  "NODE_ENV": "dev"
}

//qa.json
{
  "API_BASE_URL": "https://qa.your-project.com",
  "AUTH_URL": "https://qa.your-project.com/auth",
  "NODE_ENV": "qa"
}

然后在运行启动命令的时候加上NODE_ENV这个参数,这样你在webpack的config文件中就可以通过process.env.NODE_ENV去获取这个参数,并且在config文件中可以将之前的config文件require进来在代码中进行使用:

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpackMerge = require("webpack-merge");

const modeConfig = env => require(`./build-utils/webpack.${env}`)(env);
const envConfig = JSON.stringify(require("config"));

module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
  return webpackMerge(
    {
      mode,
      output: {
        filename: "bundle.js"
      },
      plugins: [
                                    new HtmlWebpackPlugin(), 
                                    new webpack.ProgressPlugin(),
                                    new webpack.DefinePlugin({ CONFIG: envConfig }),
                             ]
    },
    modeConfig(mode)
  );
};

// axios.js
const client = axios.create({
  baseURL: CONFIG.API_BASE_URL,
  timeout: 10000,
});
查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 2月23日

Webpack原理(2) — 加载原理

webpack的核心目的和功能就是打包JavaScript代码,在时间的推进过程中,其逐渐演化成了一个生态体系,成为前端打包代码和处理开发时候必不可少的一个工具。

本文首发于我的个人博客: https://teobler.com, 转载请注明出处, 文章中提到的所有源代码来自于https://github.com/thelarkinn...

以下代码位于feature/01-fem-first-script分支

NPM scripts

Install & Run

首先将代码clone到你本地,然后运行yarn install。既然文章讲的是webpack,那么问题来了,当你运行这个命令的时候,发生了什么?

在你运行yarn install这个命令的时候,首先会在你的node_modules目录下添加一个.bin文件夹,里面是一些二进制可执行文件(包括webpack它本身),这些文件可以被node_modules里面你下载的所有packages运行,里面的可执行文件都在npm这个作用域下才可执行。

比如当你直接运行webpack的时候,是会抛错的:

$ webpack
zsh: command not found: webpack

但是此时如果你在package.json文件中加入下面的代码:

"script" {
    "webpack": "webpack"
}

然后再运行npm run webpack此时将会以默认设置运行node_modules中的webpack(注:这里需要webpack4及以上版本),在这个script标签中NPM允许你在其作用域中运行任何合法的script,甚至是bash的script。

这时其实我们的代码库里面是没有任何webpack的配置的,其默认回去寻找项目中的src目录下的index文件,但是这时webpack会抛出一个warning,推荐你设置环境变量以使用不同环境下的默认设置

Compose Scripts

NPM script有一个强大的功能是能够将已有的命令合并起来,并且还可以提供额外的参数,比如我们可以加一个新的script去运行之前的webpack并在新的命令里传入参数,避免重复新增和修改之前的script :

"script" {
    "webpack": "webpack",
    "dev": "npm run webpack -- --mode development"
    // 这里的 -- 代表将后面的参数传入前面的命令中
}
以下代码位于feature/03-fem-debug-script分支

Debugging

我不知道各位读者是怎么debug一个Node程序的,至少对于我来说在这之前我完全依赖于console.log,其实node早已经为我们提供了一个方便的方式,只需要在package.json文件中加入一个script:

"script": {
    "debug": "node --inspect --inspect-brk /path/to/file/you/want/to/debug"
}

运行这段script,然后打开你的chrome,在地址栏输入chrome://inspect或者在新版本的chrome打开控制台,在左上角有一个Node的logo,点击即可打开Node专用的devtool。

如果此时我们将文件路径换成./node_modules/webpack/bin/webpack.js的话,我们就可以debug webpack了。你可以再devtool里看到webpcck是怎样被加载的,每一步是如何进行的等等。这个步骤不单单对了解webpack在做什么有用,如果你在编写自己的plugin或者loader的话,你可以用这种方式去做debug。这就是传说中的"debug driven development"。

First Module

src目录下加入一个新的文件foo.js,里面只写一个导出:

export default "foo";

然后在index.js里面将其import进来console出来:

import foo from "./foo";

console.log(foo);

然后直接运行npm run dev,这时你应该能看到webpack将两个文件打包成功,并且在项目目录下出现了一个新的dist文件夹,里面的main.js文件就是刚刚打包好的内容(这是webpack的默认配置)。在控制台运行node ./dist/main.js你就能看到foo被打印出来了。

如果每次修改都run一次script未免太麻烦,所以webpack还提供了watching mode,只需要在dev命令后加上--watch的flag,在每次修改文件保存后webpack就会自动重新打包最新的代码。

ES Module Syntax

在上面的例子中foo文件直接export default了,但是有一些情况我们的一个文件中可能包含多个部分:

// bar.js
export const firstPart = "first";
export const secondPart = "second";

// index.js
import foo from "./foo.js";
import { firstPart, secondPart } from "./bar.js";

console.log(foo, firstPart, secondPart);
CommonJS也是几乎相同的用法,但是不推荐在0202年的这个时间点使用CommonJS,毕竟其不支持tree-shaking等等功能(老旧系统当我没说)

Tree Shaking

在默认情况下webpack会在打包production代码的时候启用这个功能,这个功能能够舍弃那些我们没有用到的死代码,一个通用的例子是,你引入了lodash,但是只用到了里面的一个函数,那么只要你正确引入并使用(比如使用ES Module Syntax等等)了lodash,在最后打包的时候webpack就只会打包你使用过的函数而不是一整个lodash库。

Bundle Result

开始前请将代码切到feature/031-all-module-types分支并且在根目录下创建webpack.config.js文件,并在其中加上一下内容:

module.exports = {
    mode: "none"
};

之后直接运行npm run webpack然后你就能在dist目录下看到打包好的,没有经过任何处理的main.js文件,我们来看看webpack打包后的代码到底是什么样子的。

首先我们看到的是一个IFEE,这个IFEE就是webpack的运行时(runtime code),它接受了一个参数modules,这个modules在下面可以看到是一个数组,其实就是我们打包的各个文件的真实代码,只不过webpack帮我们做了一些处理,打包的时候可以在控制台看到一些index:

index-of-bundle-files.png

它们就对应代码里面注释里的一个个文件:

the-first-bundled-file.png

那么这段runtime code里面做了什么呢?

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     function __webpack_require__(moduleId) {
/******/
/******/         // Check if module is in cache
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }
/******/         // Create a new module (and put it into the cache)
/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId,
/******/             l: false,
/******/             exports: {}
/******/         };
/******/
/******/         // Execute the module function
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/         // Flag the module as loaded
/******/         module.l = true;
/******/
/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }
/******/
/******/
/******/     // expose the modules object (__webpack_modules__)
/******/     __webpack_require__.m = modules;
/******/
/******/     // expose the module cache
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // define getter function for harmony exports
/******/     __webpack_require__.d = function(exports, name, getter) {
/******/         if(!__webpack_require__.o(exports, name)) {
/******/             Object.defineProperty(exports, name, {
/******/                 configurable: false,
/******/                 enumerable: true,
/******/                 get: getter
/******/             });
/******/         }
/******/     };
/******/
/******/     // define __esModule on exports
/******/     __webpack_require__.r = function(exports) {
/******/         Object.defineProperty(exports, '__esModule', { value: true });
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } :
/******/             function getModuleExports() { return module; };
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };
/******/
/******/     // Object.prototype.hasOwnProperty.call
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // __webpack_public_path__
/******/     __webpack_require__.p = "";
/******/
/******/
/******/     // Load entry module and return exports
/******/     return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([here, is, file, modules]);

// 篇幅所限,剩下的代码请到代码库中查看
  • 第3行设置了一个已经加载过的modules的cache变量
  • 第6行有一个require函数,它会检查传入的module是否存在于上面的cache中,如果存在的话就会直接return cache里面的exports字段,如果不存在就会创建一个module放入那个cache中(不过此时的exports字段是空的,并且没有被加载),之后将这个module传入另一个require函数中(实际上是直接call了这个module),执行完后将其标记为已加载,最后再把这个module的exports字段返回
  • 第37行与ESM的动态绑定有关,它是一个支持循环依赖的特性,本来应该在浏览器端实现的一个功能,之所以使用defineProperty是因为webpack将各个module的exports做了冻结处理,防止其被修改
  • 第48行的函数是为了与CommonJS交互,因为CommonJS没有default export,所以webpack使用了与TS相同的处理方式,在上面定义了一个常量标记这是一个CommonJS的module
  • 第53行的函数是为了与non-harmony的modules交互
  • 第69行就是webpack的执行入口,它将第一个module传入require方法执行了
  • 之后执行的就是真正的我们自己写的代码了,如果将打包后的代码和我们自己写的代码作对比,你能够很轻易的发现第78行到83行就是在做import,下面的函数就是在做console

所以这些代码就是我们在使用webpack build的时候所发生的一切了(当然,这仅仅是最简单的那部分)

查看原文

赞 0 收藏 0 评论 0

teobler 发布了文章 · 2月20日

Webpack原理(1) — Why Webpack

本文首发于我的个人博客: https://teobler.com ,转载请注明出处

我们怎么使用JS

众所周知,我们在HTML文件中使用JavaScript只能通过script标签来引入:

<script data-original="./index.js"></script>

<script>
    console.log('Hello World');
</script>

如果只是这样用,有什么问题呢?这样引入JavaScript是没办法大量引入的,什么算大量呢?也不用多,加入某个页面我需要20个JavaScript文件,怎么办?好像我只能加20个script标签,然后一个一个去请求。但是这样的话首先会使页面加载变慢,你需要在加载页面后去请求大量的script,更为重要的是,浏览器是有请求限制的:

浏览器只允许一定数量的请求能够 'fetch data',所以如果在同一时刻发起大量的请求的话,对于浏览器来说会有相当严重的性能问题。可能有人会说,9102年了,我用HTTP/2啊,没问题。是,对于大部分情况来说的确没有问题了,name对于特殊情况呢?比如Airbnb和MS的outlook,他们用超过3000个modules去进行构建。

咋办呢?那我把所有需要的JavaScript代码写到同一个文件里就好了嘛,是的,历史就是这么发展的,所以老程序员们可能都见到过那种一万十万行的巨大的JavaScript文件。这种办法的确缓解了上述问题,可是,这样的文件,我还需要讲缺点嘛?不说别的,假如我用到的某个function有问题,我想debug一下,咋整呢?闹呢?我在第一行定义了一个变量,我要去第一万行找它,然后这他么还是一个全局变量,咋玩呢?

然后呢,万能的前端同学们想到了一个办法 — IIFE 啥是IIFE呢 — immediately invoked function expression,中文是立即执行函数,它可以干啥用呢:

var outerScope = 1;

const whatever = (function(dataWillUsedInside){
    var outerScope = 0;
    return {
        someAttribute: 'youWantThis'
    }
})(0);

// 1
console.log(outerScope);

这下游戏规则好像变简单了一点了,我们可以自己写很多JS文件,然后把它们封装成一个个的IIFE,然后封装到一个文件里面,好了,解决了一个问题,至少变量污染的问题解决了嘛。然后呢,开始进入工具时代了,这个时候大家开始寻找各种各样的工具 — make, grunt, gulp, broccoli...

那么新的问题又来了 — 如果我想修改下某个文件,然后呢,我需要编译所有的文件,包括那些我都没有动过的。第二个问题是,比如我想引某个库,按需引用?不存在的,我只能一坨的引进来,这就有点可怕了,尤其还是以当时的网络状况来看。而且说不定你还需要引好多个库呢,那也只能全部放进来。而且这样编写成IIFE的“文件”由于各种原因会导致你的网页加载很慢。

JS的模块化

所以这个时候前端的同学们又开始想办法了,我们得有模块化这个东西呀。Node其实就是把V8“拿”到了server端,那么问题来了,没有了browser,没有了DOM,我们还怎么使用JS?所以Node.JS的出现带来了模块化,带来了CommonJS。

// index.js
const path = require('path');
const [add, subtract] = require(''./math');

/*
 * math.js (has two named exports [add, subtract])
 */
const divideFn = require('./division');

exports.add = (first, second) => first + second;
exports.subtract = (first, second) => first - second;
exports.divide = divideFn;

/*
 * dibision.js (has a exports 'divide')
 */
module.exports = (first, second) => first / second;

上面的代码是一个简单的CommonJS的例子,有三个文件,他们可以通过使用require相互引用。你可以使用匿名的默认exports,也可以使用具名的导出。之后再在别的文件内以特定的语法导入。所以到现在我们就解决了作用域的问题,我们不再需要IFFE了,这个时候我们不用再担心变量污染的问题了,同时也解决了IFFE的缺点。而同时出现的NPM更是让各个开发者自己写的各种包能够让每一个人使用。

可是还有问题,这玩意儿是Node的,不支持浏览器。而且如果你是一个写过别的语言的程序员的话你也应该知道动态绑定,但是CommonJS并不支持动态绑定。于是自我引用和循环引用层出不穷。而且他的同步算法贼慢。又为了解决这些问题,出现了一些有意思的东西:browserify, requireJS, systemJS…这些工具的目的很简单,就是让你能够在浏览器里面使用CommonJS。但是它们并不支持静态引入,也就是说你要用某个库你还是只能全部引进来。而且也并不是所有人在写库的时候都会使用CommonJS,毕竟还有个AMD不是吗。所以可以说这套玩意儿不能算是'module system'。

于是ES Module出现了。据说与之相关的文档,在1998年就能看到,所以这是一个断断续续设计并开发了大概10多20年的玩意儿,不过他的语法更友好了些,至少不是一些require之类的让人看到一头雾水的词语了:

import {uniq, forOf, bar} from 'lodash-es';
import * as utils from 'utils';

export const uniqConst = uniq([1, 2, 3, 4]);

现在看起来我们好像有了一个完整的module system了,重用,封装这些基本特性看起来都还不错。但是呢?几个问题又来了:这玩意儿好像挺难使用在Node里面的 — 这也是现在好多团队正在努力的方向;而且它直接在浏览器里面使用的话 — 非常非常慢,慢到你以为网站挂了,而且不用多,10个modules就能达到这个效果。因为在浏览器从上到下读取这个JS文件的时候,首先遇到import,然后去找这个包在哪,找到相应的路径,然后验证一下这些东西还能不能用,最后把这个文件读进来,然后继续在这个文件里重复这个步骤 — 一直到所有的依赖都读完了。需要注意的是,这一切都是在runtime完成的,也就是在加载你的网页的时候。

webpack横空出世

之前我们提到过,一个NPM中的包可能是用的不同的module format,你不能说哪一种是不对的不能使用的,所以在面对不同的方法的时候你也需要使用不同的使用方法。而且需要说明的是,上面我们说的所有东西都只是关于JS的,别忘了一个web app还有CSS还有各种静态资源。我们需要的是一个支持所有module format,并且同时还能支持除了JS以外的别的文件的一个“系统”或者说工具。

webpack是什么?

webpack is a module bundler lets you write any module format(mixed also), compiles then for the browser. And it supports static async bundling.

很简单有很强大的定义对吧,它几乎解决了上面所有的问题,那么它是怎么被创造出来的呢?这是一个有意思的小故事。

2012年,一个叫做Tobias的,在Newberg(美国一个城市)读master的德国人要写一片学位论文。他之前是写c#的,从来没有写过一个web界面。他在一些特定的场景需要用到Google Web Toolkit中的一个叫做code splitting的功能。而在他的论文中他需要写一个web app,他就想找一个包含这个功能的库来用。他找到的这个库叫webmake,这也是一个bundler。但是却没有code splitting这个功能,于是他提了一个issue,并且写了一堆如何实现这个功能的代码,希望维护者能够加入这个功能。在一番讨论过后维护者拒绝了他,于是在经过同意之后,他把这个库fork到了了过去并自己加上了这个功能,给新的库取名为webpack。

2014年,Dan Abramov在Stack Overflow上提了一个关于hot module replacement的问题,Tobias用很大的篇幅给他介绍了这个还在开发的功能,详细解释了这个功能怎么在webpack里工作的,以及这个功能有多棒,你可以不用刷新浏览器了!

2015年,这时在Instagram工作的Pete Hunt通过一次演讲告诉了世界他们是如何使用webpack打包发布他们的react app的。然后你懂得,webpack就火了。像Facebook这样的公司也开始使用webpack了。但是其实Tobias只是每周大概花5 6个小时在webpack中。

是的,在这两个讨论中,webpack彻底火了,走向了世界。

中国有句话叫做“以史为鉴,可以知兴替”。在了解这些历史的时候除了觉得很有意思,也会有一些思考:在历史的大潮里,有多少人是可以做那个改变方向的人?又有多少人死在了历史的长河里?要想不被淹死,只有奋力向上。

查看原文

赞 8 收藏 4 评论 0

teobler 赞了文章 · 2月17日

微信小程序wx.request请求数据报错:不在以下 request 合法域名列表中

首先写一个后台的数据接口,地址是:http://localhost/weicms/index...

然后使用wx.request调用后台数据接口的地址

示例代码

1 wx.request({  
2   url: 'http://localhost/weicms/index.php?s =/addon/Cms/Cms/getList',  
3   data: {  
4     x: '',  
5     y: ''  
6   },  
7   header: {  
8     'content-type': 'application/json'// 默认值  
9   },  
10  success (res) {  
11     console.log(res.data)  
12    }  
13  })

运行代码,效果如下图:

1.jpg

从上图中看到页面一片空白,没有获取到数据,并且控制台报错(request 合法域名校验出错;http://localhost 不在以下 request 合法域名列表中)

为何出现这种错误?

打开wx.request网络请求的开发文档可以看到

2.jpg

上面截图中红色框就是问题所在(小程序服务器域名配置中是不能使用IP地址跟localhost),示例代码中wx.request请求的url地址包含localhost,因此出错。

但是一般开发过程中都要先在本地开发调试。如果没法使用ip地址跟localhos,本地开发调试过程中如何获取数据呢,有没有办法在本地开发调试的时候屏蔽这个错误呢?

答案是有的。开发文档中指出了可以跳过域名校验,如下图:

3.png

具体在哪里开启不检验域名的选项呢?在微信开发者工具中,点击详情后,选中不检验合法域名,如下图所示:

4.jpg

此时,再次运行代码后,效果如下图:

5.jpg

从上图看到数据已经成功获取到了,且控制也没有报错,只是提示:配置中关闭合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书检查

查看原文

赞 65 收藏 2 评论 0

teobler 发布了文章 · 2月17日

用React hooks实现TDD

本文首发于我的个人博客: https://teobler.com, 转载请注明出处

由于篇幅所限文章中并没有给出demo的所有代码,大家如果有兴趣可以将代码clone到本地从commit来看整个demo的TDD过程,配合文章来看会比较清晰。本文涉及的所有代码地址: teobler/TDD-with-React-hooks-demo

前端TDD的痛

从进公司前认识了TDD,到实践TDD,过程中自己遇到或者小伙伴们一起讨论的比较频繁的一个问题是 — 前端不太好TDD / 前端TDD的投入收益比不高。为啥会这样呢?

我们假设你在写前端时全程TDD,那么你需要做的是 — 先assert页面上有一个button,然后去实现这个button,之后assert点击这个button之后会发生什么,最后再去实现相应的逻辑。

这个过程中有一个问题,因为前端中UI和逻辑强耦合,所以在TDD的时候你需要先实现UI,然后选中这个UI上的组件,trigger相应的行为,这个过程给开发人员增加了不少负担。

诚然,这样写出来的代码严格遵循了TDD的做法,也得到了TDD给我们带来的各种好处,但是据我观察下来,身边的小伙伴们没有一个人认同这样的做法。大家的痛点在于UI部分的TDD过于痛苦并且收益太低,而且由于UI和逻辑强耦合,后续的逻辑部分也需要先选取页面上的元素trigger出相应的执行逻辑。

这些痛点在项目组引入了hooks之后有了显著的改善,从引入hooks到现在快一年的时间,组里的小伙伴们一起总结除了一套测试策略。在此我们将React的组件分为三类 — 纯逻辑组件(比如request的处理组件,utils函数等),纯UI组件(比如展示用的Layout,Container组件等)和两者结合的混合组件(比如某个页面)。

纯逻辑组件

这部分组件没啥好说的,全都是逻辑,tasking,测试,实现,重构一条龙,具体咋写我们这里不讨论。

// combineClass.test.ts
describe('combineClass', () => {
    it('should return prefixed string given only one class name', () => {
        const result = combineClass('class-one');
        expect(result).toEqual('prefix-class-one');
    });

    it('should trim space for class name', () => {
        const result = combineClass('class-one ');
        expect(result).toEqual('prefix-class-one');
    });

    it('should combine two class name and second class name should not add prefix', () => {
        const result = combineClass('class-one', 'class-two');
        expect(result).toEqual('prefix-class-one class-two');
    });

    it('should combine three class name and tail class name should not add prefix', () => {
        const result = combineClass('class-one', 'class-two', 'class-three');
        expect(result).toEqual('prefix-class-one class-two class-three');
    });
});

// combineClass.ts
const CLASS_PREFIX = "prefix-";
export const combineClass = (...className: string[]) => {
    const resultName = className.slice(0);
    resultName[0] = CLASS_PREFIX + className[0];

    return resultName
        .join(' ')
        .trim();
};

纯UI组件

这类组件我们没有一个个去测试组件里面的元素,而是按照UX的要求build完组件以后加上一个jest的json snapshot测试。

注意这里的snapshot并不是大家印象中的e2e测试中的截图,而是jest里将组件render出来之后使用json生成一份UI的dom结构,在下次测试时,生成一份新的快照与旧的快照进行比对,从而得出两个UI不一样的地方,实现对UI的保护。

但是其实使用snapshot测试有两个问题:

  1. snapshot相比较于一般的单元测试来说运行速度较慢,如果项目中大量使用的snapshot测试的话,在运行所有单元测试的时候会比较明显的感受到单元测试的速度被拖慢了,一定程度上违背了单元测试快速反馈的初衷;
  2. 维护snapshot的人工成本较大,snapshot测试最大的问题在于你只要改动了任何UI的部分,这个测试都会挂掉,这个时候就需要仔细对比不同的地方以决定是更新snapshot还是改错地方了,而如果此时团队里有“省心”的队友无脑更新snapshot的话,这个测试相当于浪费了资源。
    // Content.test.tsx
    describe('Content', () => {
        it('should render correctly', () => {
            const {container} = render(<Content/>);
            expect(container).toMatchSnapshot();
        });
    });
    
    // Content.test.tsx.snap
    // Jest Snapshot v1, https://goo.gl/fbAQLP
    
    exports[`Content should render correctly 1`] = `
    <div>
      <main
        class="prefix-layout-content"
      />
    </div>
    `;
    
    // Content.tsx
    export const Content: React.FC<React.HTMLAttributes<HTMLElement>> = (props) => {
        const { className = '', children, ...restProps } = props;
    
        return (
            <main className={combineClass('layout-content', className)} {...restProps}>
                {children}
            </main>
        );
    };

逻辑与UI混合组件

这个部分我们就需要hooks的帮忙了,这样的组件不是UI和逻辑强耦合嘛,那我们就可以将两者拆开。于是这样的组件我们会这样写:

  1. 首先将UI页面build出来,但是需要的callback全部写成空函数
  2. 将所有callback或者是页面需要用到的逻辑抽到一个hook中
  3. 此时hook里的代码没有UI只有逻辑,故可以使用测试库对hook进行单独的逻辑测试,所以此时hook的开发可以按照逻辑组件的开发进行TDD
  4. 整个混合组件开发完成后,补上一个snapshot测试,需要注意的是可能该组件在渲染时需要一些数据,在写snapshot测试时应该确保准备的数据是完备的,否则快照会渲染出一份根本没有数据的错误组件
    // usePageExample.test.ts
    import {act, renderHook} from "@testing-library/react-hooks";
    
    describe('usePageExample', () => {
        let mockGetUserId: jest.Mock;
    let mockValidate: jest.Mock;
    
    
    beforeAll(() => {
        mockGetUserId = jest.fn();
        mockValidate = jest.fn();
    
        jest.mock('../../../../request/someRequest', () => ({
            getUserId: mockGetUserId,
        }));
        jest.mock('../../../../validator/formValidator', () => ({
            formValidate: mockValidate,
        }));
    });
    
    afterAll(() => {
        mockGetUserId.mockReset();
        mockValidate.mockReset();
    });
    
    it('should trigger request with test string when click button', () => {
        const {usePageExample} = require('../usePageExample');
        const {result} = renderHook(() => usePageExample());
    
        act(() => {
            result.current.onClick();
        });
    
        expect(mockGetUserId).toBeCalled();
    });
    
    it('should validate form values before submit', () => {
        const {usePageExample} = require('../usePageExample');
        const {result} = renderHook(() => usePageExample());
        const formValues = {id: '1', name: 'name'};
    
        act(() => {
            result.current.onSubmit(formValues);
        });
    
        expect(mockValidate).toBeCalledWith(formValues);
    });
    
    });
    
    // usePageExample.ts
    import {getUserId} from "../../../request/someRequest";
    import {formValidate} from "../../../validator/formValidator";
    
    export interface IFormValues {
        email: string;
        name: string;
    }
    
    export const usePageExample = () => {
        const onClick = () => {
            getUserId();
        };
        const onSubmit = (formValues: IFormValues) => {
            formValidate(formValues);
        };
    
        return {onClick, onSubmit};
    };
    
    // PageExample.tsx
    import * as React from "react";
    import {usePageExample} from "./hooks/usePageExample";
    
    export const PageExample: React.FC<IPageExampleProps> = () => {
        const {onClick, onSubmit} = usePageExample();
    
        return (
            <div>
                <form onSubmit={() => onSubmit}>
                    <input type="text"/>
                </form>
                <button onClick={onClick}>test</button>
            </div>
        );
    };

这篇文章算是给大家提供了一个hooks的TDD思路,当然其中还有一些我们也觉得不是很完善的地方(比如UI的测试),大家如果有更好的实践的话欢迎一起讨论。

查看原文

赞 2 收藏 1 评论 0

teobler 发布了文章 · 2017-07-22

菜鸟理解setTimeout和setInterval

写在前面,最近在准备校招,陆陆续续做一些之前的总结,写了一个小系列的文章,想借此机会记录下来,也能以后有个地方能进行查阅,上一篇文章在css基础总结希望能帮助一下和我一样的菜鸟们好了,正文开始。

这是一个老生常谈,新手掉坑的问题,算是一个比较经典的对于javascript运行机制的理解问题,我在这里粗浅的谈一下自己的理解,话不多说,进入正题:

两者表面上的区别

  1. setTimeout() 方法用于在指定毫秒数之后调用其中的函数

  2. setInterval() 方法则是在间隔一定毫秒后重复调用其中的函数

透过现象看本质

时间精确问题

由于js是运行在单线程的环境当中的,单线程就意味着任务的执行需要依赖任务队列。实际运行时是将两个方法的代码块移出当前运行环境(从任务队列移出到回调队列中),当执行完当前任务后,检查回调队列中有无需要执行的任务(对应这两个方法为是否已经到执行时间),可是如果时间到时恰好有别的任务在进行的话,由于其单线程的机制,该方法就只能等到当前任务结束之后才能运行。

回到方法本身,这就相当于其他的正常任务在一个队列中,当遇到这两个方法时,就将他们移出队列,并开始计时,当时间到时,直接“插队”到队首,如果队首有正在执行的任务,则排在次队首,等待执行。也就是说,这仅仅是“计划”在未来某一个时间执行某个任务,并不能保证精确的时间。

setInterval重复执行问题

这个方法执行时仅当没有该计时器的其他代码示例时才进行下一轮的执行。这样的规则就会导致某些间隔会被跳过,同时多个间隔可能比预期时间要短。所以为了避免setInterval所造成的问题,可以用setTimeout来通过循环代替setInterval方法,从而实现一个重复的定时器(除非必要,尽量避免代码中出现setInterval)

方法中使用this的问题

在两个方法中传入函数时(即第一个函数参数中含有另外一个函数),此函数中的this会只想window对象。这是由于两个方法调用的代码在与所在函数完全分离的执行环境上(第一条中有讲到的两个方法的运行机制),这就会导致这些代码中包含的this关键字会指向window(或全局)对象。

但是要注意,如果this只是在两个方法中而不是在方法中的函数中时,this的指向符合我们的预期为当前对象。

解决方法:

1.将当前对象的this存为一个变量,定时器内的函数利用闭包来访问这个变量,如下:

var num = 0;
function Obj (){
    var that = this;    //将this存为一个变量,此时的this指向obj
    this.num = 1,
    this.getNum = function(){
        console.log(this.num);
    },
    this.getNumLater = function(){
        setTimeout(function(){
            console.log(that.num);    //利用闭包访问that,that是一个指向obj的指针
        }, 1000)
    }
}
var obj = new Obj;
obj.getNum();          //1  打印的为obj.num,值为1
obj.getNumLater()      //1  打印的为obj.num,值为1

2.利用bind()方法:

var num = 0;
function Obj (){
    this.num = 1,
    this.getNum = function(){
        console.log(this.num);
    },
    this.getNumLater = function(){
        setTimeout(function(){
            console.log(this.num);
        }.bind(this), 1000)    //利用bind()将this绑定到这个函数上
    }
}
var obj = new Obj;
obj.getNum();                 //1  打印的为obj.num,值为1
obj.getNumLater()             //1  打印的为obj.num,值为1

bind()方法是在Function.prototype上的一个方法,当被绑定函数执行时,bind方法会创建一个新函数,并将第一个参数作为新函数运行时的this。在这个例子中,在调用setTimeout中的函数时,bind方法创建了一个新的函数,并将this传进新的函数,执行的结果也就是正确的了。关于bind方法可参考 MDN bind

清除计时器

clearTimeout()

在在使用setTimeout时,该方法会返回一个唯一的关于当前计时器的计时ID,在clearTimeout()方法中传入这个ID值即可取消对应的Timeout

clearInterval()

同上

参考

上面是我在使用过程中遇到问题后在网上查阅后自己的一些总结,希望对和我一样的新手有所帮助,想要更深入了解他们的区别和js的一些运行机制,请入传送门:

传送门1---阮大的剖析

传送门2---this指向

传送门3---调用执行


查看原文

赞 4 收藏 21 评论 0

认证与成就

  • 获得 21 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-12-01
个人主页被 454 人浏览