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

teobler 提出了问题 · 2017-07-22

关于Vue + Element-UI的一个布局问题

一个项目,有一个页面需要实现这样的功能:
整体界面

整体描述:图中包括“优惠券”按钮下的boder往上的部分为固定高度,下方左侧菜单栏为element-ui中的menu组件,menu组件右边的content区域为自己写的vue组件,菜单中不同的选项会对应不同的组件内容

需求:
在大屏手机中,左侧的menu占满剩下的高度。在屏幕上划时,图片展示部分(375 * 150部分)上移,商铺头像(80 * 80部分)下移,与“商铺号”所在白框等高,之后与header一起固定在顶部,同时下方div自动填满高出来的那部分。

在小屏手机中,初始menu不全部展示:
图片描述

在屏幕上划时,上方div除header外隐藏,下方div占满剩下高度,并将menu固定在左侧。

解决:这个问题卡了我一天了,因为我是新手,没能解决,然后百度过后好像也没有我的这种案例,所以把问题放上来了,描述可能有不清楚的地方,还请见谅,哪里不清楚我可以继续补充,谢谢。

代码:
template:

<template>
  <div class="store">
    <div class="store-header">
      <router-link to="/home">
        <span class="back-home"><img data-original="../../assets/back.png" alt="back"></span>
      </router-link>

      <span class="store-name">{{ storeName }}</span>
      <span class="search-in-store">
        <el-input placeholder="店内搜索" icon="search" v-model="input" :on-icon-click="handleIconClick"></el-input>
      </span>

      <el-popover class="more-menu" ref="popover1" placement="bottom" width="80" trigger="click">
        <ul class = "menu">
          <li class="store-info">店铺信息</li>
          <li class="store-contact">联系商家</li>
          <li class="store-collection">收藏</li>
          <li class="store-report">举报</li>
        </ul>
      </el-popover>
      <span class="more" v-popover:popover1>
        <img data-original="../../assets/menu.png" alt="more">
      </span>
    </div>

    <div class = "ticket-bar">
      <div class = "curtain">
        <div class = "store-avatar"><img data-original="http://temp.im/80x80" alt="store-avatar"></div>
        <div class = "store-slogan">
          <router-link to="/storeInfo"><p>xxxxxxx</p></router-link>
        </div>
      </div>

      <div class = "store-resume">
        商铺号:{{ storeId }} | {{ collected }}人收藏 <br />
        店铺等级
      </div>
      <router-link to="card" class = "to-ticket">
        <el-button type="primary">优惠券</el-button>
      </router-link>
      <span class = "share"><img data-original="../../assets/share.png" alt="share"></span>
    </div>

    <div class = "store-content">
      <el-row>
        <el-col :span="6" class = "store-menu">
          <el-menu default-active ="storeContent_1" :router = true class="el-menu-vertical">
            <el-menu-item class = "el-menu-item" index="storeContent_1"><i class="el-icon-menu"></i>全部</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_2"><i class="el-icon-menu"></i>零食</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_3"><i class="el-icon-menu"></i>饮料</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_4"><i class="el-icon-menu"></i>厨卫</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_5"><i class="el-icon-menu"></i>洗浴</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_6"><i class="el-icon-menu"></i>服装</el-menu-item>
            <el-menu-item class = "el-menu-item" index="storeContent_7"><i class="el-icon-menu"></i>办公</el-menu-item>
          </el-menu>
        </el-col>

        <el-col :span="24">
          <div class = "content">
            <router-view></router-view>
          </div>
        </el-col>
      </el-row>
    </div>
  </div>
</template>

style:

.store {
    height 100%
    overflow auto
    display flex
    flex-direction column
    .store-header {
      height 2.5rem
      .back-home {
        float left
        margin 0.3rem 1.7rem 0 0.2rem
      }
      .store-name {
        float left
        height 2.5rem
        line-height 3rem
        margin-right 14%
      }
      .search-in-store {
        width 40vw
        float left
        margin-top 0.2rem
      }
      .more {
        float right
        margin 0.2rem 0.5rem
      }
      .more-menu{
        padding 0
        .menu{
          list-style none
          font-size 15px
        }
      }
    }
    .ticket-bar{
      width 100%
      overflow hidden
      border-bottom 1px solid #777
      .curtain{
        height 9.5rem
        background-image url('http://temp.im/375x150')
        position relative
        .store-avatar{
          width 4.8rem
          height 4.8rem
          border-radius 2.4rem
          position absolute
          bottom -2.4rem
          left 1.5rem
        }
        .store-slogan{
          position absolute
          bottom 0.5rem
          left 8.2rem
          p{
            margin 0
          }
        }
      }
      .store-resume{
        font-size 14px
        margin-left 7rem
      }
      .share{
        float right
        height 2rem
        width 2rem
        margin 0.2rem 20% 0 20%
      }
      .to-ticket{
        float right
        margin-top 0.1rem
        margin-right 20%
      }
    }
    .store-content{
      width 100%
      .store-menu{
        height 100%
        position fixed
        left 0
        ul{
          min-height 25rem
        }
        .el-menu-item{
          height 3.5rem
        }
      }
      .content{
        margin-left 13.5%
      }
    }
  }

关注 1 回答 0

teobler 回答了问题 · 2017-07-22

设置这一句代码axios.defaults.timeout什么意思?

通俗来说就是,发送请求后,2s还没有收到请求答复,就超时报错

关注 3 回答 2

teobler 回答了问题 · 2017-07-21

解决css3中字体在哪里下载,我去1688上下载的是png和svg格式的图片啊,求ttf==

你说的时iconfont吧,去阿里图标库,注册一个账号,然后搜索你想要的图标库或者单个图标,添加到你的购物车(基本都是免费的),然后进入购物车选择下载代码,他会下你需要的所有东西,然后里面有一个html文件会教你怎么使用这些iconfont,要是有什么不懂可以追问,有用的话采纳下哟

关注 2 回答 2

teobler 赞了文章 · 2017-07-20

[聊一聊系列]聊一聊网页的分段传输与渲染那些事儿

欢迎大家收看聊一聊系列,这一套系列文章,可以帮助前端工程师们了解前端的方方面面(不仅仅是代码):
https://segmentfault.com/blog...

这一节,请跟随笔者聊一聊,网页的分段传输与渲染,用一些非常规手段优化我们的网站响应速度。

1 CHUNKED编码

1.1 传统的渲染方法

1.1.1 传统的渲染方法怎么做?

按照常理,我们渲染一张网页,必定是网页全部拼装完毕,然后生成HTML字符串,传送至客户端。这也意味着,如果一张网页处理的有快有慢的话,必须串行等到所有的逻辑都处理完毕。后端才能进行返回。(这也是我们目前网页的一般逻辑)。如下面的例子,三个很慢的读数据操作,均执行完毕后,才传送渲染页面。渲染效果如图1.1.1,15s之后才传送并渲染出页面:
normal.php

<?php
function getOneData() {
    usleep(5000000);
    return '我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据'; 
}

$var1 = getOneData();

function getTwoData() {
    usleep(5000000);
    return '是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据';
}
$var2 = getTwoData();

function getThreeData() {
    usleep(5000000);
    return '我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据'; 
}
$var3 = getThreeData();

// 渲染模板并输出
include('./normal.html.php');

normal.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div>1. <?php echo $var1;?></div>
        <div>2. <?php echo $var2;?></div>
        <div>3. <?php echo $var3;?></div>
    </body>
</html>

clipboard.png
图1.1.1

上述例子,在本文后github中的normal文件夹中。

1.1.2 传统的渲染方法有哪些弊端?

如上所示,我们能看到,直出的网页中,存在着后端数据串行,互相等待的尴尬局面。这也为我们后续的优化埋下了伏笔。

1.2 分段传输

1.2.1 何为分段传输?

http1.1中引入了一个http首部,Transfer-Encoding:chunked。这个首部标识了实体采用chunked编码传输,chunked编码可以将实体分块儿进行传输,并且chunked编码的每一块内容都会自标识长度。这给了web开发者一个启示,如果需要多个数据,而多个数据均返回较慢的话。可以处理完一块就返回一块,让浏览器尽早的接收到html,可以先行渲染。

1.2.2 如何分段传输?

既然知道了我们可以将网页一块儿一块儿的传送,那么我们就可以将上面的网页进行改造,拿好一块儿需要的数据,便渲染一块儿,无需等待,而模板方面,自然也要拆分为三段,供服务端拿一块儿的模板,就渲染一块儿出去,效果如图1.2.2.1。
normal.php

<?php
function getOneData() {
    usleep(5000000);
    return '我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出>的第一个数据我是取出的第一个数据我是取出的第一个数据我是取出的第一个数据';
}
// 取出第一块儿的数据
$var1 = getOneData();
// 渲染第一块儿
include('./normal1.html.php');
//刷新到缓冲区,渲染第一份儿模板,传送到客户端
ob_flush();
flush();

function getTwoData() {
    usleep(5000000);
    return '我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出>的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出>的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出>的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出>的第二个数据我是取出的第二个数据我是取出的第二个数据我是取出的第二个数据';
}
// 取出第二块儿的数据
$var2 = getTwoData();
// 渲染第二块儿
include('./normal2.html.php');
//刷新到缓冲区,渲染第二份儿模板,传送到客户端
ob_flush();
flush();

function getThreeData() {
    usleep(5000000);
    return '我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出>的第三个数据我是取出的第三个数据我是取出的第三个数据我是取出的第三个数据';
}
// 获取第三块儿的数据
$var3 = getThreeData();
// 渲染第三块儿
include('./normal3.html.php');
// 将第三份儿的模板,传送到客户端
ob_flush();
flush();

normal1.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div>1. <?php echo $var1;?></div>

normal2.html.php

<div>2. <?php echo $var2;?></div>

normal3.html.php

        <div>3. <?php echo $var3;?></div>
    </body>
</html>

clipboard.png
图1.2.2.1
上述例子,在本文后github中的chunked文件夹中。

对比图图1.1.1与图1.2.2.1我们可以发现,虽然最后总的处理时长不变,但是采用了分段输出的网页,可以尽早的将一段HTML渲染到客户端,这样用户可以使用先到达的部分。另一方面,尽早的页面反馈,也可以减少用户等待的焦躁情绪。综上,使用此种优化方法,可以提速网页的渲染速度。

1.2.3 分段传输小TIPs

我们代码虽然如上所述,但是读者尝试的时候可能会发现,并没有什么效果。和我截图并不一样,还是等到15s后一起渲染出来了。这里要提醒大家一下,可能是由于nginx配置的原因。如果使用的是nginx做server的话,要使用如下配置才能看到效果。

http {
    ....
    fastcgi_buffer_size 1k; 
    fastcgi_buffers 16 1k; 
    gzip  off;
    ....
}

其实读者们可以这么理解上面的配置,nginx会在攒够一块儿缓冲区的量后,可以将一块儿数据发出去。上面我们配置了fastcgi_buffers 16 1k; 就是16块儿,大小为1K的缓存。

我们的数据量太小了,连默认的一块儿缓冲区都填不满,没法看到分块儿发送的效果,所以这里我们将缓冲区给调小为1K,这样就能1K为单位分块儿,1K一发,体现出实验效果了。笔者这里建议做实验的时候,最好把gzip给关了,因为,咱们做实验的时候数据量不大,实际使用中建议chunked与gzip均开启(如图1.2.3.1,如果量比较大的话,gzip与chunked均开启使用效果更佳哦~~~)。
clipboard.png
图1.2.3.1

1.2.4 分段传输适用场景

当页面的某些后端处理比较耗时的时候,可以试试采用分段传输,可以渲染一部分,就发送一部分到客户端,虽然总时长不变,但是浏览器在全部传输完之前不会处于干等状态。可以尽早的渲染并给予用户反馈。

2 BIGPIPE

2.1 分段传输的局限

刚刚笔者和读者们一起做了分段传输的实验,思路是基于读者们想展示的网页也是上快下慢的。可是读者们有没有想过,如果整个网页中,最快的是下方,而最慢的是上方呢?这样我们就无法利用分段传输的优势了吗?如图2.1.1,整个页面依旧是被最慢的第一部分数据渲染给hold住了。而后两块儿渲染较快,完全可以先传输过来。

<?php
// 获取第一块儿数据最慢
function getOneData() {
    usleep(2000000);
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第一个数据';
    }   
    return $str;
}

$var1 = getOneData();
// 渲染第一块儿
include('./normal1.html.php');
ob_flush();
flush();

// 获取第二块儿数据较快
function getTwoData() {
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第二个数据';
    }   
    return $str;
}
$var2 = getTwoData();
// 渲染第二块儿
include('./normal2.html.php');
ob_flush();
flush();

// 获取地三块儿数据也较快
function getThreeData() {
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第三个数据';
    }   
    return $str;
}
$var3 = getThreeData();
// 渲染第三块儿
include('./normal3.html.php');
ob_flush();
flush();

clipboard.png
图2.1.1

上述例子,在本文后github中的bigpipprepare文件夹中。

2.2 解决分段传输顺序的问题

看完上述描述,读者们肯定在想,如果能把最慢的部分放置于底部传过来就好了。于是有了一种加载思路,便是使用js回填的方式,先将左边最慢的部分架空,然后在底部写上js回填。这样不就可以先渲染相对较快的右侧两块儿了么。如图2.2.1
后端可以先渲染快的模板,然后再渲染最慢的模板。

<?php
// 渲染第一块儿的架子,还未获取内容
include('./normal1.html.php');
ob_flush();
flush();

// 获取第二块儿数据较快
function getTwoData() {
    $str = ''; 
    for ($i = 0; $i < 50; $i++) {
        $str .= '我是取出的第二个数据';
    }   
    return $str;
}
$var2 = getTwoData();

// 渲染第二块儿
include('./normal2.html.php');
ob_flush();
flush();

// 获取地三块儿数据也较快
function getThreeData() {
    $str = ''; 
    for ($i = 0; $i < 70; $i++) {
        $str .= '我是取出的第三个数据';
    }   
    return $str;
}
$var3 = getThreeData();
// 渲染第三块儿
include('./normal3.html.php');
ob_flush();
flush();

// 获取第一块儿数据最慢
function getOneData() {
    usleep(2000000);
    $str = ''; 
    for ($i = 0; $i < 50; $i++) {
        $str .= '我是取出的第一个数据';
    }   
    return $str;
}
$var1 = getOneData();
// 渲染回填第一块儿
include('./normal4.html.php');
ob_flush();
flush();

normal1.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
        <style>
            html, body {
                margin: 0;
            }   
            .part1 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #0f0;
                outline: 1px solid #000;
            }   
            .part2 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #f00;
                outline: 1px solid #000;
            }   
            .part3 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #00f;
                outline: 1px solid #000;
            }   
        </style>
    </head>
    <body>
        <div class="part1">
        </div>

normal2.html.php

<div class="part2">2. <?php echo $var2;?></div>

normal3.html.php

<div class="part3">3. <?php echo $var3;?></div>

normal4.html.php

        <script>
            // 把最慢且顶在前面的部分用js回填回去
            document.querySelector('.part1').innerHTML = "<?php echo $var1?>";
        </script>
    </body>
</html>

clipboard.png
图2.2.1
如上图,可以看到49ms的时候,就已经渲染出来了右侧两块儿,2S的时候,左侧也渲染出来了。

上述例子,在本文后github中的bigpipe文件夹中。

2.3 回填思路的扩展与并行化

我们刚刚做了一个实验,是将耗时最慢的块儿放在底部。然而,事实情况是,如果你也不知道哪块儿慢了呢?或者是,你的几块儿数据区块儿是并行的呢?出于刚刚的经验,我们可以把页面上所有的块儿都架空,然后并行渲染,谁快谁就先渲染回填js。这样就可以达到并行且先到先渲染的目的了。我这里做了个php并行取并回填的实验,如图2.3.1,可以看到,中间红色的虽然被阻塞,但是框架先行渲染出来了所有的内容均是空的。绿色最快,先行回填渲染了出来,蓝色稍慢,也跟着渲染了出来,最后红色完毕,回填渲染结束了。
并行渲染的PHP(normal.php)

<?php
function asyncRequest($host, $url, $port=8082, $conn_timeout=30, $rw_timeout=86400) {
    $errno = ''; 
    $errstr = ''; 
    $fp = fsockopen($host, $port, $errno, $errstr, $conn_timeout);
    if (!$fp) {
       echo "Server error:$errstr($errno)";
       return false;
    }   
    stream_set_timeout($fp, $rw_timeout);
    stream_set_blocking($fp, false);

    $rq = "GET $url HTTP/1.0\r\n";
    $rq .= "Host: $host\r\n";
    $rq .= "Connect: close\r\n\r\n";
    fwrite($fp, $rq);
    return $fp;
}

function asyncFetch(&$fp) {
   if ($fp === false) return false;

   if (feof($fp)) {
      fclose($fp);
      $fp = false;
      return false;
   }   
   return fread($fp, 10000);
}

$fp1 = asyncRequest('localhost', '/bigpipeparal/data1.php');
$fp2 = asyncRequest('localhost', '/bigpipeparal/data2.php');
$fp3 = asyncRequest('localhost', '/bigpipeparal/data3.php');

include('normal_frame.html.php');
ob_flush();
flush();
while (true) {
    sleep(1);
    $r1 = asyncFetch($fp1);
    $r2 = asyncFetch($fp2);
    $r3 = asyncFetch($fp3);
    //谁快谁先渲染并flush刷出
    if ($r1 != false) {
        preg_match('/\|(.+)\|/i', $r1, $res);
        $var1 = $res[1];
        include('normal1.html.php');
    }

    if ($r2 != false) {
        preg_match('/\|(.+)\|/i', $r2, $res);
        $var2 = $res[1];
        include('normal2.html.php');
    }

    if ($r3 != false) {
        preg_match('/\|(.+)\|/i', $r3, $res);
        $var3 = $res[1];
        include('normal3.html.php');
    }

    if ($r1 == false && $r2 == false && $r3 == false) {
        break;
    }
  
    ob_flush();
    flush();
}

主框架的模板,架空,等待回填。normal_frame.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
        <style>
            html, body {
                margin: 0;
            }   
            .part1 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #0f0;
                outline: 1px solid #000;
            }   
            .part2 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #f00;
                outline: 1px solid #000;
            }   
            .part3 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #00f;
                outline: 1px solid #000;
            }   
        </style>
    </head>
    <body>
        <!--三块儿全部架空,等待回填--> 
        <div class="part1"></div>
        <div class="part2"></div>
        <div class="part3"></div>
    </body>
</html>

具体回填模板,normal1.html.php/normal2.html.php/normal3.html.php

<script>
    document.querySelector('.part1').innerHTML = "第一块儿回填!其值如下:<?php echo $var1?>";
</script>

clipboard.png
图2.3.1

上述例子,在本文后github中的bigpipeparal文件夹中。

2.4 为什么不用ajax?

相信读着在此处会有疑问,为什么慢的数据,不用ajax去请求呢?这样模板框架也能尽早的渲染出来。ajax毕竟是请求。相信很多读着也有这样的经历,后端处理如果遇到了瓶颈,那么有的时候我们会选择同步页面渲染完之后,再发个请求去获取后端数据。但是笔者认为,这样做有一定弊端:
1、ajax毕竟是个请求,请求就要有连接,要有解析等过程。
2、服务端和客户端都会有闲的时候,发送ajax之前服务端闲,发送ajax出去之后,浏览器又闲着了。
所以,我们使用bigpipe的方式还是比多发送一个ajax有优势的。

3 分段传输与bigpipe适用场景

3.1 分段传输的适用场景

笔者总结了一些使用分块儿传输比较合适的场景
1 前端需要尽早传输head中的一些css/js外联文件的情况下(可以先flush给客户端head前面的html内容,让浏览器尽早的去请求)
2 后端处理渲染的数据,上方较快,下方较慢的情况(可以先行渲染上方较快的部分)

3.2 使用bigpipe的场景

对于更为复杂一点的bigpipe方式,如果上面的情况就适用于你的网站了的话,则最好采用简单的分块传输,否则如下情况,需要回填,则采用bigpipe方式渲染页面。毕竟,使用js回填还是有性能损耗的。
1 后端有较慢的数据处理,阻塞住了页面的情况下,且最慢的部分不是在网页的最后。(可以把最慢的部分变为回填)
2 后端有多块儿数据要并行处理的情况下(你也不知道哪块儿先回来了,所以先渲染一个架子。对于并行的请求,先回来的先flush回填)

3.3 国内的应用

据笔者观察,新浪微博正是采用了bigpipe的方式进行渲染,如图3.3.1,我们看到新浪微博的左侧导航栏与中间feed流区块儿都是架空的:
clipboard.png
图3.3.1
在下方,有对左侧导航栏和中间feed流部分的回填,如图3.3.2
clipboard.png
图3.3.2

所以,整个网页的渲染效果如下(如图3.3.3/图3.3.4/图3.3.5)
clipboard.png
图3.3.3
clipboard.png
图3.3.4
clipboard.png
图3.3.5
笔者猜测,可能微博是并行渲染这几块儿的数据,所以采用了bigpipe的方式。

4 课后作业

请读者们回想一下,自己的网站到底适不适合使用分块儿传输,能否使用上面的技术,使自己的网站更快一些呢?如果使用的话,是适合使用普通的chuned提速呢?还是使用bigpipe进行提速呢?

如有说明不周的地方欢迎回复详询

本文中所有的例子,均在我的github上可以找到:
https://github.com/houyu01/ch...

接下来的一篇文章,我将会和读者们一起聊聊HTTPS那些事儿,不要走开,请关注我.....

https://segmentfault.com/a/11...

如果喜欢本文请点击下方的推荐哦,你的推荐会变为我继续更文的动力。

以上内容仅代表笔者个人观点,如有意见笔者愿意学习参考各读者的建议。

查看原文

赞 18 收藏 82 评论 5

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

校招进行时(四)---css基础

恩,小菜鸟又来了,上篇文章在这,话不多说,这次罗列总结一下css基础知识,和我一样的菜鸟可以看看。

引入方式

css的引入方式主要有以下几种:

  • 外部样式表

    通过在head标签中加入link标签来引入外部样式表,因为其良好的分离性和可维护性,大多数css样式都是通过这种方式引入的

  • 内部样式表

    直接将css样式放入style标签置于head标签内

  • 内联样式表

    直接将css样式写入html元素的style属性

盒子模型与BFC

在一个文档中,每个元素都被表示为一个矩形的盒子。确定这些盒子的尺寸, 属性 (颜色,背景,边框方面) 和位置是渲染引擎的目标。

在CSS中,使用标准盒模型描述这些矩形盒子中的每一个。这个模型描述了元素所占空间的内容。每个盒子有四个边:外边距边(margin), 边框边(border), 内填充边(padding) 与 内容边(content)

行内元素和块级元素

内联元素(inline)

  • 和相邻的内联元素在同一行

  • padding和margin的left和right可以进行设置改变

  • 宽度(width)、高度(height)、内边距的top/bottom(padding-top/padding-bottom)和外边距的top/bottom(margin-top/margin-bottom)都不可改变,固定为里面文字或图片撑开的大小

块级元素(block)

  • 总是独占一行,表现为另起一行开始,而且其后的元素也必须另起一行显示

  • 宽度(width)、高度(height)、内边距(padding)和外边距(margin)都可控制

内联块元素(inline-block)

  • 拥有内在尺寸,可设置高宽,但不会自动换行

  • 一些浏览器默认的inline-block元素: input 、img 、button 、textarea 、label

外边距重叠

在CSS当中,相邻的两个盒子(可能是兄弟关系也可能是祖先关系)的外边距可以结合成一个单独的外边距。这种合并外边距的方式被称为折叠,并且因而所结合成的外边距称为折叠外边距。

折叠结果遵循下列计算规则:

  • 两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值

  • 两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值

  • 两个外边距一正一负时,折叠结果是两者的相加的和

BFC

Formatting context

在说BFC之前,先解释一下Formatting context,即FC。其是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有 Block fomatting context (简称BFC)和 Inline formatting context (简称IFC)。CSS2.1 中只有 BFC 和 IFC, CSS3 中还增加了 GFC 和 FFC。

BFC定义

BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box(块级元素)参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。

BFC内部规则

  • 内部的Box会在垂直方向,一个接一个地放置

  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠

  • 每个box的margin-left,与内容块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此

  • BFC的区域不会与float box重叠

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此

  • 计算BFC的高度时,浮动元素也参与计算

BFC的作用

BFC的常用功能如下

  • 文档布局

  • 清除浮动

  • 清除重叠外边距

触发BFC的条件

  • 根元素

  • float属性不为none

  • overflow不为visible

  • position为absolute或fixed

  • display为inline-block, table-cell, table-caption, flex, inline-flex

例子

  • 自适应两栏式布局

<style>
    body {
      margin: 0;
      padding: 0;
      position: relative;
    }
    .aside {
        width: 200px;
        height: 150px;
        float: left;
        background: #f66;
    }
    .main {
        height: 200px;
        overflow: hidden;
        background: #fcc;
    }
</style>

<body>
    <div class="aside"></div>
    <div class="main"></div>
</body>

效果

根据BFC第三条规则,虽然存在浮动元素aside,但是main元素的左边依然会跟包含块的border相接触,这时通过设置main元素的overflow:hidden触发产生一个新的BFC,便实现了简单的两栏自适应布局。

更多例子可以查看

选择器与优先级

选择器

css通过选择器关联html标签,以达到控制html元素样式的效果,基本的选择器有以下几种:

  • 派生选择器: 直接用html标签进行选择

  • 类选择器: 使用html元素的class属性进行选择

  • id选择器: 使用html元素的id属性进行选择

以上三种选择器为css中最基本的选择器,其他选择器都是三者的延伸、扩展或者组合,例如:

  • 伪元素选择器: 利用 : 选择伪元素

  • 后代选择器: 利用空格进行后代的选择

      .header .nav{//选取header类中的nav类}
    
  • 兄弟选择器(猫头鹰选择器): 使用+来选择两个紧接着的元素,且它们拥有相同的父元素,因为其样子酷似猫头鹰,所以又被称为猫头鹰选择器,猫头鹰选择器虽然冷门,但是应用在多个相同元素的排列的时候会自动帮你处理一些边缘问题,这里就不展开了详情请戳

      .warp div + div{//选取warp类下的所有兄弟div}
    
  • 群组选择器: 利用逗号进行多个元素的选取

      div, .avatar, a{//选取所有div、avatar类和a标签}
    
  • 属性选择器: 利用中括号选择带有特定属性的元素

      a[title]{//选择所有带有title属性的a标签}
    

优先级(特殊性)

一般来说,越复杂越精确的选择器优先级就越高,在css权威指南上,是这样来定义和区分优先级的:

选择器的特殊性由选择器本身的组件确定。特殊性值表现为4个部分,如:0, 0, 0, 0

一个选择器的具体特殊性如下确定:

  • 对于选择器中给定的各个ID属性值,加 0100

  • 对于选择器中给定的各个类属性值、属性选择或伪类,加 0010

  • 对于选择器中给定的各个元素或伪元素,加 0001。伪元素是否有特殊性?在这方面CSS2有些自相矛盾,但是在CSS2.1中明确指出,伪元素有特殊性,并且为0001

  • 结合符和统配选择器对特殊性没有任何贡献(后面还会更多地介绍这些值)

  • 内联样式的声明特殊性都是1000

  • 重要性:有时某个声明可能非常重要,超过了其他所有声明。CSS2.1称之为重要声明,并允许这些声明的结束分号之前插入 !important 来标志

CSS的优先级还遵循叠加规则,即

span#xxx .songs li{//这个选择器的特殊性为0112}

补充

* 号代表通配符,选取文档中的所有元素,一般不建议使用,首先过于暴力,其次影响渲染性能

样式最后的渲染效果与样式定义在文件中的先后顺序有关,即后面的覆盖前面的,与在html文档中的先后关系无关。例如:

<style>
  .A{ color:blue;}
  .B{ color:red;}
</style>

<body>
<p class='B A'> 123 </p>
</body>

最后“123”的颜色是 red

CSS Hack

什么是CSS Hack

由于不同厂商的流览器或某浏览器的不同版本(如IE6-IE11,Firefox/Safari/Opera/Chrome等),对CSS的支持、解析不一样,导致在不同浏览器的环境中呈现出不一致的页面展现效果。这时,我们为了获得统一的页面效果,就需要针对不同的浏览器或不同版本写特定的CSS样式,我们把这个针对不同的浏览器/不同版本写相应的CSS code的过程,叫做CSS hack

CSS Hack的方法

  • 条件注释法

<!--[if IE]>
这段文字只在IE浏览器显示
<![endif]-->
  • 类内属性前缀法

.hack{  
  background-color:red; /* All browsers */  
  background-color:blue !important;/* All browsers but IE6 */  
  *background-color:black; /* IE6, IE7 */  
  +background-color:yellow;/* IE6, IE7*/  
  background-color:gray\9; /* IE6, IE7, IE8, IE9, IE10 */  
  background-color:purple\0; /* IE8, IE9, IE10 */  
  background-color:orange\9\0;/*IE9, IE10*/  
  _background-color:green; /* Only works in IE6 */  
}  
  • 媒体查询

@media \0screen\,screen\9 {
  .hack{
    /* IE 6 7 8 */
  }
}

@media screen and (-webkit-min-device-pixel-ratio:0) {
   .demo {
      /* Webkit内核 */
    }
}

基础布局

垂直居中

方法有很多,我大概罗列一下我用得比较多的

  • 包裹法

<div id="warpper">  
  <div id="content">Content here</div>
</div>

#warpper {
  float: left;
  height: 50%;
  margin-bottom: -150px;
}

#content {
  clear: both;
  height: 300px;
  position: relative;
}

这种方法在需要垂直居中的div外再包裹一个div,并将其设置浮动,margin-bottom为内部div的 1/2 * height,之后content触发BFC,内部也能放元素。优点是兼容性比较好,缺点是增加了额外的元素,并且高度不能改变。

  • flexbox

<div id="warpper">  
  <div id="content">Content here</div>
</div>

#warpper {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

用起来最舒服最简单的方法,但是,兼容性是个大问题,比如IE要IE11才兼容,很尴尬。

水平居中

水平居中比较简单,一般不会问到,但是作为复习也说得过去

  • margin

<div id="content">Content here</div>


#content {
  margin: 0 auto;
}
  • 绝对定位

<div id="content">Content here</div>

.content {
    position: absolute;
    width: 200px;
    left: 50%;
    margin-left: -(width/2);
}
  • flexbox

<div id="warpper">  
  <div id="content">Content here</div>
</div>

#warpper {
  display: flex;
  align-items: center;
}

栅格布局

原理

一个栅格布局一般分为:容器(container)、行(row)、列(col)

其中容器用于确定整体布局的宽度,设定了容器的宽度以后,再设置一个容器中容纳多少行,最后在在每一行中确定有多少列,同时列与列之间的间隔也被提前规定,这使得我们在布局的时候就很简单了,不用过多的考虑对齐问题。其中列是真正显示文档内容的元素。

列宽

一个能用的栅格系统,会提前准备一个声明列宽的类,将容器的宽度平分为几个等分,这个等分一般时3或4的倍数,这样比较容易排版,当然,分的越多排版越精确

嵌套

栅格系统可以嵌套,即列也可以作为容器继续嵌套栅格,这便是嵌套栅格

列的换行与行的偏移

一个完美支持响应式的栅格系统支持列的换行,即同一行的不同列之间的高度可以不相同,这种行为能够大大加强栅格系统的响应式能力。同时其还支持列的偏移,即可以不从第一列开始,将某几列作为空隙。

具体的栅格系统可参考bootstrap官网

后记

作为一个菜鸟能罗列收集的东西大概也就那么多了,还有一些比较基础的由于篇幅原因就没有放上来,当然还有好多也很重要的东西没罗列出来,这要归结于自己还是太菜,要是对你有帮助的话,就点个赞咯。有问题的话也接受一切批评和建议,我会努力加油的。上一篇文章在本篇头部,所有文章收录于我的博客中。

查看原文

赞 2 收藏 9 评论 0

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

校招进行时(三)---html基础

这篇文章衔接上篇,主要罗列一些前端面试中可能问到的html中最基本的问题。

(格式有点乱,内容有点水,罗列了一些基本用法,大家随便看看)

常用标签

容器标签

  • div

    div标签本身无特殊意义,作为一个块级容器,主要用于组合其他html元素常用于页面的布局。

  • span

    span标签与div标签类似,本身无特殊意义,但它在结合诸如class,lang,或者dir属性时,可作为行内元素的容器。它起到描述(文档内容)的作用。

文本标签

  • h1-h6

    h标签用于设置网页标题或文章标题,为了符合语义化,尽量用h1作为整个网页或网站的标题,h2作下一级标题,以此类推

  • p

    p标签用于设置网页的文体,是大多数文字的主要标签,表示文章或某些文字的一个段落。

  • em 与 strong

    em标签的作用是强调内容,strong标签的作用是着重内容,在浏览器中都会被渲染成加粗字体,但是在英文文章中,表示强调的文字会用斜体,例如:专有名词、术语、外来名词等;而strong则使用粗体,表示需要着重表现的文字。同时,如果使用盲人阅读设备,strong标签中的内容会被重读。

列表与表格标签

  • ul、ol 与 li

    ul标签代表多项无序列表,即无数值排序的集合;而ol标签代表多项有序列表,是有数值排序的集合。当li标签嵌套在ul标签中时,是无序列表中的列表项,此时li的顺序在列表中没有意义;而当li嵌套在ol标签中时,则是有序列表中的列表项,此时第一个li标签则排序为一,以此类推。

  • dl 与 dt、dd

    dl是一个定义列表,用来解释说明一些术语或特定词句。其中dt为术语部分(待解释部分),dd为dt的解释说明部分。

  • table

    table标签用于定义表格,在早期由于浏览器对css技术的不支持,人们大量利用table标签进行页面布局,在现代的前端开发中已经摒弃了这种布局方式。但是也不用一棒子打死,table布局在布局表单内容时还是要方便的多。

    在使用table标签时,tr标签定义行,th标签定义表头,td标签定义表格单元,更复杂的表格还会包含caption、col、colgroup、thead、tfoot 以及 tbody 元素。

    注意:在 HTML5 中,table标签仅支持 "border" 属性,并且只允许使用值 "1" 或 ""

表单标签

  • form

    form标签在文档中定义了一个表单,表单中有各种表单控件,最后浏览器会将表单中的信息提交到服务器。其中,form标签有几个常用的重要属性:

    • name

    name标签可以让我们方便的用js找到某个特定的表单,从而找到此表单下的表单控件,这样就可以对表单中的各个部分进行控制了。(form表单中的表单控件也有name属性)

    • action

    action属性是当前表单所要提交到的服务器处理url,表单会被提交到action属性中的页面进行处理。

    • method

    提交表单到服务器的方法,可选GET和POST,两个方法的特点和作用可到网上查阅,今后我也会慢慢整理。

  • input

    input标签用于接收用户的填写的信息,通过form表单提交到服务器,同时通过设置type属性的不同值可以赋予input标签不同的功能,常用功能如下:

    • text(默认): 用于接收文本信息如用户名等

    • password: 用于接收密码

    • radio: 单选按钮(使用value属性标注提交值)

    • checkbox: 复选框(使用value属性标注提交值)

    • file: 文件上传

    • image: 图像上传

    • data: 输入日期控件(年月日)

  • button

    将button标签归类到这里其实是不太合适的(但是我不知道怎么归了啊=。=)button标签在表单中主要是用于提交表单,当用户填写完成后点击按钮进行表单的提交等操作。通过设置type属性也有不同的作用:

    • submit: 此按钮提交表单数据给服务器。未指定时,此值为默认值,或者如果此属性动态变为空值或无效值

    • reset: 此按钮重置所有组件为初始值

    • button: 此按钮没有默认行为。它可以有与元素事件相关的客户端脚本,当事件出现时可触发

  • select 与 option

    select标签为下拉菜单,需要配合option标签一起使用,option标签为下拉菜单中的选项。通过指定select标签中的mutiple或size属性可设置select为下拉菜单或是列表框

  • textarea

    用于定义多行文本域,cols和rows属性是必须要填写的,他们用于指定文本域的宽度和高度。多行文本域比较特殊,除了普通的事件属性,他还可以指定onselect属性,用于表示文本域里面的内容被选中时候的事件。

超链接(锚点)

  • a

    a标签用于创建一个到其他网页,文件,或同一页面内的位置,当然也可以是电子邮件地址或任何其他URL的超链接。下面是几个常用的属性:

    • href

    这是一个必需属性为锚定义一个超文本链接来源。这表示链接目标的URL或URL片段

    • download

    此属性指示浏览器下载URL而不是导航到URL,因此将提示用户将其保存为本地文件。

    如果属性有一个值,它将在保存提示中用作预先填写的文件名 (用户仍然可以根据需要更改文件名)。对允许的值没有限制,但是 / 和 被转换为下划线。大多数文件系统限制文件名中的一些标点符号,浏览器会相应地调整建议的名称。

    • target

    该属性指定在何处显示链接的资源。 取值为标签(tab),窗口(window),或框架(iframe)等浏览上下文的名称或其他关键词。以下关键字具有特殊的意义:

    • _self: 当前页面加载,会覆盖掉当前页面。此值是默认的,如果没有指定属性的话

    • _blank: 新窗口打开,根据浏览器的不同设置,会在新标签页或新的浏览器窗口中打开页面

    • _parent: 加载响应到当前框架的父框架或当前的HTML5浏览上下文的父浏览上下文。如果没有parent框架或者浏览上下文,此选项的行为方式相同_self。

    • _top: IHTML4中:加载的响应成完整的,原来的窗口,取消所有其它frame。 HTML5中:加载响应进入顶层浏览上下文(即,浏览上下文,它是当前的一个的祖先,并且没有parent)。如果没有parent框架或者浏览上下文,此选项的行为方式相同_self

图片标签

  • img

    用来设置文档中的图像内容,主要属性如下:

    • src: 图像的 URL,这个属性对 <img> 元素来说是必需的

    • alt: 这个属性定义了描述图像的替换文本。如果图像的URL是错误的,该图像不在支持的格式列表中,或者如果图像还没有被下载,用户将看到这个显示。

    注意:在标准规范中,省略这个属性表明该图像是内容的关键部分,但没有等效的文本可用。把这个属性设置为空字符串,表明该图像不是内容的关键部分,非可视化浏览器在渲染的时候可能会忽略它。

html5新增常用标签

3D效果与动画

  • canvas

canvas 标签定义图形,比如图表和其他图像。这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。

音频视频

  • audio

audio 标签用于在文档中表示音频内容。它可以包含多个音频资源, 这些音频资源可以使用 src 属性或者source 元素来进行描述; 浏览器将会选择最合适的一个来使用。对于不支持 audio 元素的浏览器,audio 元素也可以作为浏览器不识别的内容加入到文档中。

  • video

用于在文档中嵌入视频内容。

ps.对于html5来说,常用的标签大概就这么多,原因还是浏览器的兼容问题,大多数标签使用起来不方便。主要使用还是特定场景下的api调用,如摄像头,gps定位等。html5的具体特性可查阅MDN:HTML5

特殊标签

  • <!DOCTYPE>

准确的说,!DOCTYPE不应该算是一个html标签。<!DOCTYPE> 告知浏览器当前的 HTML (或 XML)文档是哪一个版本,应该用那种规范来解析当前文档. Doctype 是一条声明,而不是一个标签; 也可以把它叫做 "文档类型声明", 或 简称为 "DTD".

  • meta

meta标签位于html文档头部中的head标签中。meta标签用来描述一个HTML网页文档的属性,例如作者、日期和时间、网页描述、关键词、页面刷新等元数据。这些数据将用于服务搜索引擎和其他网络服务.

由于meta标签的属性实在太多,这里列举几个常用属性:

  • name

    name属性主要用于描述网页,比如网页的关键词,叙述等。与之对应的属性值为content,content中的内容是对name填入类型的具体描述。其中name属性有三个常用的取值,分别是:

    • keyword: 告诉搜索引擎你网站的关键词

    • description: 用于告诉搜索网站你网站的主要内容

    • viewport: 移动设备窗口设置

    其中重点说下viewport的设置:

    width: 控制 viewport 的大小,可以指定的一个值,例如 600 或 device-width 为设备的宽度(单位为缩放为 100% 时的 CSS 的像素)

    height: 和 width 相对应,指定高度

    initial-scale: 初始缩放比例,也即是当页面第一次 load 的时候缩放比例

    maximum-scale: 允许用户缩放到的最大比例

    minimum-scale: 允许用户缩放到的最小比例

    user-scalable: 用户是否可以手动缩放

      <meta name="viewport" content="width=device-width, initial-scale=1">
    
    • http-equiv

      这个属性用于设置http请求相关参数。使用方法与name一样,需要配合content使用,先使用http-equiv定义,再使用content进行相关设置。

      • content-Type: 设置字符集,在html5中已经修改为 charset,一般推荐设置成 utf-8 字符集

          <meta charset="utf-8">
        
      • X-UA-Compatible: 设置浏览器采用何种版本渲染当前页面,一般选择最新版本

          <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        
      • cache-control: 指定请求和响应遵循的缓存机制

      no-cache: 先发送请求,与服务器确认该资源是否被更改,如果未被更改,则使用缓存。

      no-store: 不允许缓存,每次都要去服务器上,下载完整的响应。(安全措施)

      public : 缓存所有响应,但并非必须。因为max-age也可以做到相同效果

      private : 只为单个用户缓存,因此不允许任何中继进行缓存。(比如说CDN就不允许缓存private的响应)

      maxage : 表示当前请求开始,该响应在多久内能被缓存和重用,而不去服务器重新请求。例如:max-age=60表示响应可以再缓存和重用 60 秒。

      no-siteapp: 禁止自动转码。假设某网页没有进行移动端适配,在移动端进行浏览时,从某个入口(例如百度)进入该网页,可以防止该入口对网页进行移动设备转码。虽然转码的意图是好的,但是有的时候转码之后效果不尽人意,就可以设置这个属性。

        <meta http-equiv="Cache-Control" content="no-siteapp" />
      
查看原文

赞 4 收藏 28 评论 11

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

webpack

个人博客前端渣渣不定期分享自己所学的前端知识

使用webpack也有一段时间了,但是没有系统的去学习,最近无心代码,正好用这段时间系统地看一下webpack,借鉴一下前辈们的经验,防止忘记,记录下来。

什么是webpack


Webpack 是一个开源的前端打包工具。Webpack 提供了前端开发缺乏的模组化开发方式,将各种静态资源视为模组,并从它生成优化过的程式码。

Webpack可以从终端、或是更改 webpack.config.js 来设定各项功能。

要使用 Webpack 前须先安装 Node.js。Webpack 其中一个特性是使用载入器来将资源转化成模组。开发者可以自订载入器的顺序、格式来因应专案的需求。

上面是来自wiki的解释,简单来说webpack 是一个模块打包器。

它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。为前端开发人员提供了一种模组化的开发方式。webpack将开发过程中的各个js等文件进行打包成组件,可以说,在webpack中,所有资源都是模块。各个模块在使用的时候加载,相对于之前<script>的加载方式来说,webpack解决了像全局变量冲突,js必须按顺序加载等问题,同时方便开发人员进行资源管理,上手后简单易用。

安装


由于webpack依赖于Node.js运行,所以在安装webpack前要先安装Node.js。

安装完Node.js后,利用其包管理工具npm安装webpack:

$ npm install webpack -g

这样就将webpack安装到了全局环境,可以运行

$ webpack -v(或-h)

来查看已安装的webpack的版本号(或使用帮助)

在一般的开发环境中,我们会进入一个已经初始化的项目目录,使用命令

$ npm install webpack --save-dev

来安装webpack到项目中,之后就能够在项目中的配置文件中来配置具体项目的webpack,方便与他人协同的开发,又不会与本地的全局配置产生冲突。

怎么用


安装以后当然就是使用啦,这边我就不贴例子了,可以移步阮一峰的教程教程里面的例子怎么使用阮大已经讲得很清楚了

ps.教程中有提到,一些例子要跑起来需要安装依赖,要安装这些依赖可以在clone的项目目录下运行

$ npm install --save-dev

有关这方面的具体解释可以到网上搜索一下有关npm的知识,这里就略过了。

Loader


webpack默认只能加载js文件,要想加载其他资源文件,就需要使用loader进行转换,转换以后可以使用require进行加载

ps.loader与require的例子在上面阮大的教程中也有,具体使用可以进一步看看。

关于配置


webpack的具体工作方式,可以修改项目中的配置文件,文件一般是

webpack.config.js

具体配置方法需要对应到具体的项目实际当中,这一步我也没有研究过,在这就先略去,以后有机会碰到了,再继续往下延伸。

延伸阅读

Loader

require

webAPP

单页面应用

查看原文

赞 0 收藏 1 评论 2