<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0">
  <channel>
    <title>IT瘾javascript推荐</title>
    <link>https://itindex.net/tags/javascript</link>
    <description>IT社区推荐资讯 - ITIndex.net</description>
    <language>zh</language>
    <copyright>https://itindex.net/</copyright>
    <generator>https://itindex.net/</generator>
    <docs>http://backend.userland.com/rss</docs>
    <image>
      <url>https://itindex.net/images/logo.gif</url>
      <title>IT社区推荐资讯 - ITIndex.net</title>
      <link>https://itindex.net/tags/javascript</link>
    </image>
    <item>
      <title>如何优雅的实现网页多主题风格换肤功能？</title>
      <link>https://itindex.net/detail/62655-%E7%BD%91%E9%A1%B5-%E5%8A%9F%E8%83%BD</link>
      <description>&lt;p&gt;  &lt;strong&gt;海阔凭鱼跃，天高任鸟飞。好久不见！我是猫力Molly&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;对于网页换肤，例如最常见的深色、浅色风格已经是很常见的一个需求了。一直以来也有很多的实现方案，这里我主要介绍一下基于   &lt;code&gt;CSS variable&lt;/code&gt;的实现方式&lt;/p&gt; &lt;h2&gt;简单列举下一些其它实现方式&lt;/h2&gt; &lt;p&gt;  &lt;strong&gt;1、把不同风格样式写到不同的类名下面，通过切换类名来实现换肤&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这种方式没啥明显的优点，只是单纯的实现了此需求。反而增加了css样式文件代码冗余且会造成大量重复代码，样式代码不利于拓展维护，且开发效率低下&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2、实现多套主题样式文件，通过 link 标签动态加载不同的样式文件&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这种方式的优点大概是做到了按需加载吧，但同时也造成了需要拷贝大量重复代码来单独修改，也算是做到了样式隔离，相比上一种方式稍稍提高了一点可维护性吧&lt;/p&gt; &lt;p&gt;在多个样式文件切换的时候，可能会有加载延迟。这时候可以考虑使用   &lt;code&gt;alternate&lt;/code&gt; 来解决&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3、通过less或sass的变量方式实现&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这种方式我们可以将所有风格变量抽离出来，在样式代码中直接使用该变量，是一种比较推荐的方式。极大提高了代码的拓展性和维护性&lt;/p&gt; &lt;h2&gt;CSS variable的实现方式&lt;/h2&gt; &lt;p&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000043451608" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;如图所示，目前主流浏览器都已经支持  &lt;code&gt;css variable&lt;/code&gt;，我们尽管放心使用&lt;/p&gt; &lt;p&gt;  &lt;code&gt;CSS variable&lt;/code&gt; 允许我们在 css 里面声明变量，在变量前加上两根小横线即可（--）&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;body {
  --foo: #000;
  --bar: #fff;
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;需要注意的是  &lt;code&gt;css vars&lt;/code&gt;变量声明，区分大小写  &lt;code&gt;--foo&lt;/code&gt; 与   &lt;code&gt;--Foo&lt;/code&gt; 是两个不同的变量&lt;/p&gt; &lt;h3&gt;var() 函数&lt;/h3&gt; &lt;p&gt;使用var()函数来读取变量&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;p{
    color:var(--foo)
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;code&gt;var()&lt;/code&gt;函数支持第二个参数，用于表示变量的默认值，如果变量值不存在，则以默认值为准&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;p{
    color:var(--fooo, #ccc)
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;关于  &lt;code&gt;var()&lt;/code&gt;函数此处不做过多赘述，详情请查阅  &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/var" rel="nofollow noreferrer"&gt;官方文档&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;方案落地&lt;/h2&gt; &lt;p&gt;大致思路：不管深色或是浅色风格，我们都可以把它视作一个个主题。把每个主题的颜色值、盒子宽高、图片地址等抽离为一个字典对象结构。一个主题对应一个配置文件，再通过切换配置文件来实现主题风格的变化&lt;/p&gt; &lt;h3&gt;一、和UI设计师沟通好各主题的色阶&lt;/h3&gt; &lt;p&gt;一个主题对应一份配置文件，所以我们需要提前和UI设计师沟通好各主题对应的色阶，字号，一些通用样式规则等&lt;/p&gt; &lt;p&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000043451609" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000043451610" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;code&gt;css vars&lt;/code&gt;变量名称是不变的，变量值随着主题的切换而发生改变&lt;/p&gt; &lt;p&gt;我的UI同事使用的是 figma，然后我发现 figma 右侧的信息栏里面有颜色编号，正好可以使用这个来当做变量名称。在编码阶段，看到这个编号，就知道用什么变量名了，非常方便。&lt;/p&gt; &lt;p&gt;如果你的UI同事使用的是别的设计工具，最好也是提前约定好变量名，使其大家都方便&lt;/p&gt; &lt;p&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000043451611" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;二、将各主题色阶抽离为一个字典对象&lt;/h3&gt; &lt;p&gt;dark.js&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
  &amp;apos;--grey900&amp;apos;: &amp;apos;#EBEEF5&amp;apos;,
  &amp;apos;--grey600&amp;apos;: &amp;apos;#A7ABC0&amp;apos;,
  &amp;apos;--grey500&amp;apos;: &amp;apos;#72768D&amp;apos;,
  &amp;apos;--grey400&amp;apos;: &amp;apos;#5D6177&amp;apos;,
  &amp;apos;--grey300&amp;apos;: &amp;apos;#404759&amp;apos;,
  &amp;apos;--grey200&amp;apos;: &amp;apos;#2C323E&amp;apos;,
  &amp;apos;--grey100&amp;apos;: &amp;apos;#282B32&amp;apos;,
  &amp;apos;--grey50&amp;apos;: &amp;apos;#171B22&amp;apos;,
  &amp;apos;--grey0&amp;apos;: &amp;apos;#222730&amp;apos;,
  ...
}  &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;white.js&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
  &amp;apos;--grey900&amp;apos;: &amp;apos;#1F2429&amp;apos;,
  &amp;apos;--grey600&amp;apos;: &amp;apos;#646C73&amp;apos;,
  &amp;apos;--grey500&amp;apos;: &amp;apos;#8D9399&amp;apos;,
  &amp;apos;--grey400&amp;apos;: &amp;apos;#C3C7CB&amp;apos;,
  &amp;apos;--grey300&amp;apos;: &amp;apos;#E4E6E7&amp;apos;,
  &amp;apos;--grey200&amp;apos;: &amp;apos;#EFF0F1&amp;apos;,
  &amp;apos;--grey100&amp;apos;: &amp;apos;#F4F5F6&amp;apos;,
  &amp;apos;--grey50&amp;apos;: &amp;apos;#F8F9FA&amp;apos;,
  &amp;apos;--grey0&amp;apos;: &amp;apos;#FFFFFF&amp;apos;,
  ...
}  &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;三、通过js设置style变量&lt;/h3&gt; &lt;p&gt;这里我们需要用到   &lt;code&gt;document.body.style&lt;/code&gt; 的api&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// 设置变量
document.body.style.setProperty(&amp;apos;--foo&amp;apos;, &amp;apos;#666&amp;apos;)

// 读取变量
document.body.style.getPropertyValue(&amp;apos;--foo&amp;apos;)

// 删除变量
document.body.style.removeProperty(&amp;apos;--foo&amp;apos;)&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;遍历变量字典对象，根据不同主题，给网页设置对应变量&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import C from &amp;apos;@/utils/cssVarMap&amp;apos;

setCssVar (flag) {
  const varList = Object.entries(flag ? C.white : C.dark)
  varList.forEach(([key, val]) =&amp;gt; {
    document.body.style.setProperty(key, val)
  })
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;至此，我们已经完成根据不同主题设置不同主题变量了，可以愉快的在样式文件里面使用  &lt;code&gt;css vars&lt;/code&gt;&lt;/p&gt; &lt;p&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000043451612" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;这种方式操作简单，且极大的提高了代码的拓展性和维护性。之后再有别的主题，也不过是多增加一份配置文件而已，不会增加额外的副作用。&lt;/p&gt; &lt;h3&gt;举一反三&lt;/h3&gt; &lt;h4&gt;1、结合媒体查询&lt;/h4&gt; &lt;p&gt;通过结合媒体查询，我们可以实现更复杂的交互场景&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;body {
  --foo: #fff
}

p {
    color: var(--foo)
}

@media screen and (min-width: 768px) {
  body {
    --foo: #000
  }
}&lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;2、结合js业务逻辑&lt;/h4&gt; &lt;p&gt;在一些特殊需求场景下，我们可以结合js业务逻辑，动态追加或编辑   &lt;code&gt;css vars&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;const docStyle = document.documentElement.style;

document.addEventListener(&amp;apos;mousemove&amp;apos;, (e) =&amp;gt; {
  docStyle.setProperty(&amp;apos;--foo&amp;apos;, e.clientX);
});&lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;3、存储一些信息&lt;/h4&gt; &lt;p&gt;既然是声明变量，那么就有存储信息的功能。我们可以试着将一些信息存储在   &lt;code&gt;css vars&lt;/code&gt; 里面，再通过  &lt;code&gt;document.body.style.getPropertyValue(&amp;apos;--foo&amp;apos;)&lt;/code&gt;去读取使用。不过大部分场景应该使用不到这种方法，也算是提供一种思路吧。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;css vars&lt;/strong&gt;是个潜力股，一起来挖掘它更多巧妙的用法吧&lt;/p&gt; &lt;h2&gt;感谢&lt;/h2&gt; &lt;p&gt;欢迎关注我的个人公众号  &lt;a href="https://getapi.run/recommend/yy/1.jpg" rel="nofollow noreferrer"&gt;前端有猫腻&lt;/a&gt;每天给你推送新鲜的优质好文。回复 “福利” 即可获得我精心准备的前端知识大礼包。愿你一路前行，眼里有光！&lt;/p&gt; &lt;p&gt;感兴趣的小伙伴还可以加我  &lt;a href="http://tool.edian.xyz" rel="nofollow noreferrer"&gt;微信：猫力molly&lt;/a&gt;或  &lt;a href="http://tool.edian.xyz" rel="nofollow noreferrer"&gt;前端交流群&lt;/a&gt;和众多优秀的前端攻城狮一起交流技术，一起玩耍！&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript css html react.js vue.js</category>
      <guid isPermaLink="true">https://itindex.net/detail/62655-%E7%BD%91%E9%A1%B5-%E5%8A%9F%E8%83%BD</guid>
      <pubDate>Wed, 22 Feb 2023 10:00:00 CST</pubDate>
    </item>
    <item>
      <title>前端首屏渲染时间的极致优化</title>
      <link>https://itindex.net/detail/62461-%E5%89%8D%E7%AB%AF-%E6%B8%B2%E6%9F%93-%E6%97%B6%E9%97%B4</link>
      <description>&lt;p&gt;  &lt;img alt="20220722174820" src="https://segmentfault.com/img/bVc23MP" title="20220722174820"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们知道，用户体验是 Web 产品最为重要的部分。尽可能减少首屏加载时间，更为流畅地展示用户所需求的内容，会是用户是否留存的关键因素。&lt;/p&gt; &lt;p&gt;而随着现代 Web 业务可供用户的交互行为越来越多，前端项目的复杂度越来越高，每个页面的渲染时间也必然越来越长，这就导致了用户的体验不佳，用户的操作变慢。&lt;/p&gt; &lt;p&gt;为此，前端工程师们在首屏请求的各个阶段中持续钻研，不断探究如何将首次页面渲染的时间减少到更小，力求提供更为优秀的产品体验。&lt;/p&gt; &lt;h2&gt;CSR（Client Side Render）&lt;/h2&gt; &lt;p&gt;  &lt;img alt="20220720162452" src="https://segmentfault.com/img/bVc23MS" title="20220720162452"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;浏览器渲染是最简单，最符合 Web 应用设计思路的渲染方式。&lt;/p&gt; &lt;p&gt;所谓浏览器渲染，就是将应用所需的页面展示、前端逻辑、接口请求全都在用户的浏览器中执行。它很好的实现了前后端的解耦，让前端开发更为独立，也让后台实现更为简单。&lt;/p&gt; &lt;p&gt;同时，为了缓解用户的等待焦虑，我们可以用 loading 态，或者骨架屏，进一步提升异步请求接口时的用户体验。&lt;/p&gt; &lt;p&gt;不过，随着业务复杂程度提高，浏览器渲染的开销也会变大，我们无法控制用户侧使用的机器性能，很多时候，用户使用的机器性能甚至不足以满足应用的需求，造成卡顿，甚至崩溃，这一点在移动端上尤甚。&lt;/p&gt; &lt;p&gt;而浏览器渲染由于前端的动态性过高，也会带来 SEO 不佳的问题。&lt;/p&gt; &lt;h2&gt;SSR（Server Side Render）&lt;/h2&gt; &lt;p&gt;  &lt;img alt="20220720162513" src="https://segmentfault.com/img/bVc23MT" title="20220720162513"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;服务端渲染的出现时间实际上是要比浏览器渲染要更早的。在 Web 应用发展的早期，所有的 ASP、JSP 等模板引擎构建的前端页面实际上就是服务端渲染的结果。而此时的服务端渲染无法进行前后端职责的解耦，因此逐步被浏览器渲染淘汰。&lt;/p&gt; &lt;p&gt;但在处理首屏体验的问题上，服务端渲染有着独到的优势。它能提前再服务端中完成页面模板的数据填充，从而一次性返回完整的首屏内容，从而面对 SEO 的爬取时能获取到更多有效的关键信息。&lt;/p&gt; &lt;p&gt;此外，由于其能快速直出首页的真实数据，体验往往比 loading 态更佳，在 TTI 的表现上更为出色。&lt;/p&gt; &lt;p&gt;但是，服务端渲染也有其自身的局限性。因为从本质上来说，SSR 服务无法完全与前端页面解耦开来。因此市面上较完备的 SSR 解决方案都只解决首屏的服务端渲染，并采用同构的方式，增加一层 node 中间层的方式来解决前端与 SSR 服务的更新同步问题，并与后端开发项目解耦。&lt;/p&gt; &lt;p&gt;但这无疑增加了项目的复杂度，并且随着业务的复杂程度变高，服务端渲染往往需要调起多个接口去请求数据并填充页面，这样可能会导致在 TTFB 上有一定劣势。&lt;/p&gt; &lt;p&gt;当然，最重要的是，服务端渲染对于服务器的负载要求是很高的。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220722153734" src="https://segmentfault.com/img/bVc23MU" title="20220722153734"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;上图是引用的字节的某项目的 SSR 服务的单机 QPS 承载表现。我们可以看出，对于一个高访问量的网页应用来说，提供一个较为复杂的 SSR 服务的成本是相当高的，需要花费大量的金钱来堆机器。&lt;/p&gt; &lt;p&gt;因此，从降本增效的角度考虑，我们需要评估 SSR 带来的 ROI 是否符合预期。&lt;/p&gt; &lt;h2&gt;NSR（Native Side Render）&lt;/h2&gt; &lt;p&gt;在移动互联网的浪潮下，移动端机能飞速提升，那么 Web 应用是否能搭上这一班车，将 Native 的性能利用起来，提升页面渲染性能呢？答案是肯定的，这就需要介绍到 NSR 了。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220720162547" src="https://segmentfault.com/img/bVc23MV" title="20220720162547"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;Native 渲染的本质其实还是 SSR，只不过提供服务的 Server 转变为了客户端。由于需要用到客户端机能，因此此种实现通常应用在移动端 APP，或者 PWA 下。&lt;/p&gt; &lt;p&gt;当链接被点击时，先借助浏览器启用一个 JS 运行时，并加载 APP 中存储的 Html 模板，发送 xhr 请求预加载页面数据，从而在客户端本地拼接并渲染生成一个有数据的 Html 首屏，形成首次 NSR。同时可以将该首屏 Html 缓存在客户端，供下次页面打开时，实现   &lt;code&gt;stale-while-revalidate&lt;/code&gt; 的缓存效果。&lt;/p&gt; &lt;p&gt;由于 NSR 将服务器的渲染工作放在了客户端的一个个独立设备中，既实现了页面的预加载，同时又不会增加额外的服务器压力。达到秒看的效果。&lt;/p&gt; &lt;p&gt;这种能力在拥有客户端或者支持 PWA 的应用中应用广泛，例如手 Q，腾讯文档 APP 中都拥有通过 APP 中的离线包来实现首屏渲染加速的能力。&lt;/p&gt; &lt;h2&gt;ESR（Edge Side Render）&lt;/h2&gt; &lt;p&gt;那么，对于纯 Web 应用，而又由于兼容性等原因暂时无法支持 PWA 的页面，有没有一个合适的首屏渲染加速方案呢？&lt;/p&gt; &lt;p&gt;随着云与边缘计算的快速发展，前端页面也需要考虑分布式的请求处理优化。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220720162606" src="https://segmentfault.com/img/bVc23MW" title="20220720162606"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们知道，CDN 节点相比真实服务节点更贴近用户，能更快将内容返回。因此我们可以将静态的 Html 内容模板缓存在 CDN 上。当接到请求时，先快速将静态模板页面返回给用户，同时在 CDN 服务器上对页面动态部分发起向后端发起请求，并将获取到的动态内容在以流式下发的方式继续返回给用户。&lt;/p&gt; &lt;p&gt;这里实际上利用到了 HTTP 的 SSE（Server Send Events）协议，通过服务器向客户端发送单向事件流，实现同一个 Html 文件的分块传输预渲染。&lt;/p&gt; &lt;h2&gt;最佳实践是？&lt;/h2&gt; &lt;p&gt;这也是我们最近在腾讯文档中探索实践并落地的，通过服务中间节点的流式下发能力，实现多级首屏渲染加速。&lt;/p&gt; &lt;p&gt;对于一个复杂前端页面来说，首屏需要加载和运算的资源类型可能有很多，有需要客户端解析并执行的 JS 动效，也有需要服务端获取数据直出的数据分片展示页面。&lt;/p&gt; &lt;p&gt;通常来说，客户端只能等待服务端获取分片数据，并生成经过 SSR 渲染后的 HTML，才能开始进行 script 解析与 js 资源拉取的行为，最终渲染出完整的页面数据以及动效。&lt;/p&gt; &lt;p&gt;而既然他们所需要的计算方式不同，那么为什么不能并行来做呢？&lt;/p&gt; &lt;p&gt;我们可以在版本发布前，将未经过服务端直出的模板 HTML 进行解析，将需要发起资源请求的所有的外链脚本 url 提取出来，生成一个 HTML Header 结构，并将该 Header 内容伪装为正常 HTML 缓存在 CDN 节点中。&lt;/p&gt; &lt;p&gt;结合之前我们介绍的 HTTP SSE 协议，当用户请求时，我们可以以最快的速度向用户返回 CDN 中的 HTML header，从而让用户的浏览器提前拉取并解析外链资源。于此同时，CDN 节点将用户的请求转发给真实的服务端，从而让服务端进行真实数据的获取拼接并返回给客户端。&lt;/p&gt; &lt;p&gt;由于客户端此时已经提前拉取了外链资源，因此收到服务端分片的 SSR 后，客户端可以直接将真实数据渲染到页面中，而不需要再次等待外链资源的解析。&lt;/p&gt; &lt;p&gt;由于并行的关系，这样的 SSR 与 NSR 混合方式能大大降低复杂页面首屏渲染的时间，提升用户体验。&lt;/p&gt; &lt;p&gt;以百度首页的请求为例，通过 Chorme Network 提供的瀑布图，通过我们可以直观的看到一条请求的执行过程。&lt;/p&gt; &lt;p&gt;  &lt;img alt="20220913172530" src="https://segmentfault.com/img/bVc23MX" title="20220913172530"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;我们可以看出，除了 DNS 寻址与 SSL 建连是我们无法控制的以外，占用请求时间的大头是   &lt;code&gt;Waiting for server response&lt;/code&gt;，请求服务器 (CDN) 的时间，以及   &lt;code&gt;Content Download&lt;/code&gt;，外链资源的拉取时间。&lt;/p&gt; &lt;p&gt;而使用本文的混合方案后，理论上可以使总请求时间降低到 Max(A, B), (A 为   &lt;code&gt;Waiting for server response&lt;/code&gt;，B 为   &lt;code&gt;Content Download&lt;/code&gt;) 的水平。（当然，实际操作过程中，由于 CDN 节点进行了一次请求转发，因此拥有 SSR 能力的页面请求返回时间会更长一些）。&lt;/p&gt; &lt;h2&gt;结语&lt;/h2&gt; &lt;p&gt;前端的页面首屏时间优化是永恒的话题，本文介绍了前端界对首屏时间优化的进程，并提供了一种 SSR 与 NSR 混合的新思路，通过并行处理耗时任务的方式，进一步提升首屏加载时间，希望能够给大家提供一点参考价值。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/62461-%E5%89%8D%E7%AB%AF-%E6%B8%B2%E6%9F%93-%E6%97%B6%E9%97%B4</guid>
      <pubDate>Tue, 18 Oct 2022 11:19:21 CST</pubDate>
    </item>
    <item>
      <title>面向微前端，谈谈 JavaScript 隔离沙箱机制的古往今来</title>
      <link>https://itindex.net/detail/62300-%E5%89%8D%E7%AB%AF-javascript-%E9%9A%94%E7%A6%BB</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt;

 &lt;p&gt;随着微前端的不断发展、被更多的团队采用，越来越多开始对沙箱这个概念有所了解。  &lt;strong&gt;沙箱，即 sandbox，意指一个允许你独立运行程序的虚拟环境，沙箱可以隔离当前执行的环境作用域和外部的其他作用域，外界无法修改该环境内任何信息，沙箱内的东西单独运行，环境间相互不受影响。&lt;/strong&gt;本文计划谈谈微前端的 JavaScript 隔离，即沙箱机制的古往今来。&lt;/p&gt;

 &lt;p&gt;要实现一个 JavaScript 沙箱，可以有很多种分类方式，比如按照具体的实现方式来区分，就至少包含如下：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;基于 Proxy 快照存储 + window 修改的实现&lt;/li&gt;
    &lt;li&gt;基于 Proxy 代理拦截 + window 激活/卸载的实现&lt;/li&gt;
    &lt;li&gt;基于普通对象快照存储的 window 属性 diff 实现&lt;/li&gt;
    &lt;li&gt;基于 iframe + 消息通信的实现&lt;/li&gt;
    &lt;li&gt;基于 ShadowRealm 提案的实现&lt;/li&gt;
    &lt;li&gt;基于 with + eval 的简单实现&lt;/li&gt;
    &lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-1.png"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;本文主要考虑沙箱机制在实现时所用到的主要 Web 技术，计划大致分为四类实现方案分别介绍，结合之下，本文目录如下：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;前言&lt;/li&gt;
    &lt;li&gt;基于 Proxy 实现的沙箱机制
       &lt;ol&gt;
          &lt;li&gt;简要谈谈 Proxy API&lt;/li&gt;
          &lt;li&gt;基于 Proxy 的沙箱实现考虑&lt;/li&gt;
          &lt;li&gt;结合微前端框架 qiankun 介绍两类沙箱实现&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
    &lt;li&gt;基于属性 diff 实现的沙箱机制&lt;/li&gt;
    &lt;li&gt;基于 iframe 实现的沙箱机制
       &lt;ol&gt;
          &lt;li&gt;基于 Proxy 及 diff 的沙箱机制边界考虑&lt;/li&gt;
          &lt;li&gt;利用 iframe 实现沙箱机制的几点思考&lt;/li&gt;
          &lt;li&gt;一段 iframe 沙箱的示例代码&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
    &lt;li&gt;各类沙箱机制对比&lt;/li&gt;
    &lt;li&gt;基于 ES 提案 ShadowRealm API 介绍
       &lt;ol&gt;
          &lt;li&gt;什么是 JavaScript 的运行环境实例&lt;/li&gt;
          &lt;li&gt;ShadowRealm API 简介&lt;/li&gt;
          &lt;li&gt;ShadowRealm 的错误捕获与更多应用场景&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
    &lt;li&gt;总结&lt;/li&gt;
    &lt;li&gt;参考&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;希望通过我自己的项目实践、阅读代码、提案梳理等方式对 JavaScript 隔离（沙箱机制）进行系统整理，其中会结合一些开源框架的实现来辅助解读，但不会针对微前端框架深入介绍，也不会就某一个沙箱机制的具体细节实现（比如如何构建闭包环境、属性读取、DOM 操作等众多边界处理）进行剖析。&lt;/p&gt;

 &lt;p&gt;如果你想了解关于 CSS 样式隔离的内容可以搜索 Shadow DOM 相关内容进一步查阅；如果你想了解微前端的主子应用加载、运行机制，可以参考 single-spa 文档、qiankun 文档、ShadowRealm 提案等内容；如果你想了解文中涉及的一些概念与 API 用法可以在 MDN 进行搜索查阅，大部分均有对应介绍。&lt;/p&gt;

 &lt;p&gt;本文在撰写中尽力保证文章的思路流畅和通俗易懂，但由于个人正从事基于微前端方案的开发，可能有些概念会潜意识认为所有读者均已了解，未能详尽每个涉及名词的统一处理或解释，此处针对一些通用的概念进行铺垫：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;   &lt;strong&gt;主应用&lt;/strong&gt;：在微前端方案中，区分主子应用，主应用通常负责全局资源的加载、隔离、控制运行，用户登陆信息等全局状态的管理等等，也被称为基座、微前端全局环境等；&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;子应用&lt;/strong&gt;：微前端方案中可以独立加载运行的一个 Web 应用，通常需要一个完备的隔离环境供其加载，文中提到的沙箱激活/卸载也是为其服务，也称微应用；&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;沙箱&lt;/strong&gt;：意指一个允许你独立运行程序的虚拟环境，沙箱可以隔离当前执行的环境作用域和外部的其他作用域，外界无法修改该环境内任何信息，沙箱内的东西单独运行，环境间相互不受影响，英文对应 sandbox，此名词常与 JavaScript 隔离一起使用；&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;qiankun&lt;/strong&gt;：一款开源方案，基于    &lt;a href="https://github.com/CanopyTax/single-spa"&gt;single-spa&lt;/a&gt; 的   &lt;a href="https://micro-frontends.org/"&gt;微前端&lt;/a&gt;实现库；&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;以下开始正文。&lt;/p&gt;

 &lt;h2&gt;基于 Proxy 的沙箱机制&lt;/h2&gt;

 &lt;p&gt;Proxy 是当下做 JavaScript 隔离用到的最主要的手段之一，接下来我们详细说说基于 Proxy 的沙箱机制。&lt;/p&gt;

 &lt;h3&gt;简要谈谈 Proxy API&lt;/h3&gt;

 &lt;p&gt;  &lt;strong&gt;Proxy 是一个标准 Web API，在 ES6 版本中被推出，这个对象可以用于创建一个对象的代理，从而实现基本操作的拦截和自定义（如属性查找、赋值、枚举、函数调用等）&lt;/strong&gt;，我们可以通过一个简单的例子来解释说明 Proxy 的作用：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log(&amp;apos;c&amp;apos; in p, p.c); // false, 37
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;在上例中，我们定义了一个 handler，其中包含一个 get 拦截器，它的作用是在属性查找时，如果对象对应属性不存在时返回数值 37，此后我们通过 Proxy 对一个空对象进行了代理，分别打印了其中的 a、b、c 属性，可以发现，其中 c 属性由于不存在而返回了 37。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-3.jpeg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h3&gt;基于 Proxy 的沙箱实现考虑&lt;/h3&gt;

 &lt;p&gt;既然 Proxy 可以用于代理对象，那么我们同样可以用其代理 window——Web 应用运行中最重要的上下文环境。每个 Web 应用都会与 window 交互，无数的 API 也同样挂靠在 window 上，要达到允许独立运行的微前端环境，首先需要 window 隔开。&lt;/p&gt;

 &lt;p&gt;在采用 Proxy 作为沙箱机制方案时，主要还是基于 get、set、has、getOwnPropertyDescriptor 等关键拦截器对 window 进行代理拦截（如下如有涉及代码，我们主要关注 get 与 set 两类拦截器）。为了让沙箱的代理拦截完备，除了 window 外，我们通常都需要关注几方面，比如一些难以代理（或者说没必要代理）的 Web API，如 Array、Number、Promise 等，此外还需要保证通过 with、eval、new Function 等方式执行的代码作用域不会逃逸，动态加载的 JavaScript 代码也算一个。&lt;/p&gt;

 &lt;p&gt;谈到这里，我们首先看看通过 Proxy 进行属性查找时的一些处理逻辑。除了在拦截器中进行一些常规的无需拦截 case 判断外，还需要对 Symbol.unscopables 属性 get 拦截器的返回值做些定义，以方便 with 等方式下代码的执行作用域正常处理，如下是个简单的例子：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const unscopables = {
  Array: true,
  Object: true,
  String: true,
  Promise: true,
  requestAnimationFrame: true,
  ...
};

// ...

{
  get: (target: FakeWindow, p: PropertyKey): any =&amp;gt; {
    // Symbol.unscopables 属性
    if (p === Symbol.unscopables) return unscopables;

    // 无需拦截的 Web API
    if (p === &amp;apos;eval&amp;apos;) {
      return eval;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;如上代码中，关于 eval 的拦截判断很好理解，这里我们停下简要介绍一下一个概念：   &lt;code&gt;Symbol.unscopables&lt;/code&gt;。&lt;/p&gt;

 &lt;p&gt;  &lt;code&gt;Symbol.unscopables&lt;/code&gt; 属性，指用于指定对象值，其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。当我们在   &lt;code&gt;unscopables&lt;/code&gt; 对象上将属性设置为 true，将使其   &lt;em&gt;unscopable&lt;/em&gt; 并且因此该属性也将不会在词法环境变量中出现。我们来看一个简单例子，以了解其效果：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const object1 = {
  property1: 42
};

object1[Symbol.unscopables] = {
  property1: true
};

with (object1) {
  console.log(property1);
  // expected output: Error: property1 is not defined
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;  &lt;em&gt;注：在微前端环境下，通常需要对一些全局变量与属性进行更全面的梳理，此处可以参考 qiankun 的实现    &lt;a href="https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/proxySandbox.ts#L255"&gt;https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/proxySandbox.ts#L255&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

 &lt;h3&gt;结合微前端框架 qiankun 介绍两类沙箱实现&lt;/h3&gt;

 &lt;p&gt;微前端框架 qiankun 中一共存在三类沙箱，基于 Proxy 实现方式不同以及是否支持多实例，可以分为两类：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;支持子应用单实例沙箱（LegacySandbox）&lt;/li&gt;
    &lt;li&gt;支持子应用多实例沙箱（ProxySandbox）&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;当我们只针对全局运行环境进行代理赋值记录，而不从中取值，那么这样的沙箱只是作为我们记录变化的一种手段，而实际操作仍在主应用运行环境中对 window 进行了读写，因此这类沙箱也只能支持单实例模式，qiankun 在实现上将其命名为 LegacySandbox。&lt;/p&gt;

 &lt;p&gt;我们先假设我们的沙箱实现上包含这几个变量（此处以 qiankun 实现为例）：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;/** 沙箱期间新增的全局变量 */
private addedPropsMapInSandbox = new Map&amp;lt;PropertyKey, any&amp;gt;();

/** 沙箱期间更新的全局变量 */
private modifiedPropsOriginalValueMapInSandbox = new Map&amp;lt;PropertyKey, any&amp;gt;();

/** 持续记录更新的(新增和修改的)全局变量的 map，用于在任意时刻做 snapshot */
private currentUpdatedPropsValueMap = new Map&amp;lt;PropertyKey, any&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;这类沙箱的激活与卸载思路可以通过如下两个函数代码解释。首先是激活函数，当沙箱被激活时，我们通过曾经记录好的更新过的全局变量（也可以称为快照）来还原子应用所需要的沙箱环境（即上下文）：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;active() {
  if (!this.sandboxRunning) {
    this.currentUpdatedPropsValueMap.forEach(
       (v, p) =&amp;gt; this.setWindowProp(p, v)
    );
  }

  this.sandboxRunning = true;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;等到需要卸载时，沙箱需要做两件事，一是将子应用运行时修改过的全局变量还原，另一个是删除子应用运行时新增的全局变量：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;inactive() {
  this.modifiedPropsOriginalValueMapInSandbox.forEach(
    (v, p) =&amp;gt; this.setWindowProp(p, v)
  );
  
  this.addedPropsMapInSandbox.forEach(
    (_, p) =&amp;gt; this.setWindowProp(p, undefined, true)
  );

  this.sandboxRunning = false;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;  &lt;em&gt;注：详尽代码可以参考 qiankun 实现    &lt;a href="https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/legacy/sandbox.ts#L51-L73"&gt;https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/legacy/sandbox.ts#L51-L73&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;如上所述，LegacySandbox 的思路在于虽然建立了沙箱代理，但在子应用运行过程中，所有的赋值仍旧会直接操作 window 对象，代理所做的事情就是记录变化（形成快照）；而针对激活和卸载，沙箱会在激活时还原子应用的状态，而卸载时还原主应用的状态，以此达到沙箱隔离的目的。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;LegacySandbox 由于会修改 window 对象，在多个实例运行时肯定会存在冲突，因此，该沙箱模式只能在单实例场景下使用，而当我们需要同时起多个实例时，ProxySandbox 便登场了。&lt;/p&gt;

 &lt;p&gt;ProxySandbox 的方案是同时用 Proxy 给子应用运行环境做了 get 与 set 拦截。沙箱在初始构造时建立一个状态池，当应用操作 window 时，赋值通过 set 拦截器将变量写入状态池，而取值也是从状态池中优先寻找对应属性。由于状态池与子应用绑定，那么运行多个子应用，便可以产生多个相互独立的沙箱环境。&lt;/p&gt;

 &lt;p&gt;由于取值赋值均在建立的状态池上操作，因此，在第一种沙箱环境下激活和卸载需要做的工作，这里也就不需要了。关于状态池的设计，可以参考代码   &lt;a href="https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/proxySandbox.ts#L81"&gt;https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/proxySandbox.ts#L81&lt;/a&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-4.jpeg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;基于属性 diff 的沙箱机制&lt;/h2&gt;

 &lt;p&gt;由于 Proxy 为 ES6 引入的 API，在不支持 ES6 的环境下，我们可以通过一类原始的方式来实现所要的沙箱，即利用普通对象针对 window 属性值构建快照，用于环境的存储与恢复，并在应用卸载时对 window 对象修改做 diff 用于子应用环境的更新保存。在 qiankun 中也有该降级方案，被称为 SnapshotSandbox。当然，这类沙箱同样也不能支持多实例运行，原因也相同。&lt;/p&gt;

 &lt;p&gt;这类方案的主要思路与 LegacySandbox 有些类似，同样主要分为激活与卸载两个部分的操作。&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;// iter 为一个遍历对象属性的方法

active() {
  // 记录当前快照
  this.windowSnapshot = {} as Window;
  iter(window, (prop) =&amp;gt; {
    this.windowSnapshot[prop] = window[prop];
  });

  // 恢复之前的变更
  Object.keys(this.modifyPropsMap).forEach((p: any) =&amp;gt; {
    window[p] = this.modifyPropsMap[p];
  });

  this.sandboxRunning = true;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;在激活时首先将 window 属性遍历存储起来（作为还原 window 所需的快照），然后在 window 上恢复子应用所需的属性变更，是的，直接修改 window 对象。&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;inactive() {
  this.modifyPropsMap = {};

  iter(window, (prop) =&amp;gt; {
    if (window[prop] !== this.windowSnapshot[prop]) {
      // 记录变更，恢复环境
      this.modifyPropsMap[prop] = window[prop];
      window[prop] = this.windowSnapshot[prop];
    }
  });

  this.sandboxRunning = false;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;而等到卸载时，将此时 window 上所包含的属性遍历存储起来（作为以后还原子应用所需的快照），然后从先前保存的 window 对象中将环境恢复。&lt;/p&gt;

 &lt;p&gt;由于未使用到 Proxy，且只利用 Object 的操作来实现，这个沙箱机制是三类机制中最简单的一种。&lt;/p&gt;

 &lt;p&gt;  &lt;em&gt;注：SnapshotSandbox 参考代码    &lt;a href="https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/snapshotSandbox.ts"&gt;https://github.com/umijs/qiankun/blob/dbbc9acdb0733b3ab28e0470c969d65b57653ff0/src/sandbox/snapshotSandbox.ts&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

 &lt;h2&gt;基于 iframe 的沙箱机制&lt;/h2&gt;

 &lt;h3&gt;基于 Proxy 及 diff 的沙箱机制边界考虑&lt;/h3&gt;

 &lt;p&gt;不论是基于 Proxy 还是 diff，其沙箱机制的方案都是通过模拟和代理来实现一个环境隔离的沙箱，只是所有 API 不同。由于是模拟，因此不可避免的在使用中需要考虑一些边界 case，我们简单来看两个问题。首先看一段代码：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;var foo = &amp;quot;hello&amp;quot;;

function foo() {}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;如上代码大家都很熟悉，在无沙箱环境下两种写法可以自动提升为   &lt;code&gt;[window.foo](http://window.foo)&lt;/code&gt;，但是 Proxy 沙箱下这类代码就需要注意，由于代码执行作用域发生了变更，所以生效的环境不再是全局 window，这时通过 proxy 的 get 拦截器大概率就会返回 undefined，于是便会产生疑问“我本地运行是有值的，为什么到微前端里就 undefined 了呢？”，对于后者，诸如 qiankun 框架中可以通过 window.proxy 获取对应上下文来取值达到目的，但前者由于限制，必须显式的定义为 window.foo 否则无法获取。&lt;/p&gt;

 &lt;p&gt;对于不了解微前端框架的同学来说，这无疑会增加了解成本。对于同类问题，我们再看一个问题描述：&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;p&gt;我的子应用新建了一个 iframe 来做些 JavaScript 逻辑，但在里面通过 window.parent.xxx 无法获取子应用 window 上的全局变量？
但这个变量实际上是存在的，我在子应用中可以把它打印出来的。&lt;/p&gt;

&lt;/blockquote&gt;

 &lt;p&gt;造成这个问题的原因类似，由于 iframe 中的 JavaScript 不在沙箱里执行，会读到外面真实的 window 上。而当你在子应用中定义了一个全局变量，方法是在沙箱里面拦截定义的，也就是方法实现写在沙箱里、方法调用读在沙箱外。解决方法有两种：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;把变量做白名单处理，强制写在外面真实的 window 上；&lt;/li&gt;
    &lt;li&gt;在 iframe 中用 window.parent.proxy 来获取对应的变量；&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;以上所述的问题源自模拟，既然是模拟那么就可能存在难以抹平的边界情况，那么有没有更好一些的解决方案呢，iframe 虽然有那么多缺点，但他就是浏览器原生提供的一个隔离环境呢，有可能吗？&lt;/p&gt;

 &lt;p&gt;常规思路下，大家想到的 iframe 都是在页面内起一个 iframe 元素，然后将需要加载的 url 填入进行加载，由于体验上的割裂，这种方式并不为大家认可，这也是为什么基于 Proxy 和 diff 的沙箱机制被提出的原因。&lt;/p&gt;

 &lt;p&gt;让我们再想想，iframe 都有什么优点？&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;使用简单，一个 url 即可，不需要其他微前端方案那样手动写入很多钩子以适配在微前端环境中的运行；&lt;/li&gt;
    &lt;li&gt;利用浏览器的设计，可以实现样式、DOM、JavaScript 代码执行的完美隔离；&lt;/li&gt;
    &lt;li&gt;页面原则上可以起无数多个 iframe 标签来加载应用，所以可以实现多应用共存；&lt;/li&gt;
    &lt;li&gt;通过 iframe 实现的沙箱可以绕过 eval 执行的限制，比如当我们的代码中使用了原生 es modules 的写法时（eval 中不支持    &lt;code&gt;import()&lt;/code&gt;），如果不做转译，代码便会抛出异常；&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;基于这个思路，如果我们不用 iframe 来加载应用，而是只将其作为一个 JavaScript 运行环境，问题是不是就解决了？&lt;/p&gt;

 &lt;h3&gt;利用 iframe 实现沙箱机制的几点思考&lt;/h3&gt;

 &lt;p&gt;我们知道，iframe 标签可以创造一个独立的浏览器级别的运行环境，该环境与主环境隔离，并有自己的 window 上下文；在通信机制上，也可以利用 postMessage 等 API 与宿主环境进行通信。具体来说，在执行 JavaScript 代码上，我们不需要做什么处理，但是要让 iframe 成为符合我们要求的沙箱，还需要重新设计。其中，和沙箱机制有关的几点包含：&lt;/p&gt;

 &lt;ul&gt;
    &lt;li&gt;   &lt;strong&gt;应用间运行时隔离；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;应用间通信；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;路由劫持；&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;我们一一来看看。  &lt;strong&gt;首先，是对运行环境的代理与隔离&lt;/strong&gt;，这也是大多数沙箱必备的基础之一。由于利用了 iframe，所以我们几乎不用担心 JavaScript 的代码运行会给沙箱外环境带来什么影响，因为在 iframe 中运行的 JavaScript 代码都是直接操作 iframe 的 window 上下文，但这里却需要考虑另一方面：如何将一些必要的操作传递出沙箱，因此也需要用到 Proxy 来做一些共享，比如路由、DOM操作等，这涉及到 location、history 等对象。通过将主应用环境下的对象透传给 iframe 中 JavaScript 使用，可以保证子应用在执行操作时，返回前进等操作可以同步到浏览器 top level 层面。此外，对于动态执行的 JavaScript 脚本（比如动态增加一个 script 元素），也需要单独考虑限制作用域，以使 script 中代码在执行时可以对应上具体的全局环境，这里可以通过为 script 包裹一层以锁定作用域内的部分全局变量取值：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const scriptInstance = document.createElement(&amp;apos;script&amp;apos;);
const script = `(function(window, self, document, location, history) {
    ${scriptString}\n
  }).bind(window.proxyWindow)(
    window.proxyWindow,
    window.proxyWindow,
    window.proxyShadowDom,
    window.proxyLocation,
    window.proxyHistory,
  );`;

scriptInstance.text = script;
document.head.appendChild(scriptInstance);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;其他方面，由于上文已经提到过关于 Proxy 对 get/set 拦截器的实现，本部分不再赘述。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;刚刚提到的 DOM 操作，我们在这里多做一些介绍。&lt;/strong&gt;当 JavaScript 操作 DOM 时，我们肯定需要让其中的操作透传到 iframe 外部进行实现，因为 iframe 里面我们不构建 DOM。如果想在隔离方案上一步到位，这里可以使用 Shadow DOM 作为样式隔离的方案，来构建子应用渲染所需的 DOM 结构，而回到 DOM 操作本身，依旧是通过 Proxy 对 iframe document 进行拦截和替换来实现的，这里依据你的样式隔离方案，来决定 document 究竟是指向主应用中的 Shadow DOM Root 节点，还是其他代理的 document 对象。此外，诸如 MutationObserver 这类的操作也需要通过代理保证在主应用上进行。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;其次，再说说通信。&lt;/strong&gt;一个完备的微前端方案需要考虑主子应用间的通信（与沙箱的通信），这样才可以对框架内的的全局状态或者子应用状态进行感知与响应，我们从同域 iframe 环境看起。&lt;/p&gt;

 &lt;p&gt;通过如下代码我们可以构建一个同域的 iframe 元素，此时，iframe 内外通信并不会存在障碍，通过各自 window 便能方便的获取对应属性值；因为是同域环境，从中取出对应的  &lt;code&gt;contentWindow&lt;/code&gt;便可以对 iframe 内容属性进行随意读取，而与此同时还与外部环境隔离。&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const iframe = document.createElement(&amp;apos;iframe&amp;apos;,{url:&amp;apos;about:blank&amp;apos;});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;而如果要单独构建通信机制，也可以利用自定义 props、event 等方式实现，或者通过 Web API 诸如 postMessage 或者 BroadcastChannel 来实现，关于此部分我在曾经的一篇文章中稍有提及，感兴趣的话可以查看《  &lt;strong&gt;   &lt;a href="https://hijiangtao.github.io/2021/04/13/Service-Worker-Practical-Notes/"&gt;Service Worker 实践指南&lt;/a&gt;&lt;/strong&gt;》。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;说回路由状态&lt;/strong&gt;，要保证 JavaScript 沙箱环境内与主应用路由状态保持一致，我们有两种实现方案：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;让 JavaScript 沙箱内路由变更操作在主应用环境生效；&lt;/li&gt;
    &lt;li&gt;同步沙箱内路由变化至主应用环境；&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;其中，针对第一种情况，我们需要做的是将诸如 location 、history 等变量代理到沙箱环境中，在这种情况下，因为我们不关心 iframe 自身的路由变化，便可以自由设置 src 属性，比如   &lt;code&gt;about:blank&lt;/code&gt; 的方式来构建 iframe，而在沙箱实现上我们可以通过前述的 Proxy 来拦截实现。&lt;/p&gt;

 &lt;p&gt;但稍微考虑下实际生产环境便会发现，第一种情况存在的限制较多，最基本的便是对沙箱内网络请求发送的处理，所以这就需要我们考虑第二种情况的实现，在这种操作下，我们的路由变化会同步到 iframe 上下文，所以我们需要针对 iframe 路由添加一个监听器，在监听到变化时处理主应用的路由，以实现两者路由同步。当然，这种情况下，我们需要针对主应用所在域名设计一个 iframe 的同域方案，比如同一域名+自定义 path 或者 hash 的实现就很简单易懂，这样也不存在跨域限制，此处不再展开。&lt;/p&gt;

 &lt;h3&gt;一段 iframe 沙箱的示例代码&lt;/h3&gt;

 &lt;p&gt;以下简单写了一个 iframe 沙箱的实现伪代码，核心依旧在 window 隔离与共享对象的处理上，主要的实现手段依旧是完善 Proxy 的 get/set 拦截器：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;class SandboxWindow {
    constructor(context, frameWindow) {
        return new Proxy(frameWindow, {
            get(target, name) {
                if (name in context) {
                    return context[name];
                } else if(typeof target[name] === &amp;apos;function&amp;apos; &amp;amp;&amp;amp; /^[a-z]/.test(name) ){
                    return target[name].bind &amp;amp;&amp;amp; target[name].bind(target);
                } else {
                    return target[name];
                }
            },
            set(target, name, value) {
                if (name in context) {
                    return context[name] = value;
                }
                target[name] = value;
            }
        })
    }
}

// 需要全局共享的变量
const context = { 
    document: window.document, 
    history: window.history, 
    location: window.location,
}

// 创建 iframe
const userInputUrl = &amp;apos;&amp;apos;;
const iframe = document.createElement(&amp;apos;iframe&amp;apos;,{url: userInputUrl});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

// 创建沙箱
const newSandboxWindow = new SandboxWindow(context, sandboxGlobal); 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;但需要注意的是，iframe 方案下，JavaScript 沙箱只是其中一部分，还需要通过完备的 HTML/JavaScript 代码拆分等方案辅助达到微前端环境的目的，这部分实现可参考 kuitos 的开源库   &lt;a href="https://github.com/kuitos/import-html-entry"&gt;import-html-entry&lt;/a&gt;；同样的，之前的几类沙箱方案也需要考虑与这些方案组合。&lt;/p&gt;

 &lt;p&gt;  &lt;em&gt;注：在实现上，如果需要区分 iframe 与主应用环境，可以通过代码    &lt;code&gt;window.parent !== window&lt;/code&gt; 进行判断。&lt;/em&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-6.png"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;各类沙箱机制对比&lt;/h2&gt;

 &lt;p&gt;通过对比 Proxy 的两类实现、属性 diff 的一种实现以及 iframe 实现方案，可以发现几类沙箱的主要特点在于（以下部分方案用 qiankun 中对三类沙箱的命名方式作为沙箱机制名称）&lt;/p&gt;

 &lt;table&gt;
  
      &lt;tr&gt;
         &lt;th&gt; &lt;/th&gt;
         &lt;th&gt;多实例运行&lt;/th&gt;
         &lt;th&gt;语法兼容&lt;/th&gt;
         &lt;th&gt;不污染全局环境（主应用）&lt;/th&gt;
    &lt;/tr&gt;
  
  
      &lt;tr&gt;
         &lt;td&gt;LegacySanbox&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
    &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td&gt;ProxySandbox&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
    &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td&gt;SnapshotSandbox&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
         &lt;td&gt;❌&lt;/td&gt;
    &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td&gt;iframe&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
         &lt;td&gt;✅&lt;/td&gt;
    &lt;/tr&gt;
  
&lt;/table&gt;

 &lt;h2&gt;基于 ES 提案   &lt;strong&gt;ShadowRealm 实现&lt;/strong&gt;&lt;/h2&gt;

 &lt;p&gt;  &lt;strong&gt;ShadowRealm 是一个 ECMAScript 标准提案，旨在创建一个独立的全局环境，它的全局对象包含自己的内建函数与对象（未绑定到全局变量的标准对象，如 Object.prototype 的初始值），有自己独立的作用域&lt;/strong&gt;，方案当前处于 stage 3 阶段。提案地址   &lt;a href="https://github.com/tc39/proposal-shadowrealm"&gt;https://github.com/tc39/proposal-shadowrealm&lt;/a&gt;&lt;/p&gt;

 &lt;h3&gt;什么是 JavaScript 的运行环境实例&lt;/h3&gt;

 &lt;p&gt;谈及提案之前，我们简单来看看什么是 Realm，下面是 Alex 附上的一个例子：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;iframe&amp;gt;
  &amp;lt;/iframe&amp;gt;
  &amp;lt;script&amp;gt;
    const win = frames[0].window;
    console.assert(win.globalThis !== globalThis); // (A)
    console.assert(win.Array !== Array); // (B)
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;在前面 iframe 沙箱机制中我们也有介绍，由于每个   &lt;code&gt;iframe&lt;/code&gt; 都有一个独立的运行环境，于是在执行时，当前 html 中的全局对象肯定与   &lt;code&gt;iframe&lt;/code&gt;的全局对象不相同（A），类似的，全局对象上的   &lt;code&gt;Array&lt;/code&gt;与   &lt;code&gt;iframe&lt;/code&gt; 中获取到的   &lt;code&gt;Array&lt;/code&gt; 也不同（B）。&lt;/p&gt;

 &lt;p&gt;这就是 realm，一个 JavaScript 运行环境（JavaScript platform）实例：包含其所必须的全局环境及内建函数等。&lt;/p&gt;

 &lt;h3&gt;ShadowRealm API 简介&lt;/h3&gt;

 &lt;p&gt;ShadowRealm API 由一个包含如下函数签名的类实现：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;declare class ShadowRealm {
  constructor();
  evaluate(sourceText: string): PrimitiveValueOrCallable;
  importValue(specifier: string, bindingName: string): Promise&amp;lt;PrimitiveValueOrCallable&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;  &lt;strong&gt;每个    &lt;code&gt;ShadowRealm&lt;/code&gt; 实例都有自己独立的运行环境实例，在 realm 中，提案提供了两种方法让我们来执行运行环境实例中的 JavaScript 代码：&lt;/strong&gt;&lt;/p&gt;

 &lt;ul&gt;
    &lt;li&gt;   &lt;code&gt;.evaluate()&lt;/code&gt;：同步执行代码字符串，类似    &lt;code&gt;eval()&lt;/code&gt;。&lt;/li&gt;
    &lt;li&gt;   &lt;code&gt;.importValue()&lt;/code&gt;：返回一个    &lt;code&gt;Promise&lt;/code&gt; 对象，异步执行代码字符串。&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;通过 evaluate 执行代码与 eval 类似，比如：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;const sr = new ShadowRealm();
console.assert(
  sr.evaluate(`&amp;apos;ab&amp;apos; + &amp;apos;cd&amp;apos;`) === &amp;apos;abcd&amp;apos;
);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;但存在一些细微的差别，比如执行作用域、调用方式以及传值类型等。例如，如果   &lt;code&gt;.evaluate()&lt;/code&gt; 返回一个函数，则该函数会被包装，这样我们就可以从外部调用它，而逻辑在 ShadowRealm 中运行，我们可以通过观察下面的 console.assert 来效果：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;globalThis.realm = &amp;apos;incubator realm&amp;apos;;

const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = &amp;apos;child realm&amp;apos;`);

const wrappedFunc = sr.evaluate(`() =&amp;gt; globalThis.realm`);
console.assert(wrappedFunc() === &amp;apos;child realm&amp;apos;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;说到另一个 API   &lt;code&gt;.importValue()&lt;/code&gt;，我们可以利用它导入一个外部模块，它会通过一个 Promise 异步返回其执行内容，和   &lt;code&gt;.evalute()&lt;/code&gt;函数一样，这个函数被包装，以允许我们在外部调用，而实际代码在 ShadowRealm 中执行，我们可以看看下面这个例子，很好的解释了这个 API 的功能：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;// main.js
const sr = new ShadowRealm();
const wrappedSum = await sr.importValue(&amp;apos;./my-module.js&amp;apos;, &amp;apos;sum&amp;apos;);
console.assert(wrappedSum(&amp;apos;hi&amp;apos;, &amp;apos; &amp;apos;, &amp;apos;folks&amp;apos;, &amp;apos;!&amp;apos;) === &amp;apos;hi folks!&amp;apos;);

// my-module.js
export function sum(...values) {
  return values.reduce((prev, value) =&amp;gt; prev + value);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;h3&gt;ShadowRealm 的错误捕获与更多应用场景&lt;/h3&gt;

 &lt;p&gt;ShadowRealm API 提案暂未针对错误捕获做详细设计，整体看上去比较简洁，因为这些在未来还有可能变化，以下为 Alex 针对当前提案下代码执行错误给出的两个例子，可以看出其中并不包含错误的原始调用堆栈等：&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;&amp;gt; new ShadowRealm().evaluate(&amp;apos;someFunc(&amp;apos;)
SyntaxError: Unexpected end of script

&amp;gt; new ShadowRealm().evaluate(`throw new RangeError(&amp;apos;The message&amp;apos;)`)
TypeError: Error encountered during evaluation
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;由于没有实践经历，这里仅对 ShadowRealm 提案及相关概念进行了简要介绍，但可以看出，这个提案的落地可能对于一个更完美的 JavaScript 沙箱设计有所帮助，当然，这个提案的应用场景远不止此，比如：&lt;/p&gt;

 &lt;ul&gt;
    &lt;li&gt;   &lt;strong&gt;Web 应用诸如     &lt;code&gt;IDE&lt;/code&gt; 或绘图等程序可以运行第三方代码，允许其以插件或者配置的方式引入；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;利用     &lt;code&gt;ShadowRealms&lt;/code&gt; 建立一个可编程环境，来运行用户的代码；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;服务器可以在     &lt;code&gt;ShadowRealms&lt;/code&gt; 中运行第三方代码；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;在 ShadowRealms 中可以运行测试运行器（Test Runner），这样外部的 JS 执行环境不会受到影响，并且每个套件都可以在新环境中启动（这有助于提高可复用性），这种场景类似于微前端的 JavaScript 沙箱；&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;strong&gt;网页抓取和网页应用测试等；&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-5.png"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;总结&lt;/h2&gt;

 &lt;p&gt;如果按照沙箱机制在实现时所用到的主要 Web 技术不同，当下已经论证、开源或者存在实现可能性的 JavaScript 沙箱机制可以分为以下几类：&lt;/p&gt;

 &lt;ol&gt;
    &lt;li&gt;基于 ES6 API Proxy 实现&lt;/li&gt;
    &lt;li&gt;基于属性 diff 实现&lt;/li&gt;
    &lt;li&gt;基于 iframe 实现&lt;/li&gt;
    &lt;li&gt;基于 ES 提案 ShadowRealm 实现&lt;/li&gt;
&lt;/ol&gt;

 &lt;p&gt;本文基于个人项目实践、阅读代码梳理等方式对每类沙箱机制均进行了介绍，部分引用了 qiankun 的代码实现，部分写了伪代码解释，部分引用了最新 ECMAScript 提案示例，但仍未能详尽每一处细节，比如没有针对微前端框架深入介绍，也不会就某一个沙箱机制的具体细节实现（比如如何构建闭包环境、属性读取的边界处理等）进行剖析，但这些对于从更大的层面了解微前端机制都不可或缺。&lt;/p&gt;

 &lt;p&gt;如果你想了解关于 CSS 样式隔离的内容可以搜索 Shadow DOM 相关内容进一步查阅；如果你想了解微前端的主子应用加载、运行机制，可以参考 single-spa 文档、qiankun 文档、ShadowRealm 提案等内容；如果你想了解文中涉及的一些概念与 API 用法可以在 MDN 进行搜索查阅，大部分均有对应介绍。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://hijiangtao.github.io/assets/in-post/2022-06-11-JavaScript-Sandbox-Mechanism-and-Its-History-2.jpeg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;参考&lt;/h2&gt;

 &lt;ol&gt;
    &lt;li&gt;   &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy"&gt;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://single-spa.js.org/"&gt;https://single-spa.js.org/&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://github.com/umijs/qiankun"&gt;https://github.com/umijs/qiankun&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://tsejx.github.io/javascript-guidebook/standard-built-in-objects/fundamental-objects/symbol/unscopables/"&gt;https://tsejx.github.io/javascript-guidebook/standard-built-in-objects/fundamental-objects/symbol/unscopables/&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables"&gt;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://juejin.cn/post/6981374562877308936"&gt;https://juejin.cn/post/6981374562877308936&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://micro-frontends.org/"&gt;https://micro-frontends.org/&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://2ality.com/2022/04/shadow-realms.html"&gt;https://2ality.com/2022/04/shadow-realms.html&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://github.com/tc39/proposal-shadowrealm"&gt;https://github.com/tc39/proposal-shadowrealm&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;   &lt;a href="https://qiankun.umijs.org/guide"&gt;https://qiankun.umijs.org/guide&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Document 微前端 隔离 沙箱机制 JavaScript 运行环境</category>
      <guid isPermaLink="true">https://itindex.net/detail/62300-%E5%89%8D%E7%AB%AF-javascript-%E9%9A%94%E7%A6%BB</guid>
      <pubDate>Sat, 11 Jun 2022 08:00:00 CST</pubDate>
    </item>
    <item>
      <title>一份简单够用的 Nginx Location 配置讲解</title>
      <link>https://itindex.net/detail/62007-nginx-location</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt; &lt;p&gt;Location 是 Nginx 中一个非常核心的配置，这篇重点讲解一下 Location 的配置问题以及一些注意事项。&lt;/p&gt; &lt;h2&gt;语法&lt;/h2&gt; &lt;p&gt;关于 Location，举个简单的配置例子：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;http { 
  server {
      listen 80;
        server_name www.yayujs.com;
        location / {
          root /home/www/ts/;
          index index.html;
        }
  }
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;大致的意思是，当你访问   &lt;code&gt;www.yayujs.com&lt;/code&gt; 的   &lt;code&gt;80&lt;/code&gt; 端口的时候，返回   &lt;code&gt;/home/www/ts/index.html&lt;/code&gt; 文件。&lt;/p&gt; &lt;p&gt;我们看下 Location 的具体语法：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location [ = | ~ | ~* | ^~ ] uri { ... }&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;重点看方括号中的   &lt;code&gt;[ = | ~ | ~* | ^~ ]&lt;/code&gt;，其中   &lt;code&gt;|&lt;/code&gt; 分隔的内容表示你可能会用到的语法，其中：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;=&lt;/code&gt; 表示精确匹配，比如：&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;location = /test {
  return 200 &amp;quot;hello&amp;quot;;
}

# /test ok
# /test/ not ok
# /test2 not ok
# /test/2 not ok&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;~&lt;/code&gt; 表示区分大小写的正则匹配，比如：&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;location ~ ^/test$ {
  [ configuration ] 
}

# /test ok
# /Test not ok
# /test/ not ok
# /test2 not ok&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;~*&lt;/code&gt; 表示不区分大小写的正则匹配&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;location ~* ^/test$ {     
    [ configuration ] 
}

# /test ok
# /Test ok
# /test/ not ok
# /test2 not ok&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;^~&lt;/code&gt; 表示 uri 以某个字符串开头&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;location ^~ /images/ {    
    [ configuration ] 
}

# /images/1.gif ok&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;而当你不使用这些语法的时候，只写 uri 的时候：&lt;/p&gt; &lt;p&gt;  &lt;code&gt;/&lt;/code&gt; 表示通用匹配：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location / {     
    [ configuration ] 
}

# /index.html ok&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;location /test {
    [ configuration ] 
}

# /test ok
# /test2 ok
# /test/ ok&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;匹配顺序&lt;/h2&gt; &lt;p&gt;当存在多个 location 的时候，他们的匹配顺序引用   &lt;a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#location" rel="nofollow noreferrer"&gt;Nginx 官方文档&lt;/a&gt;就是：&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;A location can either be defined by a prefix string, or by a regular expression. Regular expressions are specified with the preceding “~*” modifier (for case-insensitive matching), or the “~” modifier (for case-sensitive matching). To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.&lt;/p&gt;  &lt;p&gt;If the longest matching prefix location has the “^~” modifier then regular expressions are not checked.&lt;/p&gt;  &lt;p&gt;Also, using the “=” modifier it is possible to define an exact match of URI and location. If an exact match is found, the search terminates. For example, if a “/” request happens frequently, defining “location = /” will speed up the processing of these requests, as search terminates right after the first comparison. Such a location cannot obviously contain nested locations.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;翻译整理后就是：&lt;/p&gt; &lt;p&gt;location 的定义分为两种：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;前缀字符串（prefix string）&lt;/li&gt;  &lt;li&gt;正则表达式（regular expression），具体为前面带    &lt;code&gt;~*&lt;/code&gt; 和    &lt;code&gt;~&lt;/code&gt; 修饰符的&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;而匹配 location 的顺序为：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;检查使用前缀字符串的 locations，在使用前缀字符串的 locations 中选择最长匹配的，并将结果进行储存&lt;/li&gt;  &lt;li&gt;如果符合带有    &lt;code&gt;=&lt;/code&gt; 修饰符的 URI，则立刻停止匹配&lt;/li&gt;  &lt;li&gt;如果符合带有     &lt;code&gt;^~&lt;/code&gt; 修饰符的 URI，则也立刻停止匹配。&lt;/li&gt;  &lt;li&gt;然后按照定义文件的顺序，检查正则表达式，匹配到就停止&lt;/li&gt;  &lt;li&gt;当正则表达式匹配不到的时候，使用之前储存的前缀字符串&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;再总结一下就是：&lt;/p&gt; &lt;p&gt;在顺序上，前缀字符串顺序不重要，按照匹配长度来确定，正则表达式则按照定义顺序。&lt;/p&gt; &lt;p&gt;在优先级上，  &lt;code&gt;=&lt;/code&gt; 修饰符最高，  &lt;code&gt;^~&lt;/code&gt; 次之，再者是正则，最后是前缀字符串匹配。&lt;/p&gt; &lt;p&gt;我们举几个简单的例子复习下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;server {
    location /doc {
        [ configuration A ] 
    }
    location /docu {
        [ configuration B ] 
    }
}

# 请求 /document 使用 configuration B
# 虽然 /doc 也能匹配到，但在顺序上，前缀字符串顺序不重要，按照匹配长度来确定&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;server {
    location ~ ^/doc {
        [ configuration A ] 
    }
    location ~ ^/docu {
        [ configuration B ] 
    }
}

# 请求 /document 使用 configuration A
# 虽然 ~ ^/docu 也能匹配到，但正则表达式则按照定义顺序&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;server {
    location ^~ /doc {
        [ configuration A ] 
    }
    location ~ ^/docu {
        [ configuration B ] 
    }
}

# 请求 /document 使用 configuration A
# 虽然 ~ ^/docu 也能匹配到，但 ^~ 的优先级更高&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;server {
    location /document {
        [ configuration A ] 
    }
    location ~ ^/docu {
        [ configuration B ] 
    }
}

# 请求 /document 使用 configuration B
# 虽然 /document 也能匹配到，但正则的优先级更高&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;root 与 alias 的区别&lt;/h2&gt; &lt;p&gt;当我们这样设置   &lt;code&gt;root&lt;/code&gt; 的时候：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location /i/ {
    root /data/w3;
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当请求   &lt;code&gt;/i/top.gif&lt;/code&gt; ，  &lt;code&gt;/data/w3/i/top.gif&lt;/code&gt; 会被返回。&lt;/p&gt; &lt;p&gt;当我们这样设置   &lt;code&gt;alias&lt;/code&gt; 的时候：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location /i/ {
    alias /data/w3/images/;
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当请求   &lt;code&gt;/i/top.gif&lt;/code&gt; ，  &lt;code&gt;/data/w3/images/top.gif&lt;/code&gt; 会被返回。&lt;/p&gt; &lt;p&gt;乍一看两者很像，但细一看，就能看出两者的区别，root 是直接拼接   &lt;code&gt;root&lt;/code&gt; +   &lt;code&gt;location&lt;/code&gt; 而 alias 是用   &lt;code&gt;alias&lt;/code&gt; 替换   &lt;code&gt;location&lt;/code&gt;，所以 root 中最后的路径里有   &lt;code&gt;/i/&lt;/code&gt;，而 alias 中最后的路径里没有    &lt;code&gt;/i/&lt;/code&gt; 。&lt;/p&gt; &lt;p&gt;所以如果你这样使用 allias 定义一个路径：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location /images/ {
    alias /data/w3/images/;
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;其实使用 root 会更好：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;location /images/ {
    root /data/w3;
}&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;server 和 location 中的 root&lt;/h2&gt; &lt;p&gt;server 和 location 中都可以使用 root，举个例子：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;http { 
  server {
      listen 80;
        server_name www.yayujs.com;
        root /home/www/website/;
        location / {
          root /home/www/ts/;
          index index.html;
        }
  }
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果两者都出现，是怎样的优先级呢？&lt;/p&gt; &lt;p&gt;简单的来说，就是就近原则，如果 location 中能匹配到，就是用 location 中的 root 配置，忽略 server 中的 root，当 location 中匹配不到的时候，则使用 server 中的 root 配置。&lt;/p&gt; &lt;h2&gt;系列文章&lt;/h2&gt; &lt;p&gt;博客搭建系列是我至今写的唯一一个偏实战的系列教程，讲解如何使用 VuePress 搭建博客，并部署到 GitHub、Gitee、个人服务器等平台。&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;   &lt;a href="https://github.com/mqyqingfeng/Blog/issues/235" rel="nofollow noreferrer"&gt;一篇带你用 VuePress + GitHub Pages 搭建博客&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/mqyqingfeng/Blog/issues/236" rel="nofollow noreferrer"&gt;一篇教你代码同步 GitHub 和 Gitee&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/mqyqingfeng/Blog/issues/237" rel="nofollow noreferrer"&gt;还不会用 GitHub Actions ？看看这篇&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/mqyqingfeng/Blog/issues/238" rel="nofollow noreferrer"&gt;Gitee 如何自动部署 Pages？还是用 GitHub Actions!&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/mqyqingfeng/Blog/issues/239" rel="nofollow noreferrer"&gt;一份前端够用的 Linux 命令&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;微信：「mqyqingfeng」，加我进冴羽唯一的读者群。&lt;/p&gt; &lt;p&gt;如果有错误或者不严谨的地方，请务必给予指正，十分感谢。如果喜欢或者 有所启发，欢迎 star，对作者也是一种鼓励。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript 前端 nginx 后端 博客搭建</category>
      <guid isPermaLink="true">https://itindex.net/detail/62007-nginx-location</guid>
      <pubDate>Mon, 03 Jan 2022 20:24:58 CST</pubDate>
    </item>
    <item>
      <title>写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践</title>
      <link>https://itindex.net/detail/61610-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E7%AD%96%E7%95%A5</link>
      <description>&lt;h3&gt;前言&lt;/h3&gt; &lt;p&gt;笔者近半年一直在参与项目重构，在重构过程中大量应用  &lt;strong&gt;性能优化&lt;/strong&gt;和  &lt;strong&gt;设计模式&lt;/strong&gt;两方面的知识。  &lt;strong&gt;性能优化&lt;/strong&gt;和  &lt;strong&gt;设计模式&lt;/strong&gt;两方面的知识不管在工作还是面试时都是高频应用场景，趁着这次参与大规模项目重构的机会，笔者认真梳理出一些常规且必用的  &lt;code&gt;性能优化建议&lt;/code&gt;，同时结合日常开发经验整理出笔者在网易四年来实践到的认为有用的所有  &lt;code&gt;性能优化建议&lt;/code&gt;，与大家一起分享分享！(由于篇幅有限，那  &lt;code&gt;设计模式&lt;/code&gt;在后面再专门出一篇文章呗)&lt;/p&gt; &lt;p&gt;可能有些  &lt;code&gt;性能优化建议&lt;/code&gt;已被大家熟知，不过也不影响这次分享，当然笔者也将一些平时可能不会注意的细节罗列出来。&lt;/p&gt; &lt;p&gt;平时大家认为  &lt;code&gt;性能优化&lt;/code&gt;是一种无序的应用场景，但在笔者看来它是一种有序的应用场景且很多  &lt;code&gt;性能优化&lt;/code&gt;都是互相铺垫甚至一带一路。从过程趋势来看，  &lt;code&gt;性能优化&lt;/code&gt;可分为  &lt;strong&gt;网络层面&lt;/strong&gt;和  &lt;strong&gt;渲染层面&lt;/strong&gt;；从结果趋势来看，  &lt;code&gt;性能优化&lt;/code&gt;可分为  &lt;strong&gt;时间层面&lt;/strong&gt;和  &lt;strong&gt;体积层面&lt;/strong&gt;。简单来说就是  &lt;code&gt;要在访问网站时使其快准狠地立马呈现在用户眼前&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#24615;&amp;#33021;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343059" title="&amp;#24615;&amp;#33021;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;所有的  &lt;code&gt;性能优化&lt;/code&gt;都围绕着  &lt;code&gt;两大层面两小层面&lt;/code&gt;实现，核心层面是  &lt;code&gt;网络层面&lt;/code&gt;和  &lt;code&gt;渲染层面&lt;/code&gt;，辅助层面是  &lt;code&gt;时间层面&lt;/code&gt;和  &lt;code&gt;体积层面&lt;/code&gt;，而辅助层面则充满在核心层面里。于是笔者通过本文整理出关于前端  &lt;code&gt;性能优化&lt;/code&gt;的  &lt;strong&gt;九大策略&lt;/strong&gt;和  &lt;strong&gt;六大指标&lt;/strong&gt;。当然这些  &lt;code&gt;策略&lt;/code&gt;和  &lt;code&gt;指标&lt;/code&gt;都是笔者自己定义，方便通过某种方式为性能优化做一些规范。&lt;/p&gt; &lt;p&gt;因此在工作或面试时结合这些特征就能完美地诠释  &lt;code&gt;性能优化&lt;/code&gt;所延伸出来的知识了。  &lt;strong&gt;前方高能，不看也得收藏，走起！！！&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;所有代码示例为了凸显主题，只展示核心配置代码，其他配置并未补上，请自行脑补&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;九大策略&lt;/h3&gt; &lt;h4&gt;网络层面&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;网络层面&lt;/strong&gt;的性能优化，无疑是如何让资源  &lt;code&gt;体积更小加载更快&lt;/code&gt;，因此笔者从以下四方面做出建议。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;构建策略&lt;/strong&gt;：基于构建工具(   &lt;code&gt;Webpack/Rollup/Parcel/Esbuild/Vite/Gulp&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;图像策略&lt;/strong&gt;：基于图像类型(   &lt;code&gt;JPG/PNG/SVG/WebP/Base64&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;分发策略&lt;/strong&gt;：基于内容分发网络(   &lt;code&gt;CDN&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;缓存策略&lt;/strong&gt;：基于浏览器缓存(   &lt;code&gt;强缓存/协商缓存&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;上述四方面都是一步接着一步完成，充满在整个项目流程里。  &lt;strong&gt;构建策略&lt;/strong&gt;和  &lt;strong&gt;图像策略&lt;/strong&gt;处于开发阶段，  &lt;strong&gt;分发策略&lt;/strong&gt;和  &lt;strong&gt;缓存策略&lt;/strong&gt;处于生产阶段，因此在每个阶段都可检查是否按顺序接入上述策略。通过这种方式就能最大限度增加  &lt;code&gt;性能优化&lt;/code&gt;应用场景。&lt;/p&gt; &lt;h5&gt;构建策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;webpack&lt;/code&gt;做相关处理，同时也是接入最普遍的  &lt;code&gt;性能优化策略&lt;/code&gt;。其他构建工具的处理也是大同小异，可能只是配置上不一致。说到  &lt;code&gt;webpack&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;，无疑是从  &lt;code&gt;时间层面&lt;/code&gt;和  &lt;code&gt;体积层面&lt;/code&gt;入手。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;笔者发现目前webpack v5整体兼容性还不是特别好，某些功能配合第三方工具可能出现问题，故暂未升级到v5，继续使用v4作为生产工具，故以下配置均基于v4，但总体与v5的配置出入不大&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;笔者对两层面分别做出6个  &lt;code&gt;性能优化建议&lt;/code&gt;总共12个  &lt;code&gt;性能优化建议&lt;/code&gt;，为了方便记忆均使用四字词语概括，方便大家消化。⏱表示  &lt;code&gt;减少打包时间&lt;/code&gt;，表示  &lt;code&gt;减少打包体积&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;减少打包时间&lt;/strong&gt;：   &lt;code&gt;缩减范围&lt;/code&gt;、   &lt;code&gt;缓存副本&lt;/code&gt;、   &lt;code&gt;定向搜索&lt;/code&gt;、   &lt;code&gt;提前构建&lt;/code&gt;、   &lt;code&gt;并行构建&lt;/code&gt;、   &lt;code&gt;可视结构&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;减少打包体积&lt;/strong&gt;：   &lt;code&gt;分割代码&lt;/code&gt;、   &lt;code&gt;摇树优化&lt;/code&gt;、   &lt;code&gt;动态垫片&lt;/code&gt;、   &lt;code&gt;按需加载&lt;/code&gt;、   &lt;code&gt;作用提升&lt;/code&gt;、   &lt;code&gt;压缩资源&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;⏱缩减范围&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置include/exclude缩小Loader对文件的搜索范围&lt;/strong&gt;，好处是  &lt;code&gt;避免不必要的转译&lt;/code&gt;。  &lt;code&gt;node_modules目录&lt;/code&gt;的体积这么大，那得增加多少时间成本去检索所有文件啊？&lt;/p&gt; &lt;p&gt;  &lt;code&gt;include/exclude&lt;/code&gt;通常在各大  &lt;code&gt;Loader&lt;/code&gt;里配置，  &lt;code&gt;src目录&lt;/code&gt;通常作为源码目录，可做如下处理。当然  &lt;code&gt;include/exclude&lt;/code&gt;可根据实际情况修改。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    module: {
        rules: [{
            exclude: /node_modules/,
            include: /src/,
            test: /\.js$/,
            use: &amp;quot;babel-loader&amp;quot;
        }]
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱缓存副本&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置cache缓存Loader对文件的编译副本&lt;/strong&gt;，好处是  &lt;code&gt;再次编译时只编译修改过的文件&lt;/code&gt;。未修改过的文件干嘛要随着修改过的文件重新编译呢？&lt;/p&gt; &lt;p&gt;大部分  &lt;code&gt;Loader/Plugin&lt;/code&gt;都会提供一个可使用编译缓存的选项，通常包含  &lt;code&gt;cache&lt;/code&gt;字眼。以  &lt;code&gt;babel-loader&lt;/code&gt;和  &lt;code&gt;eslint-webpack-plugin&lt;/code&gt;为例。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import EslintPlugin from &amp;quot;eslint-webpack-plugin&amp;quot;;

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /\.js$/,
            use: [{
                loader: &amp;quot;babel-loader&amp;quot;,
                options: { cacheDirectory: true }
            }]
        }]
    },
    plugins: [
        new EslintPlugin({ cache: true })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱定向搜索&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置resolve提高文件的搜索速度&lt;/strong&gt;，好处是  &lt;code&gt;定向指定必须文件路径&lt;/code&gt;。若某些第三方库以常规形式引入可能报错或希望程序自动索引特定类型文件都可通过该方式解决。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;alias&lt;/code&gt;映射模块路径，  &lt;code&gt;extensions&lt;/code&gt;表明文件后缀，  &lt;code&gt;noParse&lt;/code&gt;过滤无依赖文件。通常配置  &lt;code&gt;alias&lt;/code&gt;和  &lt;code&gt;extensions&lt;/code&gt;就足够。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    resolve: {
        alias: {
            &amp;quot;#&amp;quot;: AbsPath(&amp;quot;&amp;quot;), // 根目录快捷方式
            &amp;quot;@&amp;quot;: AbsPath(&amp;quot;src&amp;quot;), // src目录快捷方式
            swiper: &amp;quot;swiper/js/swiper.min.js&amp;quot;
        }, // 模块导入快捷方式
        extensions: [&amp;quot;.js&amp;quot;, &amp;quot;.ts&amp;quot;, &amp;quot;.jsx&amp;quot;, &amp;quot;.tsx&amp;quot;, &amp;quot;.json&amp;quot;, &amp;quot;.vue&amp;quot;] // import路径时文件可省略后缀名
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱提前构建&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置DllPlugin将第三方依赖提前打包&lt;/strong&gt;，好处是  &lt;code&gt;将DLL与业务代码完全分离且每次只构建业务代码&lt;/code&gt;。这是一个古老配置，在  &lt;code&gt;webpack v2&lt;/code&gt;时已存在，不过现在  &lt;code&gt;webpack v4+&lt;/code&gt;已不推荐使用该配置，因为其版本迭代带来的性能提升足以忽略  &lt;code&gt;DllPlugin&lt;/code&gt;所带来的效益。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;DLL&lt;/strong&gt;意为  &lt;code&gt;动态链接库&lt;/code&gt;，指一个包含可由多个程序同时使用的代码库。在前端领域里可认为是另类缓存的存在，它把公共代码打包为DLL文件并存到硬盘里，再次打包时动态链接  &lt;code&gt;DLL文件&lt;/code&gt;就无需再次打包那些公共代码，从而提升构建速度，减少打包时间。&lt;/p&gt; &lt;p&gt;配置  &lt;code&gt;DLL&lt;/code&gt;总体来说相比其他配置复杂，配置流程可大致分为三步。&lt;/p&gt; &lt;p&gt;首先告知构建脚本哪些依赖做成  &lt;code&gt;DLL&lt;/code&gt;并生成  &lt;code&gt;DLL文件&lt;/code&gt;和  &lt;code&gt;DLL映射表文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { DefinePlugin, DllPlugin } from &amp;quot;webpack&amp;quot;;

export default {
    // ...
    entry: {
        vendor: [&amp;quot;react&amp;quot;, &amp;quot;react-dom&amp;quot;, &amp;quot;react-router-dom&amp;quot;]
    },
    mode: &amp;quot;production&amp;quot;,
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    chunks: &amp;quot;all&amp;quot;,
                    name: &amp;quot;vendor&amp;quot;,
                    test: /node_modules/
                }
            }
        }
    },
    output: {
        filename: &amp;quot;[name].dll.js&amp;quot;, // 输出路径和文件名称
        library: &amp;quot;[name]&amp;quot;, // 全局变量名称：其他模块会从此变量上获取里面模块
        path: AbsPath(&amp;quot;dist/static&amp;quot;) // 输出目录路径
    },
    plugins: [
        new DefinePlugin({
            &amp;quot;process.env.NODE_ENV&amp;quot;: JSON.stringify(&amp;quot;development&amp;quot;) // DLL模式下覆盖生产环境成开发环境(启动第三方依赖调试模式)
        }),
        new DllPlugin({
            name: &amp;quot;[name]&amp;quot;, // 全局变量名称：减小搜索范围，与output.library结合使用
            path: AbsPath(&amp;quot;dist/static/[name]-manifest.json&amp;quot;) // 输出目录路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后在  &lt;code&gt;package.json&lt;/code&gt;里配置执行脚本且每次构建前首先执行该脚本打包出  &lt;code&gt;DLL文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;{
    &amp;quot;scripts&amp;quot;: {
        &amp;quot;dll&amp;quot;: &amp;quot;webpack --config webpack.dll.js&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;最后链接  &lt;code&gt;DLL文件&lt;/code&gt;并告知  &lt;code&gt;webpack&lt;/code&gt;可命中的  &lt;code&gt;DLL文件&lt;/code&gt;让其自行读取。使用  &lt;a href="https://github.com/jharris4/html-webpack-tags-plugin" rel="nofollow noreferrer"&gt;html-webpack-tags-plugin&lt;/a&gt;在打包时自动插入  &lt;code&gt;DLL文件&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { DllReferencePlugin } from &amp;quot;webpack&amp;quot;;
import HtmlTagsPlugin from &amp;quot;html-webpack-tags-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        new DllReferencePlugin({
            manifest: AbsPath(&amp;quot;dist/static/vendor-manifest.json&amp;quot;) // manifest文件路径
        }),
        new HtmlTagsPlugin({
            append: false, // 在生成资源后插入
            publicPath: &amp;quot;/&amp;quot;, // 使用公共路径
            tags: [&amp;quot;static/vendor.dll.js&amp;quot;] // 资源路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;为了那几秒钟的时间成本，笔者建议配置上较好。当然也可使用  &lt;a href="https://github.com/asfktz/autodll-webpack-plugin" rel="nofollow noreferrer"&gt;autodll-webpack-plugin&lt;/a&gt;代替手动配置。&lt;/p&gt; &lt;blockquote&gt;⏱并行构建&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置Thread将Loader单进程转换为多进程&lt;/strong&gt;，好处是  &lt;code&gt;释放CPU多核并发的优势&lt;/code&gt;。在使用  &lt;code&gt;webpack&lt;/code&gt;构建项目时会有大量文件需解析和处理，构建过程是计算密集型的操作，随着文件增多会使构建过程变得越慢。&lt;/p&gt; &lt;p&gt;运行在  &lt;code&gt;Node&lt;/code&gt;里的  &lt;code&gt;webpack&lt;/code&gt;是单线程模型，简单来说就是  &lt;code&gt;webpack&lt;/code&gt;待处理的任务需一件件处理，不能同一时刻处理多件任务。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;文件读写&lt;/code&gt;与  &lt;code&gt;计算操作&lt;/code&gt;无法避免，能不能让  &lt;code&gt;webpack&lt;/code&gt;同一时刻处理多个任务，发挥多核  &lt;code&gt;CPU&lt;/code&gt;电脑的威力以提升构建速度呢？  &lt;a href="https://github.com/webpack-contrib/thread-loader" rel="nofollow noreferrer"&gt;thread-loader&lt;/a&gt;来帮你，根据  &lt;code&gt;CPU&lt;/code&gt;个数开启线程。&lt;/p&gt; &lt;p&gt;在此需注意一个问题，若项目文件不算多就不要使用该  &lt;code&gt;性能优化建议&lt;/code&gt;，毕竟开启多个线程也会存在性能开销。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import Os from &amp;quot;os&amp;quot;;

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /\.js$/,
            use: [{
                loader: &amp;quot;thread-loader&amp;quot;,
                options: { workers: Os.cpus().length }
            }, {
                loader: &amp;quot;babel-loader&amp;quot;,
                options: { cacheDirectory: true }
            }]
        }]
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;⏱可视结构&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;配置BundleAnalyzer分析打包文件结构&lt;/strong&gt;，好处是  &lt;code&gt;找出导致体积过大的原因&lt;/code&gt;。从而通过分析原因得出优化方案减少构建时间。  &lt;code&gt;BundleAnalyzer&lt;/code&gt;是  &lt;code&gt;webpack&lt;/code&gt;官方插件，可直观分析  &lt;code&gt;打包文件&lt;/code&gt;的模块组成部分、模块体积占比、模块包含关系、模块依赖关系、文件是否重复、压缩体积对比等可视化数据。&lt;/p&gt; &lt;p&gt;可使用  &lt;a href="https://github.com/webpack-contrib/webpack-bundle-analyzer" rel="nofollow noreferrer"&gt;webpack-bundle-analyzer&lt;/a&gt;配置，有了它，我们就能快速找到相关问题。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { BundleAnalyzerPlugin } from &amp;quot;webpack-bundle-analyzer&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        BundleAnalyzerPlugin()
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;分割代码&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;分割各个模块代码，提取相同部分代码&lt;/strong&gt;，好处是  &lt;code&gt;减少重复代码的出现频率&lt;/code&gt;。  &lt;code&gt;webpack v4&lt;/code&gt;使用  &lt;code&gt;splitChunks&lt;/code&gt;替代  &lt;code&gt;CommonsChunksPlugin&lt;/code&gt;实现代码分割。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;splitChunks&lt;/code&gt;配置较多，详情可参考  &lt;a href="https://webpack.docschina.org/configuration/optimization/#optimizationsplitchunks" rel="nofollow noreferrer"&gt;官网&lt;/a&gt;，在此笔者贴上常用配置。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    optimization: {
        runtimeChunk: { name: &amp;quot;manifest&amp;quot; }, // 抽离WebpackRuntime函数
        splitChunks: {
            cacheGroups: {
                common: {
                    minChunks: 2,
                    name: &amp;quot;common&amp;quot;,
                    priority: 5,
                    reuseExistingChunk: true, // 重用已存在代码块
                    test: AbsPath(&amp;quot;src&amp;quot;)
                },
                vendor: {
                    chunks: &amp;quot;initial&amp;quot;, // 代码分割类型
                    name: &amp;quot;vendor&amp;quot;, // 代码块名称
                    priority: 10, // 优先级
                    test: /node_modules/ // 校验文件正则表达式
                }
            }, // 缓存组
            chunks: &amp;quot;all&amp;quot; // 代码分割类型：all全部模块，async异步模块，initial入口模块
        } // 代码块分割
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;摇树优化&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;删除项目中未被引用代码&lt;/strong&gt;，好处是  &lt;code&gt;移除重复代码和未使用代码&lt;/code&gt;。  &lt;code&gt;摇树优化&lt;/code&gt;首次出现于  &lt;code&gt;rollup&lt;/code&gt;，是  &lt;code&gt;rollup&lt;/code&gt;的核心概念，后来在  &lt;code&gt;webpack v2&lt;/code&gt;里借鉴过来使用。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;摇树优化&lt;/code&gt;只对  &lt;code&gt;ESM规范&lt;/code&gt;生效，对其他模块规范失效。  &lt;code&gt;摇树优化&lt;/code&gt;针对静态结构分析，只有  &lt;code&gt;import/export&lt;/code&gt;才能提供静态的  &lt;code&gt;导入/导出&lt;/code&gt;功能。因此在编写业务代码时必须使用  &lt;code&gt;ESM规范&lt;/code&gt;才能让  &lt;code&gt;摇树优化&lt;/code&gt;移除重复代码和未使用代码。&lt;/p&gt; &lt;p&gt;在  &lt;code&gt;webpack&lt;/code&gt;里只需将打包环境设置成  &lt;code&gt;生产环境&lt;/code&gt;就能让  &lt;code&gt;摇树优化&lt;/code&gt;生效，同时业务代码使用  &lt;code&gt;ESM规范&lt;/code&gt;编写，使用  &lt;code&gt;import&lt;/code&gt;导入模块，使用  &lt;code&gt;export&lt;/code&gt;导出模块。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    mode: &amp;quot;production&amp;quot;
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;动态垫片&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;通过垫片服务根据UA返回当前浏览器代码垫片&lt;/strong&gt;，好处是  &lt;code&gt;无需将繁重的代码垫片打包进去&lt;/code&gt;。每次构建都配置  &lt;code&gt;@babel/preset-env&lt;/code&gt;和  &lt;code&gt;core-js&lt;/code&gt;根据某些需求将  &lt;code&gt;Polyfill&lt;/code&gt;打包进来，这无疑又为代码体积增加了贡献。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;@babel/preset-env&lt;/code&gt;提供的  &lt;code&gt;useBuiltIns&lt;/code&gt;可按需导入  &lt;code&gt;Polyfill&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;false&lt;/strong&gt;：无视   &lt;code&gt;target.browsers&lt;/code&gt;将所有   &lt;code&gt;Polyfill&lt;/code&gt;加载进来&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;entry&lt;/strong&gt;：根据   &lt;code&gt;target.browsers&lt;/code&gt;将部分   &lt;code&gt;Polyfill&lt;/code&gt;加载进来(仅引入有浏览器不支持的   &lt;code&gt;Polyfill&lt;/code&gt;，需在入口文件   &lt;code&gt;import &amp;quot;core-js/stable&amp;quot;&lt;/code&gt;)&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;usage&lt;/strong&gt;：根据   &lt;code&gt;target.browsers&lt;/code&gt;和检测代码里ES6的使用情况将部分   &lt;code&gt;Polyfill&lt;/code&gt;加载进来(无需在入口文件   &lt;code&gt;import &amp;quot;core-js/stable&amp;quot;&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在此推荐大家使用  &lt;code&gt;动态垫片&lt;/code&gt;。  &lt;code&gt;动态垫片&lt;/code&gt;可根据浏览器  &lt;code&gt;UserAgent&lt;/code&gt;返回当前浏览器  &lt;code&gt;Polyfill&lt;/code&gt;，其思路是根据浏览器的  &lt;code&gt;UserAgent&lt;/code&gt;从  &lt;code&gt;browserlist&lt;/code&gt;查找出当前浏览器哪些特性缺乏支持从而返回这些特性的  &lt;code&gt;Polyfill&lt;/code&gt;。对这方面感兴趣的同学可参考  &lt;a href="https://github.com/Financial-Times/polyfill-library" rel="nofollow noreferrer"&gt;polyfill-library&lt;/a&gt;和  &lt;a href="https://github.com/Financial-Times/polyfill-service" rel="nofollow noreferrer"&gt;polyfill-service&lt;/a&gt;的源码。&lt;/p&gt; &lt;p&gt;在此提供两个  &lt;code&gt;动态垫片&lt;/code&gt;服务，可在不同浏览器里点击以下链接看看输出不同的  &lt;code&gt;Polyfill&lt;/code&gt;。相信  &lt;code&gt;IExplore&lt;/code&gt;还是最多  &lt;code&gt;Polyfill&lt;/code&gt;的，它自豪地说：  &lt;code&gt;我就是我，不一样的烟火&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;官方CDN服务&lt;/strong&gt;：   &lt;a href="https://polyfill.io/v3/polyfill.min.js" rel="nofollow noreferrer"&gt;https://polyfill.io/v3/polyfill.min.js&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;阿里CDN服务&lt;/strong&gt;：   &lt;a href="https://polyfill.alicdn.com/polyfill.min.js" rel="nofollow noreferrer"&gt;https://polyfill.alicdn.com/polyfill.min.js&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;使用  &lt;a href="https://github.com/jharris4/html-webpack-tags-plugin" rel="nofollow noreferrer"&gt;html-webpack-tags-plugin&lt;/a&gt;在打包时自动插入  &lt;code&gt;动态垫片&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import HtmlTagsPlugin from &amp;quot;html-webpack-tags-plugin&amp;quot;;

export default {
    plugins: [
        new HtmlTagsPlugin({
            append: false, // 在生成资源后插入
            publicPath: false, // 使用公共路径
            tags: [&amp;quot;https://polyfill.alicdn.com/polyfill.min.js&amp;quot;] // 资源路径
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;按需加载&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;将路由页面/触发性功能单独打包为一个文件，使用时才加载&lt;/strong&gt;，好处是  &lt;code&gt;减轻首屏渲染的负担&lt;/code&gt;。因为项目功能越多其打包体积越大，导致首屏渲染速度越慢。&lt;/p&gt; &lt;p&gt;首屏渲染时只需对应  &lt;code&gt;JS代码&lt;/code&gt;而无需其他  &lt;code&gt;JS代码&lt;/code&gt;，所以可使用  &lt;code&gt;按需加载&lt;/code&gt;。  &lt;code&gt;webpack v4&lt;/code&gt;提供模块按需切割加载功能，配合  &lt;code&gt;import()&lt;/code&gt;可做到首屏渲染减包的效果，从而加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的  &lt;code&gt;JS代码&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;webpack v4&lt;/code&gt;提供魔术注解命名  &lt;code&gt;切割模块&lt;/code&gt;，若无注解则切割出来的模块无法分辨出属于哪个业务模块，所以一般都是一个业务模块共用一个  &lt;code&gt;切割模块&lt;/code&gt;的注解名称。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;const Login = () =&amp;gt; import( /* webpackChunkName: &amp;quot;login&amp;quot; */ &amp;quot;../../views/login&amp;quot;);
const Logon = () =&amp;gt; import( /* webpackChunkName: &amp;quot;logon&amp;quot; */ &amp;quot;../../views/logon&amp;quot;);&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;运行起来控制台可能会报错，在  &lt;code&gt;package.json&lt;/code&gt;的  &lt;code&gt;babel&lt;/code&gt;相关配置里接入  &lt;a href="https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import.html" rel="nofollow noreferrer"&gt;@babel/plugin-syntax-dynamic-import&lt;/a&gt;即可。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;{
    // ...
    &amp;quot;babel&amp;quot;: {
        // ...
        &amp;quot;plugins&amp;quot;: [
            // ...
            &amp;quot;@babel/plugin-syntax-dynamic-import&amp;quot;
        ]
    }
}&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;作用提升&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;分析模块间依赖关系，把打包好的模块合并到一个函数中&lt;/strong&gt;，好处是  &lt;code&gt;减少函数声明和内存花销&lt;/code&gt;。  &lt;code&gt;作用提升&lt;/code&gt;首次出现于  &lt;code&gt;rollup&lt;/code&gt;，是  &lt;code&gt;rollup&lt;/code&gt;的核心概念，后来在  &lt;code&gt;webpack v3&lt;/code&gt;里借鉴过来使用。&lt;/p&gt; &lt;p&gt;在未开启  &lt;code&gt;作用提升&lt;/code&gt;前，构建后的代码会存在大量函数闭包。由于模块依赖，通过  &lt;code&gt;webpack&lt;/code&gt;打包后会转换成  &lt;code&gt;IIFE&lt;/code&gt;，大量函数闭包包裹代码会导致打包体积增大(  &lt;code&gt;模块越多越明显&lt;/code&gt;)。在运行代码时创建的函数作用域变多，从而导致更大的内存开销。&lt;/p&gt; &lt;p&gt;在开启  &lt;code&gt;作用提升&lt;/code&gt;后，构建后的代码会按照引入顺序放到一个函数作用域里，通过适当重命名某些变量以防止变量名冲突，从而减少函数声明和内存花销。&lt;/p&gt; &lt;p&gt;在  &lt;code&gt;webpack&lt;/code&gt;里只需将打包环境设置成  &lt;code&gt;生产环境&lt;/code&gt;就能让  &lt;code&gt;作用提升&lt;/code&gt;生效，或显式设置  &lt;code&gt;concatenateModules&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;export default {
    // ...
    mode: &amp;quot;production&amp;quot;
};
// 显式设置
export default {
    // ...
    optimization: {
        // ...
        concatenateModules: true
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;压缩资源&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;压缩HTML/CSS/JS代码，压缩字体/图像/音频/视频&lt;/strong&gt;，好处是  &lt;code&gt;更有效减少打包体积&lt;/code&gt;。极致地优化代码都有可能不及优化一个资源文件的体积更有效。&lt;/p&gt; &lt;p&gt;针对  &lt;code&gt;HTML&lt;/code&gt;代码，使用  &lt;a href="https://github.com/jantimon/html-webpack-plugin" rel="nofollow noreferrer"&gt;html-webpack-plugin&lt;/a&gt;开启压缩功能。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import HtmlPlugin from &amp;quot;html-webpack-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        HtmlPlugin({
            // ...
            minify: {
                collapseWhitespace: true,
                removeComments: true
            } // 压缩HTML
        })
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;针对  &lt;code&gt;CSS/JS&lt;/code&gt;代码，分别使用以下插件开启压缩功能。其中  &lt;code&gt;OptimizeCss&lt;/code&gt;基于  &lt;code&gt;cssnano&lt;/code&gt;封装，  &lt;code&gt;Uglifyjs&lt;/code&gt;和  &lt;code&gt;Terser&lt;/code&gt;都是  &lt;code&gt;webpack&lt;/code&gt;官方插件，同时需注意压缩  &lt;code&gt;JS代码&lt;/code&gt;需区分  &lt;code&gt;ES5&lt;/code&gt;和  &lt;code&gt;ES6&lt;/code&gt;。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://github.com/NMFR/optimize-css-assets-webpack-plugin" rel="nofollow noreferrer"&gt;optimize-css-assets-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;CSS代码&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/webpack-contrib/uglifyjs-webpack-plugin" rel="nofollow noreferrer"&gt;uglifyjs-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;ES5&lt;/code&gt;版本的   &lt;code&gt;JS代码&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://github.com/webpack-contrib/terser-webpack-plugin" rel="nofollow noreferrer"&gt;terser-webpack-plugin&lt;/a&gt;：压缩   &lt;code&gt;ES6&lt;/code&gt;版本的   &lt;code&gt;JS代码&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;import OptimizeCssAssetsPlugin from &amp;quot;optimize-css-assets-webpack-plugin&amp;quot;;
import TerserPlugin from &amp;quot;terser-webpack-plugin&amp;quot;;
import UglifyjsPlugin from &amp;quot;uglifyjs-webpack-plugin&amp;quot;;

const compressOpts = type =&amp;gt; ({
    cache: true, // 缓存文件
    parallel: true, // 并行处理
    [`${type}Options`]: {
        beautify: false,
        compress: { drop_console: true }
    } // 压缩配置
});
const compressCss = new OptimizeCssAssetsPlugin({
    cssProcessorOptions: {
        autoprefixer: { remove: false }, // 设置autoprefixer保留过时样式
        safe: true // 避免cssnano重新计算z-index
    }
});
const compressJs = USE_ES6
    ? new TerserPlugin(compressOpts(&amp;quot;terser&amp;quot;))
    : new UglifyjsPlugin(compressOpts(&amp;quot;uglify&amp;quot;));

export default {
    // ...
    optimization: {
        // ...
        minimizer: [compressCss, compressJs] // 代码压缩
    }
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;针对  &lt;code&gt;字体/音频/视频&lt;/code&gt;文件，还真没相关  &lt;code&gt;Plugin&lt;/code&gt;供我们使用，就只能拜托大家在发布项目到生产服前使用对应的压缩工具处理了。针对  &lt;code&gt;图像&lt;/code&gt;文件，大部分  &lt;code&gt;Loader/Plugin&lt;/code&gt;封装时均使用了某些图像处理工具，而这些工具的某些功能又托管在国外服务器里，所以导致经常安装失败。具体解决方式可回看笔者曾经发布的  &lt;a href="https://juejin.cn/post/6844904192247595022" rel="nofollow noreferrer"&gt;《聊聊NPM镜像那些险象环生的坑》&lt;/a&gt;一文寻求答案。&lt;/p&gt; &lt;p&gt;鉴于此，笔者花了一点小技巧开发了一个  &lt;code&gt;Plugin&lt;/code&gt;用于配合  &lt;code&gt;webpack&lt;/code&gt;压缩图像，详情请参考  &lt;a href="https://github.com/JowayYoung/tinyimg-webpack-plugin" rel="nofollow noreferrer"&gt;tinyimg-webpack-plugin&lt;/a&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import TinyimgPlugin from &amp;quot;tinyimg-webpack-plugin&amp;quot;;

export default {
    // ...
    plugins: [
        // ...
        TinyimgPlugin()
    ]
};&lt;/code&gt;&lt;/pre&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;上述  &lt;code&gt;构建策略&lt;/code&gt;都集成到笔者开源的  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;bruce-cli&lt;/a&gt;里，它是一个  &lt;strong&gt;React/Vue&lt;/strong&gt;应用自动化构建脚手架，其零配置开箱即用的优点非常适合入门级、初中级、快速开发项目的前端同学使用，还可通过创建  &lt;code&gt;brucerc.js&lt;/code&gt;文件覆盖其默认配置，只需专注业务代码的编写无需关注构建代码的编写，让项目结构更简洁。详情请戳  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;，使用时记得查看文档，支持一个  &lt;a href="https://github.com/JowayYoung/bruce-cli" rel="nofollow noreferrer"&gt;Star&lt;/a&gt;哈！&lt;/p&gt; &lt;h5&gt;图像策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;图像类型&lt;/code&gt;做相关处理，同时也是接入成本较低的  &lt;code&gt;性能优化策略&lt;/code&gt;。只需做到以下两点即可。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;图像选型&lt;/strong&gt;：了解所有图像类型的特点及其何种应用场景最合适&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;图像压缩&lt;/strong&gt;：在部署到生产环境前使用工具或脚本对其压缩处理&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;code&gt;图像选型&lt;/code&gt;一定要知道每种图像类型的  &lt;code&gt;体积/质量/兼容/请求/压缩/透明/场景&lt;/code&gt;等参数相对值，这样才能迅速做出判断在何种场景使用何种类型的图像。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;th align="center"&gt;类型&lt;/th&gt;   &lt;th align="center"&gt;体积&lt;/th&gt;   &lt;th align="center"&gt;质量&lt;/th&gt;   &lt;th align="center"&gt;兼容&lt;/th&gt;   &lt;th align="center"&gt;请求&lt;/th&gt;   &lt;th align="center"&gt;压缩&lt;/th&gt;   &lt;th align="center"&gt;透明&lt;/th&gt;   &lt;th&gt;场景&lt;/th&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;JPG&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;有损&lt;/td&gt;   &lt;td align="center"&gt;不支持&lt;/td&gt;   &lt;td&gt;背景图、轮播图、色彩丰富图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;PNG&lt;/td&gt;   &lt;td align="center"&gt;大&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标、透明图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;SVG&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标、矢量图&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;WebP&lt;/td&gt;   &lt;td align="center"&gt;小&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;低&lt;/td&gt;   &lt;td align="center"&gt;是&lt;/td&gt;   &lt;td align="center"&gt;兼备&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;看兼容情况&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;Base64&lt;/td&gt;   &lt;td align="center"&gt;看情况&lt;/td&gt;   &lt;td align="center"&gt;中&lt;/td&gt;   &lt;td align="center"&gt;高&lt;/td&gt;   &lt;td align="center"&gt;否&lt;/td&gt;   &lt;td align="center"&gt;无损&lt;/td&gt;   &lt;td align="center"&gt;支持&lt;/td&gt;   &lt;td&gt;图标&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;  &lt;code&gt;图像压缩&lt;/code&gt;可在上述  &lt;code&gt;构建策略-压缩资源&lt;/code&gt;里完成，也可自行使用工具完成。由于现在大部分  &lt;code&gt;webpack&lt;/code&gt;图像压缩工具不是安装失败就是各种环境问题(  &lt;code&gt;你懂的&lt;/code&gt;)，所以笔者还是推荐在发布项目到生产服前使用图像压缩工具处理，这样运行稳定也不会增加打包时间。&lt;/p&gt; &lt;p&gt;好用的图像压缩工具无非就是以下几个，若有更好用的工具麻烦在评论里补充喔！&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;th align="center"&gt;工具&lt;/th&gt;   &lt;th align="center"&gt;开源&lt;/th&gt;   &lt;th align="center"&gt;收费&lt;/th&gt;   &lt;th align="center"&gt;API&lt;/th&gt;   &lt;th&gt;免费体验&lt;/th&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://www.tuhaokuai.com" rel="nofollow noreferrer"&gt;QuickPicture&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型较多，压缩质感较好，有体积限制，有数量限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://shrinkme.app" rel="nofollow noreferrer"&gt;ShrinkMe&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型较多，压缩质感一般，无数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://squoosh.app" rel="nofollow noreferrer"&gt;Squoosh&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感一般，无数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://tinyjpg.com" rel="nofollow noreferrer"&gt;TinyJpg&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感很好，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://tinypng.com" rel="nofollow noreferrer"&gt;TinyPng&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td align="center"&gt;✔️&lt;/td&gt;   &lt;td&gt;可压缩类型较少，压缩质感很好，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center"&gt;    &lt;a href="https://zhitu.isux.us" rel="nofollow noreferrer"&gt;Zhitu&lt;/a&gt;&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td align="center"&gt;✖️&lt;/td&gt;   &lt;td&gt;可压缩类型一般，压缩质感一般，有数量限制，有体积限制&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;若不想在网站里来回拖动图像文件，可使用笔者开源的图像批处理工具  &lt;a href="https://github.com/JowayYoung/img-master" rel="nofollow noreferrer"&gt;img-master&lt;/a&gt;代替，不仅有压缩功能，还有分组功能、标记功能和变换功能。目前笔者负责的全部项目都使用该工具处理，一直用一直爽！&lt;/p&gt; &lt;p&gt;  &lt;code&gt;图像策略&lt;/code&gt;也许处理一张图像就能完爆所有  &lt;code&gt;构建策略&lt;/code&gt;，因此是一种很廉价但极有效的  &lt;code&gt;性能优化策略&lt;/code&gt;。&lt;/p&gt; &lt;h5&gt;分发策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;内容分发网络&lt;/code&gt;做相关处理，同时也是接入成本较高的  &lt;code&gt;性能优化策略&lt;/code&gt;，需足够资金支持。&lt;/p&gt; &lt;p&gt;虽然接入成本较高，但大部分企业都会购买一些  &lt;code&gt;CDN服务器&lt;/code&gt;，所以在部署的事情上就不用过分担忧，尽管使用就好。该策略尽量遵循以下两点就能发挥  &lt;code&gt;CDN&lt;/code&gt;最大作用。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;所有静态资源走CDN&lt;/strong&gt;：开发阶段确定哪些文件属于静态资源&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;把静态资源与主页面置于不同域名下&lt;/strong&gt;：避免请求带上   &lt;code&gt;Cookie&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;strong&gt;内容分发网络&lt;/strong&gt;简称  &lt;strong&gt;CDN&lt;/strong&gt;，指一组分布在各地存储数据副本并可根据就近原则满足数据请求的服务器。其核心特征是  &lt;code&gt;缓存&lt;/code&gt;和  &lt;code&gt;回源&lt;/code&gt;，缓存是把资源复制到  &lt;code&gt;CDN服务器&lt;/code&gt;里，回源是  &lt;code&gt;资源过期/不存在&lt;/code&gt;就向上层服务器请求并复制到  &lt;code&gt;CDN服务器&lt;/code&gt;里。&lt;/p&gt; &lt;p&gt;使用  &lt;code&gt;CDN&lt;/code&gt;可降低网络拥塞，提高用户访问响应速度和命中率。构建在现有网络基础上的智能虚拟网络，依靠部署在各地服务器，通过中心平台的调度、负载均衡、内容分发等功能模块，使用户就近获取所需资源，这就是  &lt;code&gt;CDN&lt;/code&gt;的终极使命。&lt;/p&gt; &lt;p&gt;基于  &lt;code&gt;CDN&lt;/code&gt;的  &lt;strong&gt;就近原则&lt;/strong&gt;所带来的优点，可将网站所有静态资源全部部署到  &lt;code&gt;CDN服务器&lt;/code&gt;里。那静态资源包括哪些文件？通常来说就是无需服务器产生计算就能得到的资源，例如不常变化的  &lt;code&gt;样式文件&lt;/code&gt;、  &lt;code&gt;脚本文件&lt;/code&gt;和  &lt;code&gt;多媒体文件(字体/图像/音频/视频)&lt;/code&gt;等。&lt;/p&gt; &lt;p&gt;若需单独配置  &lt;code&gt;CDN服务器&lt;/code&gt;，可考虑  &lt;a href="https://www.aliyun.com/product/oss" rel="nofollow noreferrer"&gt;阿里云OSS&lt;/a&gt;、  &lt;a href="https://www.163yun.com/product/nos" rel="nofollow noreferrer"&gt;网易树帆NOS&lt;/a&gt;和  &lt;a href="https://www.qiniu.com/products/kodo" rel="nofollow noreferrer"&gt;七牛云Kodo&lt;/a&gt;，当然配置起来还需购买该产品对应的  &lt;code&gt;CDN服务&lt;/code&gt;。由于篇幅问题，这些配置在购买后会有相关教程，可自行体会，在此就不再叙述了。&lt;/p&gt; &lt;p&gt;笔者推荐大家首选  &lt;a href="https://www.163yun.com/product/nos" rel="nofollow noreferrer"&gt;网易树帆NOS&lt;/a&gt;，毕竟对自家产品还是挺有信心的，不小心给自家产品打了个小广告了，哈哈！&lt;/p&gt; &lt;h5&gt;缓存策略&lt;/h5&gt; &lt;p&gt;该策略主要围绕  &lt;code&gt;浏览器缓存&lt;/code&gt;做相关处理，同时也使接入成本最低的  &lt;code&gt;性能优化策略&lt;/code&gt;。其显著减少网络传输所带来的损耗，提升网页访问速度，是一种很值得使用的  &lt;code&gt;性能优化策略&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;通过下图可知，为了让  &lt;code&gt;浏览器缓存&lt;/code&gt;发挥最大作用，该策略尽量遵循以下五点就能发挥  &lt;code&gt;浏览器缓存&lt;/code&gt;最大作用。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;考虑拒绝一切缓存策略&lt;/strong&gt;：   &lt;code&gt;Cache-Control:no-store&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源是否每次向服务器请求&lt;/strong&gt;：   &lt;code&gt;Cache-Control:no-cache&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源是否被代理服务器缓存&lt;/strong&gt;：   &lt;code&gt;Cache-Control:public/private&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑资源过期时间&lt;/strong&gt;：   &lt;code&gt;Expires:t/Cache-Control:max-age=t,s-maxage=t&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;考虑协商缓存&lt;/strong&gt;：   &lt;code&gt;Last-Modified/Etag&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;  &lt;img alt="&amp;#32531;&amp;#23384;&amp;#21028;&amp;#26029;&amp;#26426;&amp;#21046;" src="https://segmentfault.com/img/remote/1460000040343060" title="&amp;#32531;&amp;#23384;&amp;#21028;&amp;#26029;&amp;#26426;&amp;#21046;"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;同时  &lt;code&gt;浏览器缓存&lt;/code&gt;也是高频面试题之一，笔者觉得上述涉及到的名词在不同语序串联下也能完全理解才能真正弄懂  &lt;code&gt;浏览器缓存&lt;/code&gt;在  &lt;code&gt;性能优化&lt;/code&gt;里起到的作用。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;缓存策略&lt;/code&gt;通过设置  &lt;code&gt;HTTP&lt;/code&gt;报文实现，在形式上分为  &lt;strong&gt;强缓存/强制缓存&lt;/strong&gt;和  &lt;strong&gt;协商缓存/对比缓存&lt;/strong&gt;。为了方便对比，笔者将某些细节使用图例展示，相信你有更好的理解。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#24378;&amp;#32531;&amp;#23384;.png" src="https://segmentfault.com/img/remote/1460000040343061" title="&amp;#24378;&amp;#32531;&amp;#23384;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#21327;&amp;#21830;&amp;#32531;&amp;#23384;.png" src="https://segmentfault.com/img/remote/1460000040343062" title="&amp;#21327;&amp;#21830;&amp;#32531;&amp;#23384;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;整个  &lt;code&gt;缓存策略&lt;/code&gt;机制很明了，  &lt;code&gt;先走强缓存，若命中失败才走协商缓存&lt;/code&gt;。若命中  &lt;code&gt;强缓存&lt;/code&gt;，直接使用  &lt;code&gt;强缓存&lt;/code&gt;；若未命中  &lt;code&gt;强缓存&lt;/code&gt;，发送请求到服务器检查是否命中  &lt;code&gt;协商缓存&lt;/code&gt;；若命中  &lt;code&gt;协商缓存&lt;/code&gt;，服务器返回304通知浏览器使用  &lt;code&gt;本地缓存&lt;/code&gt;，否则返回  &lt;code&gt;最新资源&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;有两种较常用的应用场景值得使用  &lt;code&gt;缓存策略&lt;/code&gt;一试，当然更多应用场景都可根据项目需求制定。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;频繁变动资源&lt;/strong&gt;：设置   &lt;code&gt;Cache-Control:no-cache&lt;/code&gt;，使浏览器每次都发送请求到服务器，配合   &lt;code&gt;Last-Modified/ETag&lt;/code&gt;验证资源是否有效&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;不常变化资源&lt;/strong&gt;：设置   &lt;code&gt;Cache-Control:max-age=31536000&lt;/code&gt;，对文件名哈希处理，当代码修改后生成新的文件名，当HTML文件引入文件名发生改变才会下载最新文件&lt;/li&gt;&lt;/ul&gt; &lt;h4&gt;渲染层面&lt;/h4&gt; &lt;p&gt;  &lt;strong&gt;渲染层面&lt;/strong&gt;的性能优化，无疑是如何让代码  &lt;code&gt;解析更好执行更快&lt;/code&gt;。因此笔者从以下五方面做出建议。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;CSS策略&lt;/strong&gt;：基于CSS规则&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;DOM策略&lt;/strong&gt;：基于DOM操作&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;阻塞策略&lt;/strong&gt;：基于脚本加载&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;回流重绘策略&lt;/strong&gt;：基于回流重绘&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;异步更新策略&lt;/strong&gt;：基于异步更新&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;上述五方面都是编写代码时完成，充满在整个项目流程的开发阶段里。因此在开发阶段需时刻注意以下涉及到的每一点，养成良好的开发习惯，  &lt;code&gt;性能优化&lt;/code&gt;也自然而然被使用上了。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;更多表现在编码细节上，而并非实体代码。简单来说就是遵循某些编码规则，才能将  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;发挥到最大作用。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;回流重绘策略&lt;/strong&gt;在  &lt;code&gt;渲染层面&lt;/code&gt;的  &lt;code&gt;性能优化&lt;/code&gt;里占比较重，也是最常规的  &lt;code&gt;性能优化&lt;/code&gt;之一。上年笔者发布的掘金小册  &lt;a href="https://juejin.cn/book/6850413616484040711" rel="nofollow noreferrer"&gt;《玩转CSS的艺术之美》&lt;/a&gt;使用一整章讲解  &lt;code&gt;回流重绘&lt;/code&gt;，本章已开通试读，更多细节请戳  &lt;a href="https://juejin.cn/book/6850413616484040711/section/6850413616559194119" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;。&lt;/p&gt; &lt;h5&gt;CSS策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;避免出现超过三层的   &lt;code&gt;嵌套规则&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免为   &lt;code&gt;ID选择器&lt;/code&gt;添加多余选择器&lt;/li&gt;  &lt;li&gt;避免使用   &lt;code&gt;标签选择器&lt;/code&gt;代替   &lt;code&gt;类选择器&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免使用   &lt;code&gt;通配选择器&lt;/code&gt;，只对目标节点声明规则&lt;/li&gt;  &lt;li&gt;避免重复匹配重复定义，关注   &lt;code&gt;可继承属性&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;DOM策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;缓存   &lt;code&gt;DOM计算属性&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;避免过多   &lt;code&gt;DOM操作&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;使用   &lt;code&gt;DOMFragment&lt;/code&gt;缓存批量化   &lt;code&gt;DOM操作&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;阻塞策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;脚本与   &lt;code&gt;DOM/其它脚本&lt;/code&gt;的依赖关系很强：对   &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;设置   &lt;code&gt;defer&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;脚本与   &lt;code&gt;DOM/其它脚本&lt;/code&gt;的依赖关系不强：对   &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;设置   &lt;code&gt;async&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;回流重绘策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;缓存   &lt;code&gt;DOM计算属性&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;使用类合并样式，避免逐条改变样式&lt;/li&gt;  &lt;li&gt;使用   &lt;code&gt;display&lt;/code&gt;控制   &lt;code&gt;DOM显隐&lt;/code&gt;，将   &lt;code&gt;DOM离线化&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;异步更新策略&lt;/h5&gt; &lt;ul&gt;  &lt;li&gt;在   &lt;code&gt;异步任务&lt;/code&gt;中修改   &lt;code&gt;DOM&lt;/code&gt;时把其包装成   &lt;code&gt;微任务&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;六大指标&lt;/h3&gt; &lt;p&gt;笔者根据  &lt;code&gt;性能优化&lt;/code&gt;的重要性和实际性划分出  &lt;code&gt;九大策略&lt;/code&gt;和  &lt;code&gt;六大指标&lt;/code&gt;，其实它们都是一条条活生生的  &lt;code&gt;性能优化建议&lt;/code&gt;。有些  &lt;code&gt;性能优化建议&lt;/code&gt;接不接入影响都不大，因此笔者将  &lt;code&gt;九大策略&lt;/code&gt;定位高于  &lt;code&gt;六大指标&lt;/code&gt;。针对  &lt;code&gt;九大策略&lt;/code&gt;还是建议在开发阶段和生产阶段接入，在项目复盘时可将  &lt;code&gt;六大指标&lt;/code&gt;的条条框框根据实际应用场景接入。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;六大指标&lt;/code&gt;基本囊括大部分  &lt;code&gt;性能优化&lt;/code&gt;细节，可作为  &lt;code&gt;九大策略&lt;/code&gt;的补充。笔者根据每条  &lt;code&gt;性能优化建议&lt;/code&gt;的特征将  &lt;code&gt;指标&lt;/code&gt;划分为以下六方面。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;加载优化&lt;/strong&gt;：资源在加载时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;执行优化&lt;/strong&gt;：资源在执行时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;渲染优化&lt;/strong&gt;：资源在渲染时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;样式优化&lt;/strong&gt;：样式在编码时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;脚本优化&lt;/strong&gt;：脚本在编码时可做的性能优化&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;V8引擎优化&lt;/strong&gt;：针对   &lt;code&gt;V8引擎&lt;/code&gt;特征可做的性能优化&lt;/li&gt;&lt;/ul&gt; &lt;h5&gt;加载优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#21152;&amp;#36733;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343063" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#21152;&amp;#36733;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;执行优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#25191;&amp;#34892;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343064" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#25191;&amp;#34892;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;渲染优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#28210;&amp;#26579;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343065" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#28210;&amp;#26579;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;样式优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#26679;&amp;#24335;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343066" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#26679;&amp;#24335;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;脚本优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#33050;&amp;#26412;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343067" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-&amp;#33050;&amp;#26412;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h5&gt;V8引擎优化&lt;/h5&gt; &lt;p&gt;  &lt;img alt="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-V8&amp;#24341;&amp;#25806;&amp;#20248;&amp;#21270;.png" src="https://segmentfault.com/img/remote/1460000040343068" title="&amp;#20845;&amp;#22823;&amp;#25351;&amp;#26631;-V8&amp;#24341;&amp;#25806;&amp;#20248;&amp;#21270;.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;总结&lt;/h3&gt; &lt;p&gt;  &lt;strong&gt;性能优化&lt;/strong&gt;作为老生常谈的知识，必然会在工作或面试时遇上。很多时候不是想到某条  &lt;code&gt;性能优化建议&lt;/code&gt;就去做或答，而是要对这方面有一个整体认知，知道为何这样设计，这样设计的目的能达到什么效果。&lt;/p&gt; &lt;p&gt;  &lt;code&gt;性能优化&lt;/code&gt;不是通过一篇文章就能全部讲完，若详细去讲可能要写两本书的篇幅才能讲完。本文能到给大家的就是一个方向一种态度，学以致用呗，希望阅读完本文会对你有所帮助。&lt;/p&gt; &lt;p&gt;最后，笔者将本文所有内容整理成一张高清脑图，由于体积太大无法上传，可关注笔者个人公众号  &lt;strong&gt;IQ前端&lt;/strong&gt;并回复  &lt;code&gt;性能优化&lt;/code&gt;获取口袋知识图谱吧！&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端 html css javascript 性能优化</category>
      <guid isPermaLink="true">https://itindex.net/detail/61610-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E7%AD%96%E7%95%A5</guid>
      <pubDate>Wed, 14 Jul 2021 08:00:00 CST</pubDate>
    </item>
    <item>
      <title>中小型前端团队代码规范工程化最佳实践 - ESLint</title>
      <link>https://itindex.net/detail/61336-%E5%89%8D%E7%AB%AF-%E5%9B%A2%E9%98%9F-%E4%BB%A3%E7%A0%81</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt; &lt;blockquote&gt;There are a thousand Hamlets in a thousand people&amp;apos;s eyes.&lt;/blockquote&gt; &lt;p&gt;一千个程序员，就有一千种代码风格。在前端开发中，有几个至今还在争论的代码风格差异：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;单引号还是双引号？&lt;/li&gt;  &lt;li&gt;代码行结束是否需要分号？&lt;/li&gt;  &lt;li&gt;两个空格还是四个空格？&lt;/li&gt;  &lt;li&gt;...&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;这几个代码风格差异在协同开发中经常会被互相吐槽，甚至不能忍受。&lt;/p&gt; &lt;p&gt;除此之外，由于 JavaScript 的灵活性，往往一段代码能有多种写法，这时候也会导致协同时差异。并且，有一些写法可能会导致不易发现的 bug，或者这些写法的性能不好，开发时也应该避免。&lt;/p&gt; &lt;p&gt;为了解决这类静态代码问题，每个团队都需要一个统一的 JavaScript 代码规范，团队成员都遵守这份代码规范来编写代码。当然，靠人来保障代码规范是不可靠的，需要有对应的工具来保障，ESLint 就是这个工具。&lt;/p&gt; &lt;p&gt;有的读者看到这里，可能会说：Prettier 也可以保证代码风格一致。是的，Prettier 确实可以按照设置的规则对代码进行统一格式化，后面的文章也会有对应的介绍。但是需要明确的一点是，Prettier 只会在格式上对代码进行格式化，一些隐藏的代码质量问题 Prettier 是无法发现的，而 ESLint 可以。&lt;/p&gt; &lt;h2&gt;关于 ESLint&lt;/h2&gt; &lt;p&gt;关于   &lt;a href="https://eslint.org/" rel="nofollow noreferrer"&gt;ESLint&lt;/a&gt;，它的 Slogan 是 Find and fix problems in your JavaScript code。如上文所说，它可以发现并修复你 JavaScript 代码中的问题。来看一下官网上描述 ESLint 具备的三个特性：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;Find Problems&lt;/strong&gt;。ESLint 通过静态代码分析可以快速发现代码中的问题。ESLint 可以运行在大多数文本编辑器中，并且也可以在工作流中接入 ESLint&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;Fix Automatically&lt;/strong&gt;。ESLint 发现的很多问题都可以自动修复&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;Customize&lt;/strong&gt;。可以定制 ESLint 检查规则&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;基于以上描述，我们在前端工程化中可以这样使用 ESLint：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;基于业界现有的 ESLint 规范和团队代码习惯定制一套统一的 ESLint 代码规则&lt;/li&gt;  &lt;li&gt;将统一代码规则封装成 ESLint 规则包接入&lt;/li&gt;  &lt;li&gt;将 ESLint 接入脚手架、编辑器以及研发工作流中&lt;/li&gt;&lt;/ol&gt; &lt;h2&gt;快速上手&lt;/h2&gt; &lt;p&gt;  &lt;strong&gt;先简单介绍一下如何使用 ESLint，如果已经有所了解的同学，可以直接跳过这一节。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;新建一个包含   &lt;code&gt;package.json&lt;/code&gt; 的目录（可以在空目录下执行   &lt;code&gt;npm init -y&lt;/code&gt;），新建一个   &lt;code&gt;index.js&lt;/code&gt;：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// index.js
const name = &amp;apos;axuebin&amp;apos;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;安装   &lt;code&gt;eslint&lt;/code&gt; ：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;npm install eslint --save-dev&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后执行   &lt;code&gt;./node_modules/.bin/eslint --init&lt;/code&gt; 或者   &lt;code&gt;npx eslint --init&lt;/code&gt; 生成一个 ESLint 配置文件   &lt;code&gt;.eslintc.js&lt;/code&gt;：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;module.exports = {
  env: {
    es2021: true,
  },
  extends: &amp;apos;eslint:recommended&amp;apos;,
  parserOptions: {
    ecmaVersion: 12,
  },
  rules: {},
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;生成好配置文件之后，就可以执行   &lt;code&gt;./node_modules/.bin/eslint index.js&lt;/code&gt; 或者   &lt;code&gt;npx eslint index.js&lt;/code&gt; 命令对文件进行检查。结果如下：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814307" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;  &lt;code&gt;index.js&lt;/code&gt; 中的代码命中了   &lt;code&gt;no-unused-vars&lt;/code&gt; 这个规则，默认情况下，这个规则是会报   &lt;code&gt;error&lt;/code&gt; 的，也就是 ESLint   &lt;strong&gt;不允许代码中出现未被使用的变量&lt;/strong&gt;。这是一个好习惯，有利于代码的维护。&lt;/p&gt; &lt;h3&gt;简单配置&lt;/h3&gt; &lt;p&gt;我们来尝试配置 ESLint 的检查规则。以分号和引号举例，现在你作为团队代码规范的指定人，希望团队成员开发的代码，都是  &lt;strong&gt;单引号&lt;/strong&gt;和  &lt;strong&gt;带分号&lt;/strong&gt;的。&lt;/p&gt; &lt;p&gt;打开   &lt;code&gt;.eslintrc.js&lt;/code&gt; 配置文件，在   &lt;code&gt;rules&lt;/code&gt; 中添加相关配置项：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;module.exports = {
  env: {
    es2021: true,
  },
  extends: &amp;apos;eslint:recommended&amp;apos;,
  parserOptions: {
    ecmaVersion: 12,
  },
  rules: {
    semi: [&amp;apos;error&amp;apos;, &amp;apos;always&amp;apos;],
    quotes: [&amp;apos;error&amp;apos;, &amp;apos;single&amp;apos;],
  },
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后我们将   &lt;code&gt;index.js&lt;/code&gt; 中的代码改成：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// index.js
const name = &amp;quot;axuebin&amp;quot;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行   &lt;code&gt;eslint&lt;/code&gt; 命令之后：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814306" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;可以看到检查结果如下：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;[no-unused-vars] &lt;/strong&gt;&amp;apos;name&amp;apos; is assigned a value but never used。定义了 name 变量却未使用。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;[quotes] &lt;/strong&gt;Strings must use singlequote。字符串必须使用单引号。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;[semi] &lt;/strong&gt;Missing semicolon。缺失分号。&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;老老实实地按照规范修改代码，使用单引号并将加上分号。当然，如果你们希望是双引号和不带分号，修改相应的配置即可。&lt;/p&gt; &lt;p&gt;具体各个规则如何配置可以查看：  &lt;a href="https://eslint.org/docs/rules" rel="nofollow noreferrer"&gt;https://eslint.org/docs/rules&lt;/a&gt;&lt;/p&gt; &lt;h3&gt;自动修复&lt;/h3&gt; &lt;p&gt;执行   &lt;code&gt;eslint xxx --fix&lt;/code&gt; 可以自动修复一些代码中的问题，将无法自动修复的问题暴露出来。比如上文中提到的引号和分号的问题，就可以通过   &lt;code&gt;--fix&lt;/code&gt; 自动修复，而   &lt;code&gt;no-unused-vars&lt;/code&gt; 变量未使用的问题，ESLint 就无法自动修复。  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814310" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;使用配置包&lt;/h3&gt; &lt;p&gt;在   &lt;code&gt;init&lt;/code&gt; 生成的配置文件中，我们看到包含这一行代码：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;module.exports = {
  extends: &amp;quot;eslint:recommended&amp;quot;
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这一行代码的意思是，使用 ESLint 的推荐配置。   &lt;code&gt;extends: &amp;apos;xxx&amp;apos;&lt;/code&gt; 就是   &lt;strong&gt;继承&lt;/strong&gt;，当前的配置继承于   &lt;code&gt;xxx&lt;/code&gt; 的配置，在此基础上进行扩展。&lt;/p&gt; &lt;p&gt;因此，我们也可以使用任意封装好的配置，可以在   &lt;a href="https://www.npmjs.com/search?q=eslint-config" rel="nofollow noreferrer"&gt;NPM&lt;/a&gt; 上或者   &lt;a href="https://github.com/search?q=eslint-config" rel="nofollow noreferrer"&gt;GItHub&lt;/a&gt; 上搜索   &lt;code&gt;eslint-config&lt;/code&gt; 关键词获取，本文我们将这类封装好的配置称作 “配置集”。比较常见的配置包有以下几个：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;eslint-config-airbnb&lt;/strong&gt;: Airbnb 公司提供的配置集&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;eslint-config-prettier&lt;/strong&gt;: 使用这个配置集，会关闭一些可能与 Prettier 冲突的规则&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;eslint-config-react&lt;/strong&gt;: create react app 使用的配置集&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;eslint-config-vue&lt;/strong&gt;: vuejs 使用的配置集&lt;/li&gt;  &lt;li&gt;...&lt;/li&gt;&lt;/ul&gt; &lt;h2&gt;最佳实践&lt;/h2&gt; &lt;p&gt;简单了解完 ESLint 之后，对于 ESLint 的更多使用细节以及原理，在本篇文章就不展开了，感兴趣的朋友可以在官网详细了解。本文重点还是在于  &lt;strong&gt;如何在团队工程化体系中落地 ESLint&lt;/strong&gt;，这里提几个最佳实践。&lt;/p&gt; &lt;h3&gt;抽象配置集&lt;/h3&gt; &lt;p&gt;对于独立开发者以及业务场景比较简单的小型团队而言，使用现成、完备的第三方配置集是非常高效的，可以较低成本低接入 ESLint 代码检查。&lt;/p&gt; &lt;p&gt;但是，对于中大型团队而言，在实际代码规范落地的过程中我们会发现，不可能存在一个能够完全符合团队风格的三方配置包，我们还是会在   &lt;code&gt;extends&lt;/code&gt; 三方配置集的基础上，再手动在   &lt;code&gt;rules&lt;/code&gt; 配置里加一些自定义的规则。时间长了，有可能 A 应用和 B 应用里的   &lt;code&gt;rules&lt;/code&gt; 就不一样了，就很难达到统一的目的。&lt;/p&gt; &lt;p&gt;这时候，就需要一个中心化的方式来管理配置包：  &lt;strong&gt;根据团队代码风格整理（或者基于现有的三方配置集）发布一个配置集，团队统一使用这个包，就可以做到中心化管理和更新&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;除此之外，从技术层面考虑，目前一个前端团队的面对的场景可能比较复杂。比如：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;技术选型不一致&lt;/strong&gt;：框架上 PC 使用 React，H5 使用 Vue；是否使用 TypeScript&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;跨端场景多&lt;/strong&gt;：Web 端和小程序端，还有 Node&lt;/li&gt;  &lt;li&gt;...&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;以上问题在真实开发中都是存在的，所以在代码规范的工程化方案落地时，一个单一功能的配置集是不够用的，这时候还需要考虑这个配置集如何抽象。&lt;/p&gt; &lt;p&gt;为了解决以上问题，这里提供一种解决方案的思路：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814308" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;具体拆解来看，就是有一个类似 eslint-config-standard 的基础规则集（包括代码风格、变量相关、ES6 语法等），在此基础之上集成社区的一些插件（Vue/React）等，封装成统一的一个 NPM Package 发布，消费时根据当前应用类型通过不同路径来 extends 对应的配置集。&lt;/p&gt; &lt;p&gt;这里有一个 Demo，感兴趣的朋友可以看一下：  &lt;a href="http://github.com/axuebin/eslint-config-axuebin" rel="nofollow noreferrer"&gt;eslint-config-axuebin&lt;/a&gt;&lt;/p&gt; &lt;h3&gt;开发插件&lt;/h3&gt; &lt;p&gt;ESLint 提供了丰富的配置供开发者选择，但是在复杂的业务场景和特定的技术栈下，这些通用规则是不够用的。ESLint 通过插件的形式赋予了扩展性，开发者可以自定义任意的检查规则，比如 eslint-plugin-vue / eslint-plugin-react 就是 Vue / React 框架中使用的扩展插件，官网也提供了  &lt;a href="https://eslint.org/docs/developer-guide/working-with-plugins" rel="nofollow noreferrer"&gt;相关文档&lt;/a&gt;引导开发者开发一个插件。&lt;/p&gt; &lt;p&gt;一般来说，我们也不需要开发插件，但我们至少需要了解有这么个东西。在做一些团队代码质量检查的时候，我们可能会有一些特殊的业务逻辑，这时候 ESLint 插件是可以帮助我们做一些事情。&lt;/p&gt; &lt;p&gt;这里就不展开了，主要就是一些 AST 的用法，照着官方文档就可以上手，或者可以参考现有的一些插件写法。&lt;/p&gt; &lt;h3&gt;脚手架 / CLI 工具&lt;/h3&gt; &lt;p&gt;当有了团队的统一 ESLint 配置集和插件之后，我们会将它们集成到脚手架中，方便新项目集成和开箱即用。但是对于一些老项目，如果需要手动改造还是会有一些麻烦的，这时候就可以借助于 CLI 来完成一键升级。&lt;/p&gt; &lt;p&gt;本文结合上文的 Demo   &lt;a href="http://github.com/axuebin/eslint-config-axuebin" rel="nofollow noreferrer"&gt;eslint-config-axuebin&lt;/a&gt;，设计一个简单的 CLI Demo。由于当前配置也比较简单，所以 CLI 只需要做几件简单的事情即可：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;询问用户当前项目的类型（是 JavaScript 还是 TypeScript、是 React 还是 Vue）&lt;/li&gt;  &lt;li&gt;根据项目类型写    &lt;code&gt;.eslintrc.js&lt;/code&gt; 文件&lt;/li&gt;  &lt;li&gt;根据项目类型安装所需依赖（比如 vue 需要 eslint-plugin-vue）&lt;/li&gt;  &lt;li&gt;在    &lt;code&gt;package.json&lt;/code&gt; 的    &lt;code&gt;scripts&lt;/code&gt; 中写入    &lt;code&gt;&amp;quot;lint&amp;quot;: &amp;quot;eslint src test --fix&amp;quot;&lt;/code&gt;  &lt;/li&gt;&lt;/ul&gt; &lt;p&gt;核心代码如下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;const path = require(&amp;apos;path&amp;apos;);
const fs = require(&amp;apos;fs&amp;apos;);
const chalk = require(&amp;apos;chalk&amp;apos;);
const spawn = require(&amp;apos;cross-spawn&amp;apos;);

const { askForLanguage, askForFrame } = require(&amp;apos;./ask&amp;apos;);
const { eslintrcConfig, needDeps } = require(&amp;apos;./config&amp;apos;);

module.exports = async () =&amp;gt; {
  const language = await askForLanguage();
  const frame = await askForFrame();

  let type = language;
  if (frame) {
    type += `/${frame}`;
  }

  fs.writeFileSync(
    path.join(process.cwd(), &amp;apos;.eslintrc.js&amp;apos;),
    `// Documentation\n// https://github.com/axuebin/eslint-config-axuebin\nmodule.exports = ${JSON.stringify(
      eslintrcConfig(type),
      null,
      2
    )}`
  );

  const deps = needDeps.javascript;
  if (language === &amp;apos;typescript&amp;apos;) {
    deps.concat(needDeps.typescript);
  }
  if (frame) {
    deps.concat(needDeps[frame]);
  }

  spawn.sync(&amp;apos;npm&amp;apos;, [&amp;apos;install&amp;apos;, ...deps, &amp;apos;--save&amp;apos;], { stdio: &amp;apos;inherit&amp;apos; });
};&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;可运行的 CLI Demo 代码见：  &lt;a href="https://github.com/axuebin/axb-lint" rel="nofollow noreferrer"&gt;axb-lint&lt;/a&gt;，在项目目录下执行：  &lt;code&gt;axblint eslint&lt;/code&gt; 即可，如图：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814309" title="image.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h3&gt;自动化&lt;/h3&gt; &lt;p&gt;配置了 ESLint 之后，我们需要让开发者感知到 ESLint 的约束。开发者可以自己运行 eslint 命令来跑代码检查，这不够高效，所以我们需要一些自动化手段来做这个事情。当然 在开发时，编辑器也有提供相应的功能可以根据当前工作区下的 ESLint 配置文件来检查当前正在编辑的文件，这个不是我们关心的重点。&lt;/p&gt; &lt;p&gt;一般我们会在有以下几种方式做 ESLint 检查：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;开发时&lt;/strong&gt;：依赖编辑器的能力&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;手动运行&lt;/strong&gt;：在终端中手动执行 eslint 命令&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;pre-commit&lt;/strong&gt;：在提交 git 前自动执行 eslint 命令&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;ci&lt;/strong&gt;：依赖 git 的持续集成，可以将检查结果输出文件上传到服务器&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;这里提一下 pre-commit 的方案，在每一次本地开发完成提交代码前就做 ESLint 检查，保证云端的代码是统一规范的。&lt;/p&gt; &lt;p&gt;这种方式非常简单，只需要在项目中依赖   &lt;a href="https://www.npmjs.com/package/husky" rel="nofollow noreferrer"&gt;husky&lt;/a&gt; 和   &lt;a href="https://www.npmjs.com/package/lint-staged" rel="nofollow noreferrer"&gt;lint-staged&lt;/a&gt; 即可完成。安装好依赖之后，在 package.json 文件加入以下配置即可：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;{
  &amp;quot;lint-staged&amp;quot;: {
    &amp;quot;*.{js,jsx,ts,tsx}&amp;quot;: &amp;quot;eslint --cache --fix&amp;quot;
  },
  &amp;quot;husky&amp;quot;: {
    &amp;quot;hooks&amp;quot;: {
      &amp;quot;pre-commit&amp;quot;: &amp;quot;lint-staged&amp;quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;效果如图所示：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814311" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;如果代码跑 ESLint 检查抛了 Error 错误，则会中断 commit 流程：  &lt;br /&gt;  &lt;img alt="image.png" src="https://segmentfault.com/img/remote/1460000039814312" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;这样就可以确保提交到 GitHub 仓库上的代码是统一规范的。（当然，如果认为将这些配置文件都删了，那也是没办法的）&lt;/p&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;p&gt;本文介绍了 ESLint 在中小型前端团队的一些最佳实践的想法，大家可以在此基础上扩展，制订一套完善的 ESLint 工作流，落地到自己团队中。&lt;/p&gt; &lt;p&gt;本文是前端代码规范系列文章的其中一篇，后续还有关于 StyleLint/CommitLint/Prettier 等的文章，并且还有一篇  &lt;strong&gt;完整的关于前端代码规范工程化实践&lt;/strong&gt;的文章，敬请期待（也有可能就鸽了）。&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;更多原创文章欢迎关注公众号「  &lt;strong&gt;玩相机的程序员&lt;/strong&gt;」，或者加我微信 xb9207 交流&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript 代码规范 eslint 工程化</category>
      <guid isPermaLink="true">https://itindex.net/detail/61336-%E5%89%8D%E7%AB%AF-%E5%9B%A2%E9%98%9F-%E4%BB%A3%E7%A0%81</guid>
      <pubDate>Tue, 13 Apr 2021 10:14:23 CST</pubDate>
    </item>
    <item>
      <title>助力ssr，使用concent为nextjs应用加点料</title>
      <link>https://itindex.net/detail/61087-ssr-concent-nextjs</link>
      <description>&lt;p&gt;开源不易，感谢你的支持，  &lt;a href="https://github.com/concentjs/concent" rel="nofollow noreferrer"&gt;❤ star concent^_^&lt;/a&gt;  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544781" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;序言&lt;/h2&gt; &lt;p&gt;  &lt;code&gt;nextjs&lt;/code&gt;是一个非常流行的 React 服务端渲染应用框架，它很轻量，简单易上手，社区活跃，所以当我们使用  &lt;code&gt;react&lt;/code&gt;写一个需要  &lt;code&gt;ssr&lt;/code&gt;(server side render)的应用的话，基本都会首选  &lt;code&gt;nextjs&lt;/code&gt;，  &lt;code&gt;concent&lt;/code&gt;是一个新生代的  &lt;code&gt;react&lt;/code&gt;状态管理方案，它内置依赖收集系统，同时兼具有0入侵、可预测、渐进式、高性能的特点，并提供了  &lt;code&gt;lifecyle&lt;/code&gt;、  &lt;code&gt;composition api&lt;/code&gt;等灵活的api且写法超级简单，让你轻松驾驭超大规模的react应用。&lt;/p&gt; &lt;h2&gt;Hello next&lt;/h2&gt; &lt;p&gt;这里我们将使用  &lt;code&gt;create-next-app&lt;/code&gt;命令来安装一个基础的next示例应用&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;npx create-next-app hello-next&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行完毕后，可以看到一个如下的目录结构&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;|____public
|____pages
| |____ _app.js   // next应用默认的根组件
| |____index.js   // 默认首页
| |____api        // api路由文件
| | |____hello.js &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之后我们在项目根目录执行  &lt;code&gt;npm run dev&lt;/code&gt;将看到一个由  &lt;code&gt;next&lt;/code&gt;驱动的  &lt;code&gt;ssr&lt;/code&gt;默认首页  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544783" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;Hello concent&lt;/h2&gt; &lt;p&gt;这里我们将使用  &lt;code&gt;create-react-app&lt;/code&gt;命令来安装一个基础的concent示例应用&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;npx create-react-app hello-concent --template concent-ts&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;执行完毕后，可以看到一个如下的目录结构&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;|____index.tsx
|____App.tsx
|____types            // store的类型定义处
|____features           // 功能组件列表
| |____counter          // counter功能
| | |____Counter.tsx        // counter组件
| | |____model            // counter模型(包含state,reducer,computed)
|____models            // 其它全局通用的模型定义
|____configs&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;进入项目目录执行  &lt;code&gt;npm i&lt;/code&gt;，然后执行  &lt;code&gt;npm start&lt;/code&gt;即可看到一个默认的计数器页面  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544780" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;blockquote&gt;你也可以点击  &lt;a href="https://codesandbox.io/s/cra-template-concent-ts-2x347?file=/src/App.tsx" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;在线了解和编辑它。&lt;/blockquote&gt; &lt;p&gt;当然了在已有的项目里集成  &lt;code&gt;concent&lt;/code&gt;里也超级简单，因为它无需顶层提供  &lt;code&gt;Provider&lt;/code&gt;，只需要提前配置好模型即可。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { run } from &amp;apos;concent&amp;apos;;

run({ // 定义一个counter模型
  counter: {
    state: { num: 1, bigNum: 10 },
    reducer: {
      add(payload, moduleState) {
        return { num: moduleState + 1 };
      },
      async asyncAddBig() {
        await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000));
        return { bigNum: moduleState + 10 };
      }
    },
    computed: {
      doubleNum: ({ num }) =&amp;gt; num * 2, // 仅当num发生变化才触发此函数
    }
  }
})&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之后就可以全局即插即用啦，类组件和函数组件都可以用同样的方式去读取数据或调用方法，敲重点啦，  &lt;strong&gt;如果ui处是有条件语句控制是否要消费状态或衍生数据的话，推荐延迟解构的写法，这样可以让concent在每一轮渲染完毕后收集到视图对数据的最小粒度依赖&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// ###### 函数组件
function Demo(){
  // 如 state 和 moduleComputed 是按需读取的，推荐延迟解构的写法
  const { state: { num, numBig }, moduleComputed: { doubleNum }, mr } = useConcent(&amp;apos;counter&amp;apos;); 
  // ... ui 逻辑，绑数据、绑方法
}

// ###### 类组件
const DemoCls = register(&amp;apos;counter&amp;apos;)(
  class DemoCls extends React.Component{
   render(){
      const { state: { num, numBig }, moduleComputed: { doubleNum }, mr } = this.ctx; 
      // ... ui 逻辑，绑数据、绑方法
    }
  }
)&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;在next里引入concent&lt;/h2&gt; &lt;p&gt;next的基础示例目录里有个  &lt;code&gt;_app.js&lt;/code&gt;文件，它是next应用的根组件&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import &amp;apos;../styles/globals.css&amp;apos;

function MyApp({ Component, pageProps }) {
  return &amp;lt;Component {...pageProps} /&amp;gt;
}

export default MyApp&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;因使用  &lt;code&gt;concent&lt;/code&gt;之前必需提前配置好模型，所以我们只需提前创建一个  &lt;code&gt;runConcent.js&lt;/code&gt;文件&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import { run } from &amp;apos;concent&amp;apos;
import * as models from &amp;apos;./models&amp;apos;;

run(models);&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后在  &lt;code&gt;_app.js&lt;/code&gt;文件引入即可，这样根组件下的所有子组件都能够正确获取到store的数据和调动store的方法了。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import &amp;apos;../styles/globals.css&amp;apos;
+ import &amp;apos;./runConcent&amp;apos;

function MyApp({ Component, pageProps }) {
  return &amp;lt;Component {...pageProps} /&amp;gt;
}

export default MyApp&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;接着我们在next的pages目录下创建一个  &lt;code&gt;counter.js&lt;/code&gt;文件，代表这是一个页面组件，这样浏览器端可以用  &lt;code&gt;/counter&lt;/code&gt;路由来访问到这个组件的渲染视图了。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import React from &amp;apos;react&amp;apos;
import { useConcent } from &amp;apos;concent&amp;apos;
import router from &amp;apos;next/router&amp;apos;

// use next/router to do browser side router jump
function toHomePage(){
  router.push(&amp;apos;/&amp;apos;);
}

export default function Counter() {
  const { state, mr, moduleComputed } = useConcent(&amp;apos;home&amp;apos;)

  return (
    &amp;lt;div&amp;gt;
      this is counter page
      &amp;lt;h1&amp;gt;num: {state.num}&amp;lt;/h1&amp;gt;
      &amp;lt;h1&amp;gt;doubleNum: {moduleComputed.doubleNum}&amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={mr.add}&amp;gt;add&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={toHomePage}&amp;gt;to home page&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;大功告成，一个接入了  &lt;code&gt;concent&lt;/code&gt;的  &lt;code&gt;next&lt;/code&gt;应用就这样产生了，是不是特别简单呢？^_^&lt;/p&gt; &lt;p&gt;getServerSideProps 也是同样类似的做法来做哦，它们只是执行时机不同，getServerSideProps是每次请求页面都会执行，而getStaticProps是构建时执行。&lt;/p&gt; &lt;h2&gt;支持预渲染&lt;/h2&gt; &lt;p&gt;  &lt;code&gt;next&lt;/code&gt;提供两种级别的预渲染接口，即  &lt;code&gt;getServerSideProps&lt;/code&gt;和  &lt;code&gt;getStaticProps&lt;/code&gt;，两种的区别是执行时机不同，  &lt;code&gt;getServerSideProps&lt;/code&gt;是每次请求页面都会执行，而  &lt;code&gt;getStaticProps&lt;/code&gt;是构建时执行，我们先处理  &lt;code&gt;getServerSideProps&lt;/code&gt;这种情况吧，看看如何集合  &lt;code&gt;concent&lt;/code&gt;做预渲染支持。&lt;/p&gt; &lt;p&gt;首先我们不考虑  &lt;code&gt;concent&lt;/code&gt;的存在，在  &lt;code&gt;next&lt;/code&gt;里做预渲染支持，只需要在你的页面组件里暴露一个  &lt;code&gt;getServerSideProps&lt;/code&gt;接口即可。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// 此函数在每次请求改页面时被调用
export async function getServerSideProps() {
  // 调用外部 API 获取博文列表
  const res = await fetch(&amp;apos;https://.../posts&amp;apos;)
  const posts = await res.json()

  // 通过返回 { props: posts } 对象，PostPage 组件在渲染时将接收到 `posts` 参数
  return {
    props: { posts },
  }
}

function PostPage({ posts }) { // 这里接收到了 posts 参数
  // Render posts...
}

export default PostPage&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之所以  &lt;code&gt;Blog&lt;/code&gt;能够接到  &lt;code&gt;posts&lt;/code&gt;，除了暴露这个  &lt;code&gt;getServerSideProps&lt;/code&gt;这个接口之外，我们再观察一下  &lt;code&gt;_app.js&lt;/code&gt;这个根组件文件内容，可以发现关键点所在！&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;function MyApp({ Component, pageProps }) {
  return &amp;lt;Component {...pageProps} /&amp;gt;
}
export default MyApp&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;参数列表里的  &lt;code&gt;pageProps&lt;/code&gt;即是  &lt;code&gt;getServerSideProps&lt;/code&gt;返回结果里  &lt;code&gt;props&lt;/code&gt;指向的对象，然后  &lt;code&gt;next&lt;/code&gt;将其透传到目标页面组件上，所以我们才能够在  &lt;code&gt;PostPage&lt;/code&gt;参数列表里解构出  &lt;code&gt;posts&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;所以我们的切入点就可以从这里入手了，我们把getStaticProps的返回结果做一下格式约束，形如  &lt;code&gt;{module:string, state: object}&lt;/code&gt;这样的结构，然后在  &lt;code&gt;_app.js&lt;/code&gt;文件里记录到store即可&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;// 此函数在每次请求时被调用
export async function getServerSideProps() {
  // 调用外部 API 获取博文列表
  await delay();
  const posts = [
    { id: 1, name: &amp;apos;post1 -----&amp;apos; },
    { id: 2, name: &amp;apos;post2 --- welcome to use concent&amp;apos; },
  ];
  // 这个返回对象会透传给根组件的pageProps，在此返回状态所属的模块和状态实体对象
  // 在那里将状态记录到store
  return {
    props: {
      module: &amp;apos;test&amp;apos;,
      state: { posts },
    }
  };
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;此时的根组件文件改变如下&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;import &amp;apos;../styles/globals.css&amp;apos;;
+ import &amp;apos;./runConcent&amp;apos;;
+ import { setState } from &amp;apos;concent&amp;apos;;

function MyApp({ Component, pageProps }) {
  // 这里记录 getServerSideProps 的返回状态到store的对应模块
+  if (pageProps.module) {
+    setState(pageProps.module, pageProps.state);
+  }
  return &amp;lt;Component {...pageProps} /&amp;gt;
}
export default MyApp;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后我们实现的页面组件  &lt;code&gt;post-page&lt;/code&gt;代码如下&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;const PostList = React.memo(function () {
  const { state } = useConcent(&amp;apos;test&amp;apos;);
  return (
    &amp;lt;div&amp;gt;
      {state.posts.map(item =&amp;gt; &amp;lt;h3 key={item.id}&amp;gt;{item.name}&amp;lt;/h3&amp;gt;)}
    &amp;lt;/div&amp;gt;
  );
});

const PostLength = React.memo(function () {
  const { state } = useConcent(&amp;apos;test&amp;apos;);
  return &amp;lt;h1&amp;gt;{state.posts.length}&amp;lt;/h1&amp;gt;;
});

export default function PostPage() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;this is post page&amp;lt;/h1&amp;gt;
      &amp;lt;PostList /&amp;gt;
      &amp;lt;PostLength /&amp;gt;
      &amp;lt;button onClick={toHomePage}&amp;gt;to home page&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;接着我们打开浏览器访问  &lt;code&gt;/post-page&lt;/code&gt;页面吧，点击查看源码将会看到这是一个服务器端预渲染的页面  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544782" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;同理，我们也可将  &lt;code&gt;getServerSideProps&lt;/code&gt;替换为  &lt;code&gt;getStaticProps&lt;/code&gt;，上面的整个流程将依然正常工作，欢迎各位看官clone示例代码来亲自体验一下。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;git clone https://github.com/concentjs/ssr-demo-1&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;附录&lt;/h2&gt; &lt;h3&gt;doc&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://www.nextjs.cn/" rel="nofollow noreferrer"&gt;next-js doc&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://concentjs.github.io/concent-doc/" rel="nofollow noreferrer"&gt;concent doc&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;CloudBase CMS&lt;/h3&gt; &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544778" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;欢迎小哥哥们来撩  &lt;a href="https://github.com/TencentCloudBase/cloudbase-extension-cms" rel="nofollow noreferrer"&gt;CloudBase CMS&lt;/a&gt; ，打造一站式云端内容管理系统，它是云开发推出的，基于 Node.js 的 Headless 内容管理平台，提供了丰富的内容管理功能，安装简单，易于二次开发，并与云开发的生态体系紧密结合，助力开发者提升开发效率。&lt;/p&gt; &lt;blockquote&gt;  &lt;code&gt;concent&lt;/code&gt;已为其管理后台提供强力支持，新版的管理界面更加美观和体贴了。&lt;/blockquote&gt; &lt;h3&gt;FFCreator&lt;/h3&gt; &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000038544779" title=""&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;也欢迎小哥哥们来撩  &lt;a href="https://github.com/tnfe/FFCreator" rel="nofollow noreferrer"&gt;FFCreator&lt;/a&gt;，它是一个基于node.js的轻量、灵活的短视频加工库。您只需要添加几张图片或视频片段再加一段背景音乐，就可以快速生成一个很酷的视频短片。&lt;/p&gt; &lt;blockquote&gt;  &lt;code&gt;FFCreator&lt;/code&gt;是一种轻量又简单的解决方案，只需要很少的依赖和较低的机器配置就可以快速开始工作。并且它模拟实现了animate.css90%的动画效果，您可以轻松地把 web 页面端的动画效果转为视频，真的很给力。&lt;/blockquote&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>react.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/61087-ssr-concent-nextjs</guid>
      <pubDate>Sat, 19 Dec 2020 19:12:29 CST</pubDate>
    </item>
    <item>
      <title>RTSP？不存在的 -&gt; 前端实时流探索记</title>
      <link>https://itindex.net/detail/60709-rtsp-%E5%AD%98%E5%9C%A8-%E5%89%8D%E7%AB%AF</link>
      <description>&lt;p&gt;作为一个从未接触过实时流（直播流）的人，我之前对实时视频一直没有概念，而最近参与的项目刚好有视频监控的需求，在参与技术选型之前，我对前端实时流的展示进行了一下摸底。  &lt;br /&gt;  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;概览&lt;/h2&gt;
 &lt;p&gt;视频有一个流的概念，所以称流媒体。实时视频的流很好理解，因为视频是实时的，需要有一个地方不停地输出视频出来，所以整个视频可以用流来称呼。那么视频可否直接输出到前端页面上呢？可惜，如果可以的话，就没有我这篇文章了。现在摄像头的实时视频流普遍采用的是 RTSP 协议，而前端并不能直接播放 RTSP 的视频流。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;RTSP（Real-Time Stream Protocol），是 TCP/UDP 协议体系中的一个应用层协议，跟 HTTP 处在同一层。RTSP 在体系结构上位于 RTP 和RTCP 之上，它使用 TCP 或者 RTP 完成数据传输。RTSP 实时效果非常好，适合视频聊天、视频监控等方向。&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;br /&gt;那么我们就需要一层中间层，来将 RTSP 流转成前端可以支持的协议，这也引申出了目前实时流技术的几种方向：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;RTSP -&amp;gt; RTMP&lt;/li&gt;
  &lt;li&gt;RTSP -&amp;gt; HLS&lt;/li&gt;
  &lt;li&gt;RTSP -&amp;gt; RTMP -&amp;gt; HTTP-FLV&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://cdn.nlark.com/yuque/0/2020/png/171892/1592732437859-e583f2ef-a794-4f06-bb9f-68f8ae476683.png#align=left&amp;display=inline&amp;height=135&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=270&amp;originWidth=1272&amp;size=40442&amp;status=done&amp;style=none&amp;width=636" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;RTMP&lt;/h2&gt;
 &lt;p&gt;RTMP（Real Time Messaging Protocol）是属于 Adobe 的一套视频协议，这套方案需要专门的 RTMP 流媒体，并且如果想要在浏览器上播放，无法使用 HTML5 的 video 标签，只能使用 Flash 播放器。（通过使用 video.js@5.x 以下的版本可以做到用 video 标签进行播放，但仍然需要加载 Flash）。  &lt;strong&gt;它的实时性在几种方案中是最好的&lt;/strong&gt;，但是由于只能使用 Flash 的方案，所以在移动端就直接 GG 了，在 PC 端也是  &lt;a href="https://tech.sina.com.cn/digi/2020-06-18/doc-iircuyvi9075519.shtml" rel="nofollow noreferrer"&gt;明日黄花&lt;/a&gt;。  &lt;br /&gt;由于下面的两种方法也需要用到 RTMP，所以这里就展示一下 RTSP 流如何转换成 RTMP ，我们使用 ffmpeg+Nginx+nginx-rtmp-module 来做这件事：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;# 在 http 同一层配置 rtmp 协议的相关字段
rtmp {
    server {
          # 端口
        listen 1935;
            # 路径
        application test {
            # 开启实时流模式
            live on;
            record off;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;# bash 上执行 ffmpeg 把 rtsp 转成 rtmp，并推到 1935 这个端口上
ffmpeg -i &amp;quot;rtsp://xxx.xxx.xxx:xxx/1&amp;quot; -vcodec copy -acodec copy -f flv &amp;quot;rtmp://127.0.0.1:1935/live/&amp;quot;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这样我们就得到了一个 RTMP 的流，我们可以直接用 VLC 或者 IINA 来播放这个流。  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;HLS&lt;/h2&gt;
 &lt;p&gt;HLS（HTTP Live Streaming）是苹果公司提出的基于 HTTP 协议的的流媒体网络传输协议，它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载，每次只下载一些。HLS 具有跨平台性，支持 iOS/Android/浏览器，通用性强。但是它的实时性差：苹果官方建议是请求到3个片之后才开始播放。所以一般很少用 HLS 做为互联网直播的传输协议。假设列表里面的包含5个 ts 文件，每个 TS 文件包含5秒的视频内容，那么整体的延迟就是25秒。苹果官方推荐的小文件时长是 10s，所以这样就会有30s（n x 10）的延迟。  &lt;br /&gt;下面是 HLS 实时流的整个链路：  &lt;br /&gt;  &lt;img alt="image.png" src="https://cdn.nlark.com/yuque/0/2020/png/171892/1592746985691-39bb265e-5327-48f1-8c43-59116821d0d8.png#align=left&amp;display=inline&amp;height=281&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=562&amp;originWidth=800&amp;size=141493&amp;status=done&amp;style=none&amp;width=400" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;从图中可以看出来我们需要一个服务端作为编码器和流分割器，接受流并不断输出成流片段（stream），然后前端再通过一个索引文件，去访问这些流片段。那么我们同样可以使用 nginx+ffmpeg 来做这件事情。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;# 在 rtmp 的 server 下开启 hls
# 作为上图中的 Server，负责流的处理
application hls{
        live on;
        hls on;
         hls_path xxx/; #保存 hls 文件的文件夹
        hls_fragment 10s;
}&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;# 在 http 的 server 中添加 HLS 的配置:
# 作为上图中的 Distribution，负责分片文件和索引文件的输出
location /hls {
      # 提供 HLS 片段，声明类型
      types {
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
      }
      root /Users/mark/Desktop/hls; #访问切片文件保存的文件夹
      # Cache-Controll no-cache;
      expires -1;
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后同样使用 ffmpeg 推流到 hls 路径上：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;ffmpeg -i &amp;quot;rtsp://xxx.xxx.xxx:xxx/1&amp;quot; -vcodec copy -acodec copy -f flv rtmp://127.0.0.1:1935/hls&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这个时候可以看到文件夹里已经有许多流文件存在，且不停地更新：  &lt;br /&gt;  &lt;img alt="image.png" src="https://cdn.nlark.com/yuque/0/2020/png/171892/1592748990563-2ae7cbe0-6de3-4abf-8836-b48bb356f184.png#align=left&amp;display=inline&amp;height=176&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=352&amp;originWidth=832&amp;size=54983&amp;status=done&amp;style=none&amp;width=416" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;然后我们可以使用 video.js+video.js-contrib-hls 来播放这个视频：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;video&amp;lt;/title&amp;gt;
&amp;lt;!-- 引入css --&amp;gt;
&amp;lt;link href=&amp;quot;https://unpkg.com/video.js/dist/video-js.min.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;


&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&amp;quot;videoBox&amp;quot;&amp;gt;
    &amp;lt;video id=&amp;quot;video&amp;quot; class=&amp;quot;video-js vjs-default-skin&amp;quot; controls&amp;gt;
        &amp;lt;source src=&amp;quot;http://localhost:8080/hls/test.m3u8&amp;quot; type=&amp;quot;application/x-mpegURL&amp;quot;&amp;gt; 
    &amp;lt;/video&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&amp;lt;script src=&amp;quot;https://unpkg.com/video.js/dist/video.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&amp;quot;text/javascript&amp;quot; src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
videojs.options.flash.swf = &amp;quot;./videojs/video-js.swf&amp;quot;
        videojs(&amp;apos;video&amp;apos;, {&amp;quot;autoplay&amp;quot;:true}).play();
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在我的测试下，HLS 的延迟在10-20秒左右，我们可以通过调整切片的大小来减少延迟，但是由于架构的限制，延迟是一个不可忽视的问题。  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;HTTP-FLV&lt;/h2&gt;
 &lt;p&gt;接下来就是重头戏 HTTP-FLV 了，它集合了 HLS 的通用性和 RTMP 的实时性，可以做到在浏览器上用 HTML5 的 video 标签，以较低的延时播放实时流。HTTP-FLV 依靠 MIME 的特性，根据协议中的 Content-Type 来选择相应的程序去处理相应的内容，使得流媒体可以通过 HTTP 传输。除此之外，它可以通过 HTTP 302 跳转灵活调度/负载均衡，支持使用 HTTPS 加密传输，也能够兼容支持 Android，iOS 等移动端。  &lt;strong&gt;HTTP-FLV 本质上是将流转成 HTTP 协议下的 flv 文件，在 Nginx 上我们可以使用 nginx-http-flv-module 来将 RTMP 流转成 HTTP 流。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;其实 flv 格式依然是 Adobe 家的格式，原生 Video 标签无法直接播放，但是好在我们有 bilibili 家的 flv.js，它可以将 FLV 文件流转码复用成 ISO BMFF（MP4 碎片）片段，然后通过 Media Source Extensions 将 MP4 片段喂进浏览器。  &lt;br /&gt;  &lt;br /&gt;  &lt;br /&gt;在支持浏览器的协议里，延迟排序是这样的：  &lt;strong&gt;RTMP = HTTP-FLV = WebSocket-FLV &amp;lt; HLS&lt;/strong&gt;  &lt;br /&gt;而性能排序是这样的：  &lt;strong&gt;RTMP &amp;gt; HTTP-FLV = WebSocket-FLV &amp;gt; HLS&lt;/strong&gt;  &lt;br /&gt;  &lt;br /&gt;  &lt;br /&gt;说了这么多，不如直接上手看看吧：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;首先我们需要一个新的 nginx 插件：   &lt;a href="https://github.com/winshining/nginx-http-flv-module" rel="nofollow noreferrer"&gt;nginx-http-flv-module&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;在 nginx.conf 中进行一些新的配置：&lt;/li&gt;
&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;# rtmp server
application myvideo {
      live on;
      gop_cache: on; #减少首屏等待时间
}

# http server
location /live {
      flv_live on;
}&lt;/code&gt;&lt;/pre&gt;
 &lt;ol&gt;
  &lt;li&gt;依然用 ffmpeg 来推流，使用上面 RTMP 的命令&lt;/li&gt;
  &lt;li&gt;前端 import flv.js，然后使用它来播放&lt;/li&gt;
&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;// 前端使用 flv.js，开启实时模式，然后访问这个 nginx 地址下的路径即可
import flvJs from &amp;apos;flv.js&amp;apos;;

export function playVideo(elementId, src) {
  const videoElement = document.getElementById(elementId);
  const flvPlayer = flvJs.createPlayer({
    isLive: true,
    type: &amp;apos;flv&amp;apos;,
    url: src,
  });
  flvPlayer.attachMediaElement(videoElement);
  flvPlayer.load();
}

playVideo(&amp;apos;#video&amp;apos;, &amp;apos;http://localhost:8080/live?port=1985&amp;amp;app=myvideo&amp;amp;stream=streamname&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以看到 flv.js 使用了   &lt;code&gt;video/x-flv&lt;/code&gt; 这个 MIME 返回数据。  &lt;br /&gt;  &lt;img alt="image.png" src="https://cdn.nlark.com/yuque/0/2020/png/171892/1592795056450-96a46d94-7f4d-4c81-8225-647f60dc9ca4.png#align=left&amp;display=inline&amp;height=403&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=806&amp;originWidth=1184&amp;size=144529&amp;status=done&amp;style=none&amp;width=592" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;如果对延迟有更高的要求，可以尝试下面的操作：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;可以配置 flv.js 的    &lt;code&gt;enableStashBuffer&lt;/code&gt; 字段，它是 flv.js 用于控制缓存 buffer 的开关，关闭了之后可以做到最小延迟，但由于没有缓存，可能会看到网络抖动带来的视频卡顿。&lt;/li&gt;
  &lt;li&gt;可以尝试关闭 nginx 的 http 配置里的    &lt;code&gt;gop_cache&lt;/code&gt; 。   &lt;code&gt;gop_cache&lt;/code&gt; 又称关键帧缓存，其意义是控制视频的关键帧之间的缓存是否开启。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;这里引入了一个关键帧的概念：我们使用最广泛的   &lt;code&gt;H.264&lt;/code&gt; 视频压缩格式，它采用了诸如帧内预测压缩/帧间预测压缩等压缩方案，最后得到了 BPI 三种帧：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;I 帧：关键帧，采用帧内压缩技术。&lt;/li&gt;
  &lt;li&gt;P 帧：向前参考帧，在压缩时，只参考前面已经处理的帧，表示的是当前帧画面与前一帧（前一帧可能是 I 帧也可能是 P 帧）的差别。采用帧间压缩技术。&lt;/li&gt;
  &lt;li&gt;B 帧：双向参考帧，在压缩时，它即参考前面的帧，又参考它后面的帧。B 帧记录的是本帧与前后帧的差别。采用帧间压缩技术。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;带有 I 帧、B 帧和 P 帧的典型视频序列。P 帧只需要参考前面的 I 帧或 P 帧，而 B 帧则需要同时参考前面和后面的 I 帧或 P 帧。由于 P/B 帧对于 I 帧都有直接或者间接的依赖关系，所以播放器要解码一个视频帧序列，并进行播放，必须首先解码出 I 帧。假设 GOP（就是视频流中两个I帧的时间距离） 是 10 秒，也就是每隔 10 秒才有关键帧，如果用户在第 5 秒时开始播放，就无法拿到当前的关键帧了。这个时候 gop_cache 就起作用了：  &lt;strong&gt;gop_cache 可以控制是否缓存最近的一个关键帧。&lt;/strong&gt;开启 gop_cache 可以让客户端开始播放时，立即收到一个关键帧，显示出画面，当然，由于增加了对上一个帧的缓存，所以延时自然就变大了。如果对延时有更高的要求，而对于首屏时间/播放流畅度的要求没那么高的话，那么可以尝试关闭 gop_cache，来达到低延时的效果。  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;思考&lt;/h2&gt;
 &lt;p&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h3&gt;延迟与卡顿&lt;/h3&gt;
 &lt;p&gt;实时视频的延时与卡顿是视频质量中最重要的两项指标。 然而，这两项指标从理论上来说，是一对矛盾的关系——需要更低的延时，则表明服务器端和播放端的缓冲区都必须更短，来自网络的异常抖动容易引起卡顿；业务可以接受较高的延时时，服务端和播放端都可以有较长的缓冲区，以应对来自网络的抖动，提供更流畅的体验。  &lt;br /&gt;  &lt;a&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;h3&gt;直播厂商是怎么做的？&lt;/h3&gt;
 &lt;p&gt;现在各个直播平台基本上都放弃了以上这些比较传统的方式，使用了云服务商提供的 CDN，但还是离不开前文所说的几种协议与方式。如下图是阿里云的直播服务图。可以看到其流程大概分为这几步：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;采集视频流（主播端使用 RTMP 进行推流）&lt;/li&gt;
  &lt;li&gt;推流到 CDN 节点（上传流）&lt;/li&gt;
  &lt;li&gt;CDN 节点转到直播中心，直播中心类似于强大的具有计算能力的中间源，可以提供额外服务诸如落存（录制/录制到云存储/点播），转码，审核，多种协议的输出等。&lt;/li&gt;
  &lt;li&gt;直播中间分发到 CDN 节点&lt;/li&gt;
  &lt;li&gt;播放（阿里云支持 RTMP、FLV 及 HLS 三种播流协议）&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://cdn.nlark.com/yuque/0/2020/png/171892/1592800884273-dec0f614-aed6-4e8a-8857-5bbd48185c18.png#align=left&amp;display=inline&amp;height=394&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=480&amp;originWidth=919&amp;size=62855&amp;status=done&amp;style=none&amp;width=755" title="image.png"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt;
 &lt;p&gt;PS：如果你已经看到这儿了，觉得我写得还行的话，麻烦给个赞，谢谢！&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端 nginx rtsp javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/60709-rtsp-%E5%AD%98%E5%9C%A8-%E5%89%8D%E7%AB%AF</guid>
      <pubDate>Mon, 22 Jun 2020 13:10:42 CST</pubDate>
    </item>
    <item>
      <title>GitHub 上周 JavaScript 趋势榜项目</title>
      <link>https://itindex.net/detail/60607-github-javascript-%E8%B6%8B%E5%8A%BF</link>
      <description>&lt;p&gt;  &lt;img alt="header.png" src="https://segmentfault.com/img/bVbHjsw" title="header.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;1. yemount/pose-animator&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/yemount/pose-animator" rel="nofollow noreferrer"&gt;https://github.com/yemount/pose-animator&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:4237 | forks:354 | 2117 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;Pose Animator拍摄2D矢量图，并基于PoseNet和FaceMesh的识别结果实时对其包含的曲线进行动画处理。 它从计算机图形学中借鉴了基于骨骼的动画的思想，并将其应用于矢量字符。&lt;/p&gt;
 &lt;h2&gt;2. renrenio/renren-fast-vue&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/renrenio/renren-fast-vue" rel="nofollow noreferrer"&gt;https://github.com/renrenio/renren-fast-vue&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:2023 | forks:1062 | 132 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;renren-fast-vue基于vue、element-ui构建开发，实现renren-fast后台管理前端功能，提供一套更优的前端解决方案。&lt;/p&gt;
 &lt;h2&gt;3. zeit/next.js&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/zeit/next.js" rel="nofollow noreferrer"&gt;https://github.com/zeit/next.js&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:48399 | forks:7232 | 483 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;React SSR 框架&lt;/p&gt;
 &lt;h2&gt;4. discordjs/discord.js&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/discordjs/discord.js" rel="nofollow noreferrer"&gt;https://github.com/discordjs/discord.js&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:5791 | forks:1447 | 84 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;强大的 JavaScript 库，可与 Discord API 进行交互&lt;/p&gt;
 &lt;h2&gt;5. openspug/spug&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/openspug/spug" rel="nofollow noreferrer"&gt;https://github.com/openspug/spug&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:2383 | forks:534 | 476 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;开源运维平台：面向中小型企业设计的轻量级无Agent的自动化运维平台，整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。&lt;/p&gt;
 &lt;h2&gt;6. ccxt/ccxt&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/ccxt/ccxt" rel="nofollow noreferrer"&gt;https://github.com/ccxt/ccxt&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:13828 | forks:3889 | 88 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;JavaScript / Python / PHP加密货币交易API，支持超过120个 比特币 / 数字货币 交换&lt;/p&gt;
 &lt;h2&gt;7. Advanced-Frontend/Daily-Interview-Question&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/Advanced-Frontend/Daily-Interview-Question" rel="nofollow noreferrer"&gt;https://github.com/Advanced-Frontend/Daily-Interview-Question&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:18641 | forks:2284 | 207 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;我是木易杨，公众号「高级前端进阶」作者，每天搞定一道前端大厂面试题，祝大家天天进步，一年后会看到不一样的自己。&lt;/p&gt;
 &lt;h2&gt;8. quasarframework/quasar&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/quasarframework/quasar" rel="nofollow noreferrer"&gt;https://github.com/quasarframework/quasar&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:14653 | forks:1667 | 77 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;Quasar Framework-在极短时间内构建高性能 VueJS 用户界面&lt;/p&gt;
 &lt;h2&gt;9. denysdovhan/wtfjs&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/denysdovhan/wtfjs" rel="nofollow noreferrer"&gt;https://github.com/denysdovhan/wtfjs&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:17262 | forks:1140 | 193 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;有趣又棘手的 JavaScript 示例列表&lt;/p&gt;
 &lt;h2&gt;10. YMFE/yapi&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/YMFE/yapi" rel="nofollow noreferrer"&gt;https://github.com/YMFE/yapi&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:15486 | forks:2619 | 310 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;YApi 是一个可本地部署的、打通前后端及QA的、可视化的接口管理平台&lt;/p&gt;
 &lt;h2&gt;11. agalwood/Motrix&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/agalwood/Motrix" rel="nofollow noreferrer"&gt;https://github.com/agalwood/Motrix&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:21052 | forks:2625 | 272 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;一个全功能的下载管理器。&lt;/p&gt;
 &lt;h2&gt;12. jgraph/drawio-desktop&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/jgraph/drawio-desktop" rel="nofollow noreferrer"&gt;https://github.com/jgraph/drawio-desktop&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:7603 | forks:811 | 322 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;drawio 桌面版&lt;/p&gt;
 &lt;h2&gt;13. facebook/react&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/facebook/react" rel="nofollow noreferrer"&gt;https://github.com/facebook/react&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:148832 | forks:28875 | 514 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;大名鼎鼎的 React 框架， 用于构建用户界面的声明性，高效且灵活的JavaScript库。&lt;/p&gt;
 &lt;h2&gt;14. iamadamdev/bypass-paywalls-chrome&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/iamadamdev/bypass-paywalls-chrome" rel="nofollow noreferrer"&gt;https://github.com/iamadamdev/bypass-paywalls-chrome&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:4799 | forks:359 | 198 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;绕过付费墙 web 浏览器扩展&lt;/p&gt;
 &lt;h2&gt;15. cypress-io/cypress&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/cypress-io/cypress" rel="nofollow noreferrer"&gt;https://github.com/cypress-io/cypress&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:20222 | forks:1213 | 227 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;对浏览器中运行的所有内容进行快速，轻松和可靠的测试。&lt;/p&gt;
 &lt;h2&gt;16. jgraph/drawio&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/jgraph/drawio" rel="nofollow noreferrer"&gt;https://github.com/jgraph/drawio&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:16966 | forks:3550 | 296 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;diagrams.net（以前为draw.io）是一个在线图表绘制网站，在此项目中提供了源代码。&lt;/p&gt;
 &lt;h2&gt;17. twbs/bootstrap&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/twbs/bootstrap" rel="nofollow noreferrer"&gt;https://github.com/twbs/bootstrap&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:140844 | forks:68838 | 281 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;最受欢迎的HTML，CSS和JavaScript框架，用于在网络上开发响应式，移动优先项目。&lt;/p&gt;
 &lt;h2&gt;18. mui-org/material-ui&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/mui-org/material-ui" rel="nofollow noreferrer"&gt;https://github.com/mui-org/material-ui&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:57433 | forks:16064 | 276 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;React UI 库，组件可以更快，更轻松地进行Web开发。 建立自己的设计系统，或从材料设计开始。&lt;/p&gt;
 &lt;h2&gt;19. gatsbyjs/gatsby&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/gatsbyjs/gatsby" rel="nofollow noreferrer"&gt;https://github.com/gatsbyjs/gatsby&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:44405 | forks:8044 | 228 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;使用React构建快速，现代化的应用程序和网站&lt;/p&gt;
 &lt;h2&gt;20. microsoft/playwright&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/microsoft/playwright" rel="nofollow noreferrer"&gt;https://github.com/microsoft/playwright&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:12773 | forks:370 | 408 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;节点库可通过单个API自动化Chromium，Firefox和WebKit&lt;/p&gt;
 &lt;h2&gt;21. MarkerHub/eblog&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/MarkerHub/eblog" rel="nofollow noreferrer"&gt;https://github.com/MarkerHub/eblog&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:448 | forks:149 | 100 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;eblog是一个基于Springboot2.1.2开发的博客学习项目，为了让项目融合更多的知识点，达到学习目的，编写了详细的从0到1开发文档。主要学习包括：自定义Freemarker标签，使用shiro+redis完成了会话共享，redis的zset结构完成本周热议排行榜，t-io+websocket完成即时消息通知和群聊，rabbitmq+elasticsearch完成博客内容搜索引擎等。值得学习的地方很多！&lt;/p&gt;
 &lt;h2&gt;22. goldbergyoni/nodebestpractices&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/goldbergyoni/nodebestpractices" rel="nofollow noreferrer"&gt;https://github.com/goldbergyoni/nodebestpractices&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:43891 | forks:3974 | 293 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;Node.js最佳实践列表（2020年5月）&lt;/p&gt;
 &lt;h2&gt;23. grommet/grommet&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/grommet/grommet" rel="nofollow noreferrer"&gt;https://github.com/grommet/grommet&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:6300 | forks:738 | 40 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;基于react的框架，可在整齐的程序包中提供可访问性，模块化，响应性和主题化&lt;/p&gt;
 &lt;h2&gt;24. egonSchiele/grokking_algorithms&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/egonSchiele/grokking_algorithms" rel="nofollow noreferrer"&gt;https://github.com/egonSchiele/grokking_algorithms&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:4004 | forks:1558 | 49 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;《 Grokking算法》源码&lt;/p&gt;
 &lt;h2&gt;25. jonasschmedtmann/complete-javascript-course&lt;/h2&gt;
 &lt;p&gt;项目地址：  &lt;a href="https://github.com/jonasschmedtmann/complete-javascript-course" rel="nofollow noreferrer"&gt;https://github.com/jonasschmedtmann/complete-javascript-course&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;⭐stars:3629 | forks:5226 | 54 ⭐stars this week&lt;/p&gt;
 &lt;p&gt;完整 JavaScript 课程的入门文件，最终项目和常见问题解答&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>github 前端 javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/60607-github-javascript-%E8%B6%8B%E5%8A%BF</guid>
      <pubDate>Mon, 18 May 2020 17:55:40 CST</pubDate>
    </item>
    <item>
      <title>vue父子组件通信高级用法</title>
      <link>https://itindex.net/detail/59984-vue-%E7%88%B6%E5%AD%90-%E9%80%9A%E4%BF%A1</link>
      <description>&lt;p&gt;  &lt;strong&gt;vue项目的一大亮点就是组件化。使用组件可以极大地提高项目中代码的复用率，减少代码量。但是使用组件最大的难点就是父子组件之间的通信。&lt;/strong&gt;&lt;/p&gt;
 &lt;h1&gt;子通信父&lt;/h1&gt;
 &lt;h2&gt;父组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot;&amp;gt;
    我是父组件
    &amp;lt;!--父组件监听子组件触发的say方法，调用自己的parentSay方法--&amp;gt;
    &amp;lt;!--通过:msg将父组件的数据传递给子组件--&amp;gt;
    &amp;lt;children :msg=&amp;quot;msg&amp;quot; @say=&amp;quot;parentSay&amp;quot;&amp;gt;&amp;lt;/children&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import Children from &amp;apos;./children.vue&amp;apos;
export default {
  data () {
    return {
      msg: &amp;apos;hello, children&amp;apos;
    }
  },
  methods: {
      // 参数就是子组件传递出来的数据
      parentSay(msg){
          console.log(msg) // hello, parent
      }
  },

  // 引入子组件
  components:{
      children: Children
  }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;子组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hello&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;children&amp;quot; @click=&amp;quot;say&amp;quot;&amp;gt;
      我是子组件
      &amp;lt;div&amp;gt;
        父组件对我说：{{msg}}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;

  export default {
      //父组件通过props属性传递进来的数据
      props: {
          msg: {
              type: String,
              default: () =&amp;gt; {
                  return &amp;apos;&amp;apos;
              }
          }
      },
      data () {
        return {
            childrenSay: &amp;apos;hello, parent&amp;apos;
        }
      },

      methods: {
          // 子组件通过emit方法触发父组件中定义好的函数，从而将子组件中的数据传递给父组件
          say(){
              this.$emit(&amp;apos;say&amp;apos; , this.childrenSay);
          }
      }
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;子组件使用$emit方法调用父组件的方法，达到子通信父的目的。&lt;/p&gt;
 &lt;h1&gt;父通信子&lt;/h1&gt;
 &lt;h2&gt;父组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt; &amp;lt;!--Html--&amp;gt;
&amp;lt;template&amp;gt;
   &amp;lt;!--父组件触发click方法--&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot; @click=&amp;quot;say&amp;quot;&amp;gt;
    我是父组件
    &amp;lt;!--通过ref标记子组件--&amp;gt;
    &amp;lt;children ref=&amp;quot;child&amp;quot;&amp;gt;&amp;lt;/children&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import Children from &amp;apos;./children.vue&amp;apos;
export default {
  data () {
    return {
        msg: &amp;quot;hello,my son&amp;quot;
    }
  },
  methods: {
      // 通过$refs调用子组件的parentSay方法
      say(){
         this.$refs.child.parentSay(this.msg);
      }
  },

  // 引入子组件
  components:{
      children: Children
  }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;子组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hello&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;children&amp;quot; &amp;gt;
      我是子组件
      &amp;lt;div&amp;gt;
        父组件对我说：{{msg}}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;

  export default {
      data () {
        return {
            msg: &amp;apos;&amp;apos;
        }
      },

      methods: {
          // 父组件调用的JavaScript方法parentSay
          parentSay(msg){
              this.msg = msg;
          }
      }
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;父组件通过  &lt;code&gt;$refs&lt;/code&gt;调用子组件的方法。  &lt;br /&gt;以上就是父子组件通信的方式，父子组件传递数据直接用props，或者使用  &lt;code&gt;$emit&lt;/code&gt;和  &lt;code&gt;$refs&lt;/code&gt;依靠事件来传递数据。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;父子组件通信提升篇&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;上文中，子通信父是在  &lt;strong&gt;子中触发点击事件&lt;/strong&gt;然后调用父组件的方法，父通信子是在  &lt;strong&gt;父中触发点击事件&lt;/strong&gt;调用子组件的方法。但是实际情况中可能存在  &lt;strong&gt;子通信父时子组件不允许有点击事件&lt;/strong&gt;而事件在父中或者  &lt;strong&gt;父通信子时点击事件在子组件&lt;/strong&gt;中。&lt;/p&gt;
 &lt;h1&gt;子通信父时击事件在父组件&lt;/h1&gt;
 &lt;p&gt;这种情况其实很常见，例如提交一个表单时表单的内容为子组件，而保存按钮在父组件上。此时点击保存按钮想要获取子组件表单的值。这种情况下已经不单单是子通信父和父通信子了，需要将两者结合在一起使用才能完成整个通信过程。&lt;/p&gt;
 &lt;p&gt;实现的思路是在父组件中点击事件时，先通过父子通信调用子组件的方法，然后在子组件中的该方法里使用子父通信调用父组件的另一个方法并将信息传递回来。以下是代码演示：&lt;/p&gt;
 &lt;h2&gt;父组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot; @click=&amp;quot;getData&amp;quot;&amp;gt;
    我是父组件
    &amp;lt;!--父组件监听子组件触发的transData方法，调用自己的transData方法--&amp;gt;
    &amp;lt;!--通过ref标记子组件--&amp;gt;
    &amp;lt;children ref=&amp;quot;child&amp;quot; @transData=&amp;quot;transData&amp;quot;&amp;gt;&amp;lt;/children&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import Children from &amp;apos;./children.vue&amp;apos;
export default {
  data () {
    return {
      msg: &amp;apos;hello, children&amp;apos;
    }
  },
  methods: {
      getData(){
          // 调用子组件的getData方法
          this.$refs.child.getData();
      },
      // 参数就是子组件传递出来的数据
      transData(msg){
          console.log(msg) // hello, parent
      }
  },

  // 引入子组件
  components:{
      children: Children
  }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;子组件&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hello&amp;quot;&amp;gt;
    &amp;lt;div class=&amp;quot;children&amp;quot; &amp;gt;
      我是子组件
      &amp;lt;div&amp;gt;
        子组件的数据：{{childrenSay}}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;

  export default {
      data () {
        return {
            childrenSay: &amp;apos;hello, parent&amp;apos;
        }
      },
      methods: {
          // 子组件通过emit方法触发父组件中定义好的函数，从而将子组件中的数据传递给父组件
          getData() {
              this.$emit(&amp;apos;transData&amp;apos; , this.childrenSay);
          }
      }
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;另一种情况思路也和这个一样，基础就在与父通信子和子通信父的灵活运用。  &lt;br /&gt;  &lt;strong&gt;转评赞就是最大的鼓励&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>通信 vue.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59984-vue-%E7%88%B6%E5%AD%90-%E9%80%9A%E4%BF%A1</guid>
      <pubDate>Tue, 27 Aug 2019 10:52:02 CST</pubDate>
    </item>
    <item>
      <title>Web Components 入门实例教程</title>
      <link>https://itindex.net/detail/59920-web-components-%E5%AE%9E%E4%BE%8B</link>
      <description>&lt;p&gt;组件是前端的发展方向，现在流行的 React 和 Vue 都是组件框架。&lt;/p&gt;
 &lt;p&gt;谷歌公司由于掌握了 Chrome 浏览器，一直在推动浏览器的原生组件，即   &lt;a href="https://www.webcomponents.org/introduction"&gt;Web Components API&lt;/a&gt;。相比第三方框架，原生组件简单直接，符合直觉，不用加载任何外部模块，代码量小。目前，它还在不断发展，但已经可用于生产环境。&lt;/p&gt;

 &lt;p&gt;Web Components API 内容很多，本文不是全面的教程，只是一个简单演示，让大家看一下怎么用它开发组件。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201908/bg2019080604.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;一、自定义元素&lt;/h2&gt;

 &lt;p&gt;下图是一个用户卡片。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201908/bg2019080405.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;本文演示如何把这个卡片，写成 Web Components 组件，这里是最后的  &lt;a href="https://jsbin.com/yobopor/1/edit?html,js,output"&gt;完整代码&lt;/a&gt;。&lt;/p&gt;

 &lt;p&gt;网页只要插入下面的代码，就会显示用户卡片。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;user-card&amp;gt;&amp;lt;/user-card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;这种自定义的 HTML 标签，称为自定义元素（custom element）。根据规范，自定义元素的名称必须包含连词线，用与区别原生的 HTML 元素。所以，  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;不能写成  &lt;code&gt;&amp;lt;usercard&amp;gt;&lt;/code&gt;。&lt;/p&gt;

 &lt;h2&gt;二、  &lt;code&gt;customElements.define()&lt;/code&gt;&lt;/h2&gt;

 &lt;p&gt;自定义元素需要使用 JavaScript 定义一个类，所有  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;都会是这个类的实例。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
class UserCard extends HTMLElement {
  constructor() {
    super();
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，  &lt;code&gt;UserCard&lt;/code&gt;就是自定义元素的类。注意，这个类的父类是  &lt;code&gt;HTMLElement&lt;/code&gt;，因此继承了 HTML 元素的特性。&lt;/p&gt;

 &lt;p&gt;接着，使用浏览器原生的  &lt;code&gt;customElements.define()&lt;/code&gt;方法，告诉浏览器  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;元素与这个类关联。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
window.customElements.define(&amp;apos;user-card&amp;apos;, UserCard);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;h2&gt;三、自定义元素的内容&lt;/h2&gt;

 &lt;p&gt;自定义元素  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;目前还是空的，下面在类里面给出这个元素的内容。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
class UserCard extends HTMLElement {
  constructor() {
    super();

    var image = document.createElement(&amp;apos;img&amp;apos;);
    image.src = &amp;apos;https://semantic-ui.com/images/avatar2/large/kristy.png&amp;apos;;
    image.classList.add(&amp;apos;image&amp;apos;);

    var container = document.createElement(&amp;apos;div&amp;apos;);
    container.classList.add(&amp;apos;container&amp;apos;);

    var name = document.createElement(&amp;apos;p&amp;apos;);
    name.classList.add(&amp;apos;name&amp;apos;);
    name.innerText = &amp;apos;User Name&amp;apos;;

    var email = document.createElement(&amp;apos;p&amp;apos;);
    email.classList.add(&amp;apos;email&amp;apos;);
    email.innerText = &amp;apos;yourmail@some-email.com&amp;apos;;

    var button = document.createElement(&amp;apos;button&amp;apos;);
    button.classList.add(&amp;apos;button&amp;apos;);
    button.innerText = &amp;apos;Follow&amp;apos;;

    container.append(name, email, button);
    this.append(image, container);
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码最后一行，  &lt;code&gt;this.append()&lt;/code&gt;的  &lt;code&gt;this&lt;/code&gt;表示自定义元素实例。&lt;/p&gt;

 &lt;p&gt;完成这一步以后，自定义元素内部的 DOM 结构就已经生成了。&lt;/p&gt;

 &lt;h2&gt;四、  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;标签&lt;/h2&gt;

 &lt;p&gt;使用 JavaScript 写上一节的 DOM 结构很麻烦，Web Components API 提供了  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;标签，可以在它里面使用 HTML 定义 DOM。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;template id=&amp;quot;userCardTemplate&amp;quot;&amp;gt;
  &amp;lt;img src=&amp;quot;https://semantic-ui.com/images/avatar2/large/kristy.png&amp;quot; class=&amp;quot;image&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
    &amp;lt;p class=&amp;quot;name&amp;quot;&amp;gt;User Name&amp;lt;/p&amp;gt;
    &amp;lt;p class=&amp;quot;email&amp;quot;&amp;gt;yourmail@some-email.com&amp;lt;/p&amp;gt;
    &amp;lt;button class=&amp;quot;button&amp;quot;&amp;gt;Follow&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;然后，改写一下自定义元素的类，为自定义元素加载  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById(&amp;apos;userCardTemplate&amp;apos;);
    var content = templateElem.content.cloneNode(true);
    this.appendChild(content);
  }
}  
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，获取  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;节点以后，克隆了它的所有子元素，这是因为可能有多个自定义元素的实例，这个模板还要留给其他实例使用，所以不能直接移动它的子元素。&lt;/p&gt;

 &lt;p&gt;到这一步为止，完整的代码如下。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;body&amp;gt;
  &amp;lt;user-card&amp;gt;&amp;lt;/user-card&amp;gt;
  &amp;lt;template&amp;gt;...&amp;lt;/template&amp;gt;

  &amp;lt;script&amp;gt;
    class UserCard extends HTMLElement {
      constructor() {
        super();

        var templateElem = document.getElementById(&amp;apos;userCardTemplate&amp;apos;);
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
      }
    }
    window.customElements.define(&amp;apos;user-card&amp;apos;, UserCard);    
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;h2&gt;五、添加样式&lt;/h2&gt;

 &lt;p&gt;自定义元素还没有样式，可以给它指定全局样式，比如下面这样。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
user-card {
  /* ... */
}
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;但是，组件的样式应该与代码封装在一起，只对自定义元素生效，不影响外部的全局样式。所以，可以把样式写在  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;里面。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;template id=&amp;quot;userCardTemplate&amp;quot;&amp;gt;
  &amp;lt;style&amp;gt;
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #d4d4d4;
     border: 1px solid #d5d5d5;
     box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
     border-radius: 3px;
     overflow: hidden;
     padding: 10px;
     box-sizing: border-box;
     font-family: &amp;apos;Poppins&amp;apos;, sans-serif;
   }
   .image {
     flex: 0 0 auto;
     width: 160px;
     height: 160px;
     vertical-align: middle;
     border-radius: 5px;
   }
   .container {
     box-sizing: border-box;
     padding: 20px;
     height: 160px;
   }
   .container &amp;gt; .name {
     font-size: 20px;
     font-weight: 600;
     line-height: 1;
     margin: 0;
     margin-bottom: 5px;
   }
   .container &amp;gt; .email {
     font-size: 12px;
     opacity: 0.75;
     line-height: 1;
     margin: 0;
     margin-bottom: 15px;
   }
   .container &amp;gt; .button {
     padding: 10px 25px;
     font-size: 12px;
     border-radius: 5px;
     text-transform: uppercase;
   }
  &amp;lt;/style&amp;gt;

  &amp;lt;img src=&amp;quot;https://semantic-ui.com/images/avatar2/large/kristy.png&amp;quot; class=&amp;quot;image&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
    &amp;lt;p class=&amp;quot;name&amp;quot;&amp;gt;User Name&amp;lt;/p&amp;gt;
    &amp;lt;p class=&amp;quot;email&amp;quot;&amp;gt;yourmail@some-email.com&amp;lt;/p&amp;gt;
    &amp;lt;button class=&amp;quot;button&amp;quot;&amp;gt;Follow&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;样式里面的  &lt;code&gt;:host&lt;/code&gt;伪类，指代自定义元素本身。&lt;/p&gt;

 &lt;h2&gt;六、自定义元素的参数&lt;/h2&gt;

 &lt;p&gt;  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;内容现在是在  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;里面设定的，为了方便使用，把它改成参数。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;user-card
  image=&amp;quot;https://semantic-ui.com/images/avatar2/large/kristy.png&amp;quot;
  name=&amp;quot;User Name&amp;quot;
  email=&amp;quot;yourmail@some-email.com&amp;quot;
&amp;gt;&amp;lt;/user-card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;代码也相应改造。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
&amp;lt;template id=&amp;quot;userCardTemplate&amp;quot;&amp;gt;
  &amp;lt;style&amp;gt;...&amp;lt;/style&amp;gt;

  &amp;lt;img class=&amp;quot;image&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;
    &amp;lt;p class=&amp;quot;name&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p class=&amp;quot;email&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;button class=&amp;quot;button&amp;quot;&amp;gt;Follow John&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;最后，改一下类的代码，把参数加到自定义元素里面。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById(&amp;apos;userCardTemplate&amp;apos;);
    var content = templateElem.content.cloneNode(true);
    content.querySelector(&amp;apos;img&amp;apos;).setAttribute(&amp;apos;src&amp;apos;, this.getAttribute(&amp;apos;image&amp;apos;));
    content.querySelector(&amp;apos;.container&amp;gt;.name&amp;apos;).innerText = this.getAttribute(&amp;apos;name&amp;apos;);
    content.querySelector(&amp;apos;.container&amp;gt;.email&amp;apos;).innerText = this.getAttribute(&amp;apos;email&amp;apos;);
    this.appendChild(content);
  }
}
window.customElements.define(&amp;apos;user-card&amp;apos;, UserCard);    
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;h2&gt;七、Shadow DOM&lt;/h2&gt;

 &lt;p&gt;我们不希望用户能够看到  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;的内部代码，Web Component 允许内部代码隐藏起来，这叫做 Shadow DOM，即这部分 DOM 默认是隐藏的，开发者工具里面看不到。&lt;/p&gt;

 &lt;p&gt;自定义元素的  &lt;code&gt;this.attachShadow()&lt;/code&gt;方法开启 Shadow DOM，详见下面的代码。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: &amp;apos;closed&amp;apos; } );

    var templateElem = document.getElementById(&amp;apos;userCardTemplate&amp;apos;);
    var content = templateElem.content.cloneNode(true);
    content.querySelector(&amp;apos;img&amp;apos;).setAttribute(&amp;apos;src&amp;apos;, this.getAttribute(&amp;apos;image&amp;apos;));
    content.querySelector(&amp;apos;.container&amp;gt;.name&amp;apos;).innerText = this.getAttribute(&amp;apos;name&amp;apos;);
    content.querySelector(&amp;apos;.container&amp;gt;.email&amp;apos;).innerText = this.getAttribute(&amp;apos;email&amp;apos;);

    shadow.appendChild(content);
  }
}
window.customElements.define(&amp;apos;user-card&amp;apos;, UserCard);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，  &lt;code&gt;this.attachShadow()&lt;/code&gt;方法的参数  &lt;code&gt;{ mode: &amp;apos;closed&amp;apos; }&lt;/code&gt;，表示 Shadow DOM 是封闭的，不允许外部访问。&lt;/p&gt;

 &lt;p&gt;至此，这个 Web Component 组件就完成了，完整代码可以访问  &lt;a href="https://jsbin.com/yobopor/1/edit?html,js,output"&gt;这里&lt;/a&gt;。可以看到，整个过程还是很简单的，不像第三方框架那样有复杂的 API。&lt;/p&gt;

 &lt;h2&gt;八、组件的扩展&lt;/h2&gt;

 &lt;p&gt;在前面的基础上，可以对组件进行扩展。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;（1）与用户互动&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;用户卡片是一个静态组件，如果要与用户互动，也很简单，就是在类里面监听各种事件。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
this.$button = shadow.querySelector(&amp;apos;button&amp;apos;);
this.$button.addEventListener(&amp;apos;click&amp;apos;, () =&amp;gt; {
  // do something
});
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;  &lt;strong&gt;（2）组件的封装&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;上面的例子中，  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;与网页代码放在一起，其实可以用脚本把  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;注入网页。这样的话，JavaScript 脚本跟  &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;就能封装成一个 JS 文件，成为独立的组件文件。网页只要加载这个脚本，就能使用  &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt;组件。&lt;/p&gt;

 &lt;p&gt;这里就不展开了，更多 Web Components 的高级用法，可以接着学习下面两篇文章。&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;    &lt;a href="https://www.robinwieruch.de/web-components-tutorial/"&gt;Web Components Tutorial for Beginners&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;    &lt;a href="https://developers.google.com/web/fundamentals/web-components/customelements"&gt;Custom Elements v1: Reusable Web Components
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;h2&gt;九、参考链接&lt;/h2&gt;

 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://itnext.io/the-anatomy-of-web-components-d6afedb81b37"&gt;The anatomy of Web Components&lt;/a&gt;, Uday Hiwarale&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;（完）&lt;/p&gt;
 &lt;div&gt;  &lt;h3&gt;文档信息&lt;/h3&gt;
  &lt;ul&gt;
   &lt;li&gt;版权声明：自由转载-非商用-非衍生-保持署名（    &lt;a href="http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh"&gt;创意共享3.0许可证&lt;/a&gt;）&lt;/li&gt;
   &lt;li&gt;发表日期： 2019年8月 6日&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt; &lt;div&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>JavaScript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59920-web-components-%E5%AE%9E%E4%BE%8B</guid>
      <pubDate>Tue, 06 Aug 2019 17:39:44 CST</pubDate>
    </item>
    <item>
      <title>为什么我们要熟悉这些通信协议？ 【精读】</title>
      <link>https://itindex.net/detail/59874-%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE-%E7%B2%BE%E8%AF%BB</link>
      <description>&lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVD?w=1198&amp;h=868" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h1&gt;前端的最重要的基础知识点是什么？&lt;/h1&gt;
 &lt;ul&gt;
  &lt;li&gt;原生   &lt;code&gt;javaScript&lt;/code&gt;，   &lt;code&gt;HTML&lt;/code&gt;,   &lt;code&gt;CSS&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;Dom&lt;/code&gt;操作&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;EventLoop&lt;/code&gt;和渲染机制&lt;/li&gt;
  &lt;li&gt;各类工程化的工具原理以及使用，根据需求定制编写插件和包。（webpack的plugin和babel的预设包）&lt;/li&gt;
  &lt;li&gt;数据结构和算法（特别是   &lt;code&gt;IM&lt;/code&gt;以及超大型高并发网站应用等，例如   &lt;code&gt;B站&lt;/code&gt;）&lt;/li&gt;
  &lt;li&gt;最后便是通信协议&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;在使用某个技术的时候，一定要去追寻原理和底层的实现，长此以往坚持，只要自身底层的基础扎实，无论技术怎么变化，学习起来都不会太累，总的来说就是拒绝5分钟技术&lt;/blockquote&gt;
 &lt;h2&gt;从输入一个  &lt;code&gt;url&lt;/code&gt;地址，到显示页面发生了什么出发：&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;&lt;/li&gt;
  &lt;li&gt;2.建立TCP连接（三次握手）;&lt;/li&gt;
  &lt;li&gt;3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求，该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;&lt;/li&gt;
  &lt;li&gt;4.服务器对浏览器请求作出响应，并把对应的 html 文本发送给浏览器;&lt;/li&gt;
  &lt;li&gt;5.浏览器将该 html 文本并显示内容;&lt;/li&gt;
  &lt;li&gt;6.释放 TCP连接（四次挥手）;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;目前常见的通信协议都是建立在  &lt;code&gt;TCP&lt;/code&gt;链接之上&lt;/h2&gt;
 &lt;h3&gt;那么什么是  &lt;code&gt;TCP&lt;/code&gt;呢&lt;/h3&gt;
 &lt;h4&gt;TCP是因特网中的传输层协议，使用三次握手协议建立连接。当主动方发出SYN连接请求后，等待对方回答&lt;/h4&gt;
 &lt;blockquote&gt;TCP三次握手的过程如下：&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;客户端发送   &lt;code&gt;SYN&lt;/code&gt;报文给服务器端，进入   &lt;code&gt;SYN_SEND&lt;/code&gt;状态。&lt;/li&gt;
  &lt;li&gt;服务器端收到   &lt;code&gt;SYN&lt;/code&gt;报文，回应一个   &lt;code&gt;SYN&lt;/code&gt;（SEQ=y）ACK（ACK=x+1）报文，进入SYN_RECV状态。&lt;/li&gt;
  &lt;li&gt;客户端收到服务器端的SYN报文，回应一个ACK（ACK=y+1）报文，进入Established状态。&lt;/li&gt;
  &lt;li&gt;三次握手完成，TCP客户端和服务器端成功地建立连接，可以开始传输数据了。&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;如图所示：&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVC?w=1082&amp;h=914" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;code&gt;TCP&lt;/code&gt;的四次挥手：&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;建立一个连接需要三次握手，而终止一个连接要经过四次握手，这是由TCP的半关闭（half-close）造成的。具体过程如下图所示。&lt;/li&gt;
  &lt;li&gt;某个应用进程首先调用close，称该端执行“主动关闭”（active close）。该端的TCP于是发送一个FIN分节，表示数据发送完毕。&lt;/li&gt;
  &lt;li&gt;接收到这个FIN的对端执行 “被动关闭”（passive close），这个FIN由TCP确认。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;注意：FIN的接收也作为一个文件结束符（end-of-file）传递给接收端应用进程，放在已排队等候该应用进程接收的任何其他数据之后，因为，FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;一段时间后，接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。&lt;/li&gt;
  &lt;li&gt;接收这个最终FIN的原发送端TCP（即执行主动关闭的那一端）确认这个FIN。 [3]&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;既然每个方向都需要一个FIN和一个ACK，因此通常需要4个分节。&lt;/p&gt;
 &lt;blockquote&gt;特别提示：   &lt;code&gt;SYN&lt;/code&gt;报文用来通知，  &lt;code&gt;FIN&lt;/code&gt;报文是用来同步的&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVz?w=1742&amp;h=1194" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;以上就是面试官常问的三次握手，四次挥手，但是这不仅仅面试题，上面仅仅答到了一点皮毛，学习这些是为了让我们后续方便了解他的优缺点。&lt;/blockquote&gt;
 &lt;h2&gt;在  &lt;code&gt;TCP&lt;/code&gt;连接建立后，我们可以有多种协议的方式通信交换数据：&lt;/h2&gt;
 &lt;h3&gt;最古老的方式一：  &lt;code&gt;http 1.0&lt;/code&gt;
&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;早先1.0的HTTP版本，是一种无状态、无连接的应用层协议。&lt;/li&gt;
  &lt;li&gt;HTTP1.0规定浏览器和服务器保持短暂的连接，浏览器的每次请求都需要与服务器建立一个TCP连接，服务器处理完成后立即断开TCP连接（无连接），服务器不跟踪每个客户端也不记录过去的请求（无状态）。&lt;/li&gt;
  &lt;li&gt;这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。&lt;/li&gt;
  &lt;li&gt;首先，无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候，都需要进行一次TCP的连接，而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。&lt;/li&gt;
  &lt;li&gt;其次就是队头阻塞（headoflineblocking）。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达，那么下一个请求就不发送，同样的后面的请求也给阻塞了。&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;code&gt;Http 1.0&lt;/code&gt;的致命缺点,就是无法复用  &lt;code&gt;TCP&lt;/code&gt;连接和并行发送请求，这样每次一个请求都需要三次握手，而且其实建立连接和释放连接的这个过程是最耗时的，传输数据相反却不那么耗时。还有本地时间被修改导致响应头  &lt;code&gt;expires&lt;/code&gt;的缓存机制失效的问题～（后面会详细讲）&lt;/blockquote&gt;
 &lt;ul&gt;  &lt;li&gt;常见的请求报文～&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVA?w=1582&amp;h=1432" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;于是出现了  &lt;code&gt;Http 1.1&lt;/code&gt;，这也是技术的发展必然结果～&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;Http 1.1&lt;/code&gt;出现，继承了   &lt;code&gt;Http1.0&lt;/code&gt;的优点，也克服了它的缺点，出现了   &lt;code&gt;keep-alive&lt;/code&gt;这个头部字段，它表示会在建立   &lt;code&gt;TCP&lt;/code&gt;连接后，完成首次的请求，并不会立刻断开   &lt;code&gt;TCP&lt;/code&gt;连接，而是保持这个连接状态～进而可以复用这个通道&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;Http 1.1&lt;/code&gt;并且支持请求管道化，“并行”发送请求，但是这个并行，也不是真正意义上的并行，而是可以让我们把先进先出队列从客户端（请求队列）迁移到服务端（响应队列）&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;例如：客户端同时发了两个请求分别来获取html和css，假如说服务器的css资源先准备就绪，服务器也会先发送html再发送css。&lt;/blockquote&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt;B站&lt;/code&gt;首页，就有   &lt;code&gt;keep-alive&lt;/code&gt;，因为他们也有   &lt;code&gt;IM&lt;/code&gt;的成分在里面。需要大量复用   &lt;code&gt;TCP&lt;/code&gt;连接～&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVE?w=1442&amp;h=1234" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt;HTTP1.1&lt;/code&gt;好像还是无法解决队头阻塞的问题&lt;/li&gt;&lt;/ul&gt;
 &lt;blockquote&gt;实际上，现阶段的浏览器厂商采取了另外一种做法，它允许我们打开多个TCP的会话。也就是说，上图我们看到的并行，其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这，才是真正的并行！&lt;/blockquote&gt;
 &lt;h4&gt;
  &lt;code&gt;Http 1.1&lt;/code&gt;的致命缺点：&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;1.明文传输&lt;/li&gt;
  &lt;li&gt;2.其实还是没有解决无状态连接的&lt;/li&gt;
  &lt;li&gt;3.当有多个请求同时被挂起的时候 就会拥塞请求通道，导致后面请求无法发送&lt;/li&gt;
  &lt;li&gt;4.臃肿的消息首部:HTTP/1.1能压缩请求内容,但是消息首部不能压缩;在现今请求中,消息首部占请求绝大部分(甚至是全部)也较为常见.&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;我们也可以用  &lt;code&gt;dns-prefetch和 preconnect tcp&lt;/code&gt;来优化～&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;link rel=&amp;quot;preconnect&amp;quot; href=&amp;quot;//example.com&amp;quot; crossorigin&amp;gt;
&amp;lt;link rel=&amp;quot;dns=prefetch&amp;quot; href=&amp;quot;//example.com&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt;Tip&lt;/code&gt;:     &lt;code&gt;webpack&lt;/code&gt;可以做任何事情，这些都可以用插件实现&lt;/li&gt;&lt;/ul&gt;
 &lt;h3&gt;基于这些缺点，出现了  &lt;code&gt;Http 2.0&lt;/code&gt;
&lt;/h3&gt;
 &lt;h4&gt;相较于HTTP1.1，HTTP2.0的主要优点有采用二进制帧封装，传输变成多路复用，流量控制算法优化，服务器端推送，首部压缩，优先级等特点。&lt;/h4&gt;
 &lt;h4&gt;HTTP1.x的解析是基于文本的，基于文本协议的格式解析存在天然缺陷，文本的表现形式有多样性，要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧，然后采用二进制的格式进行编码，HTTP1.x的头部信息会被封装到HEADER frame，而相应的RequestBody则封装到DATAframe里面。不改动HTTP的语义，使用二进制编码，实现方便且健壮。&lt;/h4&gt;
 &lt;h4&gt;多路复用&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求，但是多个请求之间的响应会被阻塞的，所以 pipeline 至今也没有被普及应用，而 HTTP/2 做到了真正的并发请求。同时，流还支持优先级和流量控制。当流并发时，就会涉及到流的优先级和依赖。即：HTTP2.0对于同一域名下所有请求都是基于流的，不管对于同一域名访问多少文件，也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT，这个设计可以确保重要的东西可以被优先加载完&lt;/li&gt;&lt;/ul&gt;
 &lt;h4&gt;流量控制&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;TCP协议通过sliding window的算法来做流量控制。发送方有个sending window，接收方有receive window。http2.0的flow control是类似receive window的做法，数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control，如果接收方在flow window为零的情况下依然更多的frame，则会返回block类型的frame，这张场景一般表明http2.0的部署出了问题。&lt;/li&gt;&lt;/ul&gt;
 &lt;h4&gt;服务器端推送&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;服务器端的推送，就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外，服务器还可以额外向客户端推送资源，而无需客户端明确地请求。当浏览器请求一个html，服务器其实大概知道你是接下来要请求资源了，而不需要等待浏览器得到html后解析页面再发送资源请求。&lt;/li&gt;&lt;/ul&gt;
 &lt;h4&gt;首部压缩&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对，对于相同的数据，不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。&lt;/li&gt;
  &lt;li&gt;如果首部发生变化了，那么只需要发送变化了数据在Headers帧里面，新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。&lt;/li&gt;
  &lt;li&gt;本质上，当然是为了减少请求啦，通过多个js或css合并成一个文件，多张小图片拼合成Sprite图，可以让多个HTTP请求减少为一个，减少额外的协议开销，而提升性能。当然，一个HTTP的请求的body太大也是不合理的，有个度。文件的合并也会牺牲模块化和缓存粒度，可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite，让其充分地缓存起来，从而区分开迭代快的文件。&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;code&gt;Demo&lt;/code&gt;的性能对比：&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVH?w=1700&amp;h=938" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;
  &lt;code&gt;Http&lt;/code&gt;的那些致命缺陷，并没有完全解决，于是有了  &lt;code&gt;https&lt;/code&gt;，也是目前应用最广的协议之一&lt;/h2&gt;
 &lt;h3&gt;
  &lt;code&gt;HTTP+ 加密 + 认证 + 完整性保护 =HTTPS &lt;/code&gt; ?&lt;/h3&gt;
 &lt;h4&gt;可以这样认为～HTTP 加上加密处理和认证以及完整性保护后即是 HTTPS&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;如果在 HTTP 协议通信过程中使用未经加密的明文，比如在 Web 页面中输入信用卡号，如果这条通信线路遭到窃听，那么信用卡号就暴露了。&lt;/li&gt;
  &lt;li&gt;另外，对于 HTTP 来说，服务器也好，客户端也好，都是没有办法确认通信方的。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;因为很有可能并不是和原本预想的通信方在实际通信。并且还需要考虑到接收到的报文在通信途中已经遭到篡改这一可能性。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;为了统一解决上述这些问题，需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS&lt;/li&gt;&lt;/ul&gt;
 &lt;blockquote&gt;不加密的重要内容被  &lt;code&gt;wireshark&lt;/code&gt;这类工具抓到包，后果很严重～&lt;/blockquote&gt;
 &lt;h3&gt;HTTPS 是身披 SSL 外壳的 HTTP&lt;/h3&gt;
 &lt;ul&gt;  &lt;li&gt;HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL（SecureSocket Layer）和 TLS（Transport Layer Security）协议代替而已。&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;通常，HTTP 直接和 TCP 通信。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;当使用 SSL 时，则演变成先和 SSL 通信，再由 SSL和 TCP 通信了。简言之，所谓 HTTPS，其实就是身披 SSL 协议这层外壳的HTTP。&lt;/li&gt;
  &lt;li&gt;在采用 SSL 后，HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。SSL 是独立于 HTTP 的协议，所以不光是 HTTP 协议，其他运行在应用层的 SMTP和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全术。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVI?w=1736&amp;h=740" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;相互交换密钥的公开密钥加密技术 -----对称加密&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVM?w=1720&amp;h=1310" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;在对 SSL 进行讲解之前，我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密（Public-key cryptography）的加密处理方式。&lt;/li&gt;
  &lt;li&gt;近代的加密方法中加密算法是公开的，而密钥却是保密的。通过这种方式得以保持加密方法的安全性。&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;加密和解密都会用到密钥。没有密钥就无法对密码解密，反过来说，任何人只要持有密钥就能解密了。如果密钥被攻击者获得，那加密也就失去了意义。&lt;/blockquote&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;a href="https://blog.csdn.net/ituling/article/details/52541585" rel="nofollow noreferrer"&gt;https://blog.csdn.net/ituling...&lt;/a&gt;，    &lt;code&gt;Https&lt;/code&gt;加密篇幅太长，这篇文章写得很好，大家可以去看看。&lt;/li&gt;&lt;/ul&gt;
 &lt;h4&gt;HTTPS 采用混合加密机制&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。&lt;/li&gt;
  &lt;li&gt;但是公开密钥加密与共享密钥加密相比，其处理速度要慢。所以应充分利用两者各自的优势，将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式，之后的建立通信交换报文阶段则使用共享密钥加密方式。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;
  &lt;code&gt;HTTPS&lt;/code&gt;虽好，非对称加密虽好，但是不要滥用&lt;/h2&gt;
 &lt;h3&gt;HTTPS 也存在一些问题，那就是当使用 SSL 时，它的处理速度会变慢。&lt;/h3&gt;
 &lt;h4&gt;SSL 的慢分两种。一种是指通信慢。另一种是指由于大量消耗 CPU 及内存等资源，导致处理速度变慢。&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;和使用 HTTP 相比，网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 ? 响应以外，还必须进行 SSL 通信，因此整体上处理通信量不可避免会增加。&lt;/li&gt;
  &lt;li&gt;另一点是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲，比起 HTTP 会更多地消耗服务器和客户端的硬件资源，导致负载增强。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;针对速度变慢这一问题，并没有根本性的解决方案，我们会使用 SSL 加速器这种（专用服务器）硬件来改善该问题。该硬件为 SSL 通信专用硬件，相对软件来讲，能够提高数倍 SSL 的计算速度。仅在 SSL 处理时发挥 SSL加速器的功效，以分担负载。&lt;/p&gt;
 &lt;h2&gt;为什么不一直使用 HTTPS&lt;/h2&gt;
 &lt;ul&gt;  &lt;li&gt;既然 HTTPS 那么安全可靠，那为何所有的 Web 网站不一直使用 HTTPS？&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;其中一个原因是，因为与纯文本通信相比，加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密，会消耗相当多的资源，平摊到一台计算机上时，能够处理的请求数量必定也会随之减少。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;因此，如果是非敏感信息则使用 HTTP 通信，只有在包含个人信息等敏感数据时，才利用 HTTPS 加密通信。&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;特别是每当那些访问量较多的 Web 网站在进行加密处理时，它们所承担着的负载不容小觑。在进行加密处理时，并非对所有内容都进行加密处理，而是仅在那些需要信息隐藏时才会加密，以节约资源。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;除此之外，想要节约购买证书的开销也是原因之一。&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;要进行 HTTPS 通信，证书是必不可少的。而使用的证书必须向认证机构（CA）购买。证书价格可能会根据不同的认证机构略有不同。通常，一年的授权需要数万日元（现在一万日元大约折合 600 人民币）。那些购买证书并不合算的服务以及一些个人网站，可能只会选择采用HTTP 的通信方式。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVR?w=1158&amp;h=1002" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;复习完了基本的协议，介绍下报文格式：&lt;/h2&gt;
 &lt;ul&gt;  &lt;li&gt;请求报文格式&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVT?w=1728&amp;h=852" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;响应报文格式&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVU?w=1614&amp;h=690" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;所谓响应头，请求头，其实都可以自己添加字段，只要前后端给对应的处理机制即可&lt;/blockquote&gt;
 &lt;h2&gt;
  &lt;code&gt;Node.js&lt;/code&gt;代码实现响应头的设置&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;
  if (config.cache.expires) {
                        res.setHeader(&amp;quot;expries&amp;quot;, new Date(Date.now() + (config.cache.maxAge * 1000)))
                    }
                    if (config.cache.lastModified) {
                        res.setHeader(&amp;quot;last-modified&amp;quot;, stat.mtime.toUTCString())
                    }
                    if (config.cache.etag) {
                        res.setHeader(&amp;apos;Etag&amp;apos;, etagFn(stat))
                    }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;响应头的详解：&lt;/h2&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVW?w=1760&amp;h=1426" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;本人的开源项目，手写的  &lt;code&gt;Node.js&lt;/code&gt;静态资源服务器，  &lt;a href="https://github.com/JinJieTan/util-static-server" rel="nofollow noreferrer"&gt;https://github.com/JinJieTan/...&lt;/a&gt;，欢迎   &lt;code&gt;star&lt;/code&gt;~&lt;/h3&gt;
 &lt;h2&gt;浏览器的缓存策略：&lt;/h2&gt;
 &lt;ul&gt;  &lt;li&gt;首次请求：&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVX?w=1642&amp;h=1088" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;非首次请求：&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCVZ?w=1528&amp;h=1316" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;用户行为与缓存：&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbvCV0?w=1606&amp;h=1374" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;不能缓存的请求：&lt;/h2&gt;
 &lt;h3&gt;无法被浏览器缓存的请求如下：&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;HTTP信息头中包含Cache-Control:no-cache，pragma:no-cache（HTTP1.0），或Cache-Control:max-age=0等告诉浏览器不用缓存的请求&lt;/li&gt;
  &lt;li&gt;需要根据Cookie，认证信息等决定输入内容的动态请求是不能被缓存的&lt;/li&gt;
  &lt;li&gt;经过HTTPS安全加密的请求（有人也经过测试发现，ie其实在头部加入Cache-Control：max-age信息，firefox在头部加入Cache-Control:Public之后，能够对HTTPS的资源进行缓寸）&lt;/li&gt;
  &lt;li&gt;经过HTTPS安全加密的请求（有人也经过测试发现，ie其实在头部加入Cache-Control：max-age信息，firefox在头部加入Cache-Control:Public之后，能够对HTTPS的资源进行缓存，参考《HTTPS的七个误解》）&lt;/li&gt;
  &lt;li&gt;POST请求无法被缓存&lt;/li&gt;
  &lt;li&gt;HTTP响应头中不包含Last-Modified/Etag，也不包含Cache-Control/Expires的请求无法被缓存&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;即时通讯协议&lt;/h2&gt;
 &lt;h3&gt;从最初的没有  &lt;code&gt;websocket&lt;/code&gt;协议开始：&lt;/h3&gt;
 &lt;blockquote&gt;传统的协议无法服务端主动  &lt;code&gt;push&lt;/code&gt;数据，于是有了这些骚操作：&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;轮询，在一个定时器中不停向服务端发送请求。&lt;/li&gt;
  &lt;li&gt;长轮询，发送请求给服务端，直到服务端觉得可以返回数据了再返回响应，否则这个请求一直挂起～&lt;/li&gt;
  &lt;li&gt;以上两种都有瑕疵，而且比较明显，这里不再描述。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;为了解决实时通讯，数据同步的问题，出现了  &lt;code&gt;webSocket&lt;/code&gt;.&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;webSockets&lt;/code&gt;的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后，会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后，建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;webSocket&lt;/code&gt;原理： 在   &lt;code&gt;TCP&lt;/code&gt;连接第一次握手的时候，升级为   &lt;code&gt;ws&lt;/code&gt;协议。后面的数据交互都复用这个   &lt;code&gt;TCP&lt;/code&gt;通道。&lt;/li&gt;
  &lt;li&gt;客户端代码实现：&lt;/li&gt;
&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;  const ws = new WebSocket(&amp;apos;ws://localhost:8080&amp;apos;);
        ws.onopen = function () {
            ws.send(&amp;apos;123&amp;apos;)
            console.log(&amp;apos;open&amp;apos;)
        }
        ws.onmessage = function () {
            console.log(&amp;apos;onmessage&amp;apos;)
        }
        ws.onerror = function () {
            console.log(&amp;apos;onerror&amp;apos;)
        }
        ws.onclose = function () {
            console.log(&amp;apos;onclose&amp;apos;)
        }&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;  &lt;li&gt;服务端使用    &lt;code&gt;Node.js&lt;/code&gt;语言实现&lt;/li&gt;&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;const express = require(&amp;apos;express&amp;apos;)
const { Server } = require(&amp;quot;ws&amp;quot;);
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on(&amp;apos;connection&amp;apos;, (ws) =&amp;gt; {
    ws.onopen = function () {
        console.log(&amp;apos;open&amp;apos;)
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send(&amp;apos;234&amp;apos;)
        console.log(&amp;apos;onmessage&amp;apos; + data)
    }
    ws.onerror = function () {
        console.log(&amp;apos;onerror&amp;apos;)
    }
    ws.onclose = function () {
        console.log(&amp;apos;onclose&amp;apos;)
    }
});

app.listen(8000, (err) =&amp;gt; {
    if (!err) { console.log(&amp;apos;监听OK&amp;apos;) } else {
        console.log(&amp;apos;监听失败&amp;apos;)
    }
})&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;
  &lt;code&gt;webSocket&lt;/code&gt;的报文格式有一些不一样：&lt;/h2&gt;
 &lt;p&gt;![图片上传中...]&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;p&gt;客户端和服务端进行Websocket消息传递是这样的:&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;客户端：将消息切割成多个帧，并发送给服务端。&lt;/li&gt;
    &lt;li&gt;服务端：接收消息帧，并将关联的帧重新组装成完整的消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;即时通讯的心跳检测：&lt;/h2&gt;
 &lt;h3&gt;
  &lt;code&gt;ping&lt;/code&gt;and  &lt;code&gt;pong&lt;/code&gt;
&lt;/h3&gt;
 &lt;ul&gt;  &lt;li&gt;服务端   &lt;code&gt;Go&lt;/code&gt;实现：&lt;/li&gt;&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;package main

import (
    &amp;quot;net/http&amp;quot;
    &amp;quot;time&amp;quot;

    &amp;quot;github.com/gorilla/websocket&amp;quot;
)

var (
    //完成握手操作
    upgrade = websocket.Upgrader{
       //允许跨域(一般来讲,websocket都是独立部署的)
       CheckOrigin:func(r *http.Request) bool {
            return true
       },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
   var (
         conn *websocket.Conn
         err error
         data []byte
   )
   //服务端对客户端的http请求(升级为websocket协议)进行应答，应答之后，协议升级为websocket，http建立连接时的tcp三次握手将保持。
   if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
        return
   }

    //启动一个协程，每隔5s向客户端发送一次心跳消息
    go func() {
        var (
            err error
        )
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte(&amp;quot;heartbeat&amp;quot;)); err != nil {
                return
            }
            time.Sleep(5 * time.Second)
        }
    }()

   //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
   for {
         //通过websocket长链接读到的数据可以是text文本数据，也可以是二进制Binary
        if _, data, err = conn.ReadMessage(); err != nil {
            goto ERR
     }
     if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
         goto ERR
     }
   }
ERR:
    //出错之后，关闭socket连接
    conn.Close()
}

func main() {
    http.HandleFunc(&amp;quot;/ws&amp;quot;, wsHandler)
    http.ListenAndServe(&amp;quot;0.0.0.0:7777&amp;quot;, nil)
}&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;客户端的心跳检测(  &lt;code&gt;Node.js&lt;/code&gt;实现)：&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;this.heartTimer = setInterval(() =&amp;gt; {
      if (this.heartbeatLoss &amp;lt; MAXLOSSTIMES) {
        events.emit(&amp;apos;network&amp;apos;, &amp;apos;sendHeart&amp;apos;);
        this.heartbeatLoss += 1;
        this.phoneLoss += 1;
      } else {
        events.emit(&amp;apos;network&amp;apos;, &amp;apos;offline&amp;apos;);
        this.stop();
      }
      if (this.phoneLoss &amp;gt; MAXLOSSTIMES) {
        this.PhoneLive = false;
        events.emit(&amp;apos;network&amp;apos;, &amp;apos;phoneDisconnect&amp;apos;);
      }
    }, 5000);
&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;自定义即时通信协议：&lt;/h2&gt;
 &lt;h3&gt;从  &lt;code&gt;new Socket&lt;/code&gt;开始：&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;目前即时通讯大都使用现有大公司成熟的   &lt;code&gt;SDK&lt;/code&gt;接入，但是逼格高些还是自己重写比较好。&lt;/li&gt;
  &lt;li&gt;打个小广告，我们公司就是自己定义的即时通讯协议～招聘一位高级前端，地点深圳-深南大道，做跨平台   &lt;code&gt;IM&lt;/code&gt;桌面应用开发的～&lt;/li&gt;
  &lt;li&gt;客户端代码实现（Node.js）:&lt;/li&gt;
&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;
const {Socket} = require(&amp;apos;net&amp;apos;) 
const tcp = new Socket()
tcp.setKeepAlive(true);
tcp.setNoDelay(true);
//保持底层tcp链接不断，长连接
指定对应域名端口号链接
tcp.connect(80,166.166.0.0)
建立连接后
根据后端传送的数据类型 使用对应不同的解析
readUInt8 readUInt16LE readUInt32LE readIntLE等处理后得到myBuf 
const myBuf = buffer.slice(start);//从对应的指针开始的位置截取buffer
const header = myBuf.slice(headstart,headend)//截取对应的头部buffer
const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
//精确截取数据体的buffer,并且转化成js对象&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;即时通讯强烈推荐使用  &lt;code&gt;Golang&lt;/code&gt;,  &lt;code&gt;GRPC&lt;/code&gt;,  &lt;code&gt;Prob&lt;/code&gt;传输数据。&lt;/blockquote&gt;
 &lt;h2&gt;上面的一些代码，都在我的开源项目中：&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;手写的静态资源服务器,   &lt;a href="https://github.com/JinJieTan/util-static-server" rel="nofollow noreferrer"&gt;https://github.com/JinJieTan/...&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;webpack-electron-react-websocket&lt;/code&gt;的Demo,    &lt;a href="https://github.com/JinJieTan/react-electron-webpack" rel="nofollow noreferrer"&gt;https://github.com/JinJieTan/...&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;觉得写得不错，可以点个赞支持下，文章也借鉴了一下其他大佬的文章，但是地址都贴上来了～ 欢迎  &lt;code&gt;gitHub&lt;/code&gt;点个  &lt;code&gt;star&lt;/code&gt;哦～&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>html5 html css node.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59874-%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE-%E7%B2%BE%E8%AF%BB</guid>
      <pubDate>Sat, 27 Jul 2019 13:07:04 CST</pubDate>
    </item>
    <item>
      <title>[译]保持Node.js的速度-创建高性能Node.js Servers的工具、技术和提示</title>
      <link>https://itindex.net/detail/59801-node-js-%E9%80%9F%E5%BA%A6</link>
      <description>&lt;h2&gt;pre-tips&lt;/h2&gt;
 &lt;p&gt;本文翻译自:   &lt;a href="https://www.smashingmagazine.com/2018/06/nodejs-tools-techniques-performance-servers/" rel="nofollow noreferrer"&gt;Keeping Node.js Fast: Tools, Techniques, And Tips For Making High-Performance Node.js Servers&lt;/a&gt; &lt;/p&gt;
 &lt;p&gt;原文地址：  &lt;a href="https://www.smashingmagazine.com/2018/06/nodejs-tools-techniques-performance-servers/" rel="nofollow noreferrer"&gt;https://www.smashingmagazine....&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;中文标题：保持Node.js的速度-创建高性能Node.js Servers的工具、技术和提示&lt;/p&gt;
 &lt;h2&gt;快速摘要&lt;/h2&gt;
 &lt;p&gt;Node 是一个非常多彩的平台，而创建network服务就是其非常重要的能力之一。在本文我们将关注最主流的: HTTP Web servers.&lt;/p&gt;
 &lt;h2&gt;引子&lt;/h2&gt;
 &lt;p&gt;如果你已经使用Node.js足够长的时间，那么毫无疑问你会碰到比较痛苦的速度问题。JavaScript是一种事件驱动的、异步的语言。这很明显使得对性能的推理变得棘手。Node.js的迅速普及使得我们必须寻找适合这种server-side javacscript的工具、技术。&lt;/p&gt;
 &lt;p&gt;当我们碰到性能问题，在浏览器端的经验将无法适用于服务器端。所以我们如何确保一个Node.js代码是快速的且能达到我们的要求呢？让我们来动手看一些实例&lt;/p&gt;
 &lt;h2&gt;工具&lt;/h2&gt;
 &lt;p&gt;我们需要一个工具来压测我们的server从而测量性能。比如，我们使用   &lt;a href="https://github.com/mcollina/autocannon" rel="nofollow noreferrer"&gt;autocannon&lt;/a&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm install -g autocannon // 或使用淘宝源cnpm, 腾讯源tnpm&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;其他的Http benchmarking tools 包括   &lt;a href="https://httpd.apache.org/docs/2.4/programs/ab.html" rel="nofollow noreferrer"&gt;Apache Bench(ab)&lt;/a&gt; 和   &lt;a href="https://github.com/giltene/wrk2" rel="nofollow noreferrer"&gt;wrk2&lt;/a&gt;, 但AutoCannon是用Node写的，对前端来说会更加方便并易于安装，它可以非常方便的安装在 Windows、Linux 和Mac OS X.&lt;/p&gt;
 &lt;p&gt;当我们安装了基准性能测试工具，我们需要用一些方法去诊断我们的程序。一个很不错的诊断性能问题的工具便是   &lt;a href="https://github.com/nearform/node-clinic" rel="nofollow noreferrer"&gt;Node Clinic&lt;/a&gt; 。它也可以用npm安装:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm install -g clinic&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这实际上会安装一系列套件，我们将使用   &lt;a href="https://github.com/nearform/node-clinic-doctor" rel="nofollow noreferrer"&gt;Clinic Doctor&lt;/a&gt;  &lt;br /&gt;和 Clinic Flame (一个   &lt;a href="https://github.com/davidmarkclements/0x" rel="nofollow noreferrer"&gt;ox&lt;/a&gt; 的封装)&lt;/p&gt;
 &lt;blockquote&gt;译者注: ox是一个自动剖析cpu并生成node进程火焰图的工具; 而clinic Flame就是基于ox的封装。  &lt;br /&gt;另一方面， clinic工具本身其实是一系列套件的组合，它不同的子命令分别会调用到不同的子模块，例如:&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;医生诊断功能。The doctor functionality is provided by Clinic.js Doctor.&lt;/li&gt;
  &lt;li&gt;气泡诊断功能。The bubbleprof functionality is provided by Clinic.js Bubbleprof.&lt;/li&gt;
  &lt;li&gt;火焰图功能。 The flame functionality is provided by Clinic.js Flame.)&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;tips: 对于本文实例，需要 Node 8.11.2 或更高版本&lt;/p&gt;
 &lt;h2&gt;代码示例&lt;/h2&gt;
 &lt;p&gt;我们的例子是一个只有一个资源的简单的 REST server：暴露一个 GET 访问的路由   &lt;code&gt;/seed/v1&lt;/code&gt; ，返回一个大  JSON 载荷。 server端的代码就是一个app目录，里面包括一个   &lt;code&gt;packkage.json&lt;/code&gt; (依赖   &lt;code&gt;restify 7.1.0&lt;/code&gt;)、一个   &lt;code&gt;index.js&lt;/code&gt; 和 一个   &lt;code&gt;util.js&lt;/code&gt; (译者注: 放一些工具函数)&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// index.js
const restify = require(&amp;apos;restify&amp;apos;)
const server = restify.createServer()
const { etagger, timestamp, fetchContent } from &amp;apos;./util&amp;apos;

server.use(etagger.bind(server)) // 绑定etagger中间件，可以给资源请求加上etag响应头

server.get(&amp;apos;/seed/v1&amp;apos;, function () {
  fetchContent(req.url, (err, content) =&amp;gt; {
    if (err) {
      return next(err)
    }
    res.send({data: content, ts: timestamp(), url: req.url})
    next()
  })
})

server.listen(8080, function () {
  cosnole.log(&amp;apos; %s listening at %s&amp;apos;,  server.name, server.url)
})&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;// util.js
const restify = require(&amp;apos;restify&amp;apos;)
const crypto = require(&amp;apos;crypto&amp;apos;)

module.exports = function () {
    const content = crypto.rng(&amp;apos;5000&amp;apos;).toString(&amp;apos;hex&amp;apos;) // 普通有规则的随机

    const fetchContent = function (url, cb) {
        setImmediate(function () {
        if (url !== &amp;apos;/seed/v1&amp;apos;) return restify.errors.NotFoundError(&amp;apos;no api!&amp;apos;)
            cb(content)
        })
    }
    
    let last = Date.now()
    const TIME_ONE_MINUTE = 60000
    const timestamp = function () {
      const now = Date.now()
      if (now - last &amp;gt;= TIME_ONE_MINITE) {
          last = now
      }
      return last
    }
    
    const etagger = function () {
        const cache = {}
        let afterEventAttached  = false
        function attachAfterEvent(server) {
            if (attachAfterEvent ) return
            afterEventAttached  = true
            server.on(&amp;apos;after&amp;apos;, function (req, res) {
                if (res.statusCode == 200 &amp;amp;&amp;amp; res._body != null) {
                    const urlKey = crpto.createHash(&amp;apos;sha512&amp;apos;)
                        .update(req.url)
                        .digets()
                        .toString(&amp;apos;hex&amp;apos;)
                    const contentHash = crypto.createHash(&amp;apos;sha512&amp;apos;)
                    .update(JSON.stringify(res._body))
                    .digest()
                    .toString(&amp;apos;hex&amp;apos;)
                    if (cache[urlKey] != contentHash) cache[urlKey] = contentHash
                }
            })
        }
         return function(req, res, next) {
                // 译者注: 这里attachEvent的位置好像不太优雅，我换另一种方式改了下这里。可以参考: https://github.com/cuiyongjian/study-restify/tree/master/app
                attachAfterEvent(this) // 给server注册一个after钩子，每次即将响应数据时去计算body的etag值
            const urlKey = crypto.createHash(&amp;apos;sha512&amp;apos;)
            .update(req.url)
            .digest()
            .toString(&amp;apos;hex&amp;apos;)
            // 译者注: 这里etag的返回逻辑应该有点小问题，每次请求都是返回的上次写入cache的etag
            if (urlKey in cache) res.set(&amp;apos;Etag&amp;apos;, cache[urlKey])
            res.set(&amp;apos;Cache-Control&amp;apos;, &amp;apos;public; max-age=120&amp;apos;)
        }
    }
    
    return { fetchContent, timestamp, etagger }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;务必不要用这段代码作为最佳实践，因为这里面有很多代码的坏味道，但是我们接下来将测量并找出这些问题。&lt;/p&gt;
 &lt;p&gt;要获得这个例子的源码可以去  &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/tree/master/slow" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;Profiling 剖析&lt;/h2&gt;
 &lt;p&gt;为了剖析我们的代码，我们需要两个终端窗口。一个用来启动app，另外一个用来压测他。&lt;/p&gt;
 &lt;p&gt;第一个terminal，我们执行:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;node ./index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;另外一个terminal，我们这样剖析他（译者注: 实际是在压测）：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;autocannon -c100 localhost:3000/seed/v1&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这将打开100个并发请求轰炸服务，持续10秒。&lt;/p&gt;
 &lt;p&gt;结果大概是下面这个样子:&lt;/p&gt;
 &lt;table&gt;
  &lt;tr&gt;
   &lt;th&gt;stat&lt;/th&gt;
   &lt;th&gt;avg&lt;/th&gt;
   &lt;th&gt;stdev&lt;/th&gt;
   &lt;th&gt;Max&lt;/th&gt;
&lt;/tr&gt;

  &lt;tr&gt;
   &lt;td&gt;耗时(毫秒)&lt;/td&gt;
   &lt;td&gt;3086.81&lt;/td&gt;
   &lt;td&gt;1725.2&lt;/td&gt;
   &lt;td&gt;5554&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;吞吐量(请求/秒)&lt;/td&gt;
   &lt;td&gt;23.1&lt;/td&gt;
   &lt;td&gt;19.18&lt;/td&gt;
   &lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;每秒传输量(字节/秒)&lt;/td&gt;
   &lt;td&gt;237.98 kB&lt;/td&gt;
   &lt;td&gt;197.7 kB&lt;/td&gt;
   &lt;td&gt;688.13 kB&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;blockquote&gt;231 requests in 10s, 2.4 MB read&lt;/blockquote&gt;
 &lt;p&gt;结果会根据你机器情况变化。然而我们知道: 一般的“Hello World”Node.js服务器很容易在同样的机器上每秒完成三万个请求，现在这段代码只能承受每秒23个请求且平均延迟超过3秒，这是令人沮丧的。&lt;/p&gt;
 &lt;p&gt;译者注: 我用公司macpro18款 15寸 16G 256G，测试结果如下:&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyi" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;诊断&lt;/h2&gt;
 &lt;h3&gt;定位问题&lt;/h3&gt;
 &lt;p&gt;我们可以通过一句命令来诊断应用，感谢 clinic doctor 的 -on-port 命令。在app目录下，我们执行:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic doctor --on-port=&amp;apos;autocannon -c100 localhost:3000/seed/v1&amp;apos; -- node index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;译者注:  &lt;br /&gt;现在autocannon的话可以使用新的subarg形式的命令语法:&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;clinic doctor --autocannon [ /seed/v1 -c 100 ] -- node index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;clinic doctor会在剖析完毕后，创建html文件并自动打开浏览器。&lt;/p&gt;
 &lt;p&gt;结果长这个样子：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyk" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;译者的测试长这样子:  &lt;br /&gt;  &lt;img alt="--on-port&amp;#35821;&amp;#27861;" src="https://segmentfault.com/img/bVbuMym" title="--on-port&amp;#35821;&amp;#27861;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="--autocannon&amp;#35821;&amp;#27861;" src="https://segmentfault.com/img/bVbuMys" title="--autocannon&amp;#35821;&amp;#27861;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;译者注：横坐标其实是你系统时间，冒号后面的表示当前的系统时间的 - 秒数。  &lt;br /&gt;备注：接下来的文章内容分析，我们还是以原文的统计结果图片为依据。&lt;/blockquote&gt;
 &lt;p&gt;跟随UI顶部的消息，我们看到 EventLoop 图表，它的确是红色的，并且这个EventLoop延迟在持续增长。在我们深入研究他意味着什么之前，我们先了解下其他指标下的诊断。&lt;/p&gt;
 &lt;p&gt;我们可以看到CPU一直在100%或超过100%这里徘徊，因为进程正在努力处理排队的请求。Node的 JavaScript 引擎(也就是V8) 着这里实际上用 2 个 CPU核心在工作，因为机器是多核的 而V8会用2个线程。 一个线程用来执行 EventLoop，另外一个线程用来垃圾收集。 当CPU高达120%的时候就是进程在回收处理完的请求的遗留对象了(译者注: 操作系统的进程CPU使用率的确经常会超过100%，这是因为进程内用了多线程，OS把工作分配到了多个核心，因此统计cpu占用时间时会超过100%)&lt;/p&gt;
 &lt;p&gt;我们看与之相关的内存图表。实线表示内存的堆内存占用（译者注：RSS表示node进程实际占用的内存，heapUsage堆内存占用就是指的堆区域占用了多少，THA就表示总共申请到了多少堆内存。一般看heapUsage就好，因为他表示了node代码中大多数JavaScript对象所占用的内存）。我们看到，只要CPU图表上升一下则堆内存占用就下降一些，这表示内存正在被回收。&lt;/p&gt;
 &lt;p&gt;activeHandler跟EventLoop的延迟没有什么相关性。一个active hanlder 就是一个表达 I/O的对象(比如socket或文件句柄) 或者一个timer （比如setInterval）。我们用autocannon创建了100连接的请求(-c100), activehandlers 保持在103. 额外的3个handler其实是 STDOUT，STDERROR 以及 server 对象自身(译者: server自身也是个socket监听句柄)。&lt;/p&gt;
 &lt;p&gt;如果我们点击一下UI界面上底部的建议pannel面板，我们会看到：  &lt;br /&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyx" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;短期缓解&lt;/h3&gt;
 &lt;p&gt;深入分析性能问题需要花费大量的时间。在一个现网项目中，可以给服务器或服务添加过载保护。过载保护的思路就是检测 EventLoop 延迟（以及其他指标），然后在超过阈值时响应一个 &amp;quot;503 Service Unavailable&amp;quot;。这就可以让 负载均衡器转向其他server实例，或者实在不行就让用户过一会重试。  &lt;a href="https://github.com/davidmarkclements/overload-protection" rel="nofollow noreferrer"&gt;overload-protection-module&lt;/a&gt; 这个过载保护模块能直接低成本地接入到 Express、Koa 和 Restify使用。Hapi 框架也有一个  &lt;a href="https://hapijs.com/api#-serveroptionsload" rel="nofollow noreferrer"&gt;配置项&lt;/a&gt;提供同样的过载保护。（译者注：实际上看overload-protection模块的底层就是通过  &lt;a href="https://github.com/mcollina/loopbench" rel="nofollow noreferrer"&gt;loopbench&lt;/a&gt; 实现的EventLoop延迟采样，而loopbench就是从Hapi框架里抽离出来的一个模块；至于内存占用，则是overload-protection内部自己实现的采样，毕竟直接用memoryUsage的api就好了）&lt;/p&gt;
 &lt;h3&gt;理解问题所在&lt;/h3&gt;
 &lt;p&gt;就像 Clinic Doctor 说的，如果 EventLoop 延迟到我们观察的这个样子，很可能有一个或多个函数阻塞了事件循环。认识到Node.js的这个主要特性非常重要：在当前的同步代码执行完成之前，异步事件是无法被执行的。这就是为什么下面 setTimeout 不能按照预料的时间触发的原因。&lt;/p&gt;
 &lt;p&gt;举例，在浏览器开发者工具或Node.js的REPL里面执行：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;console.time(&amp;apos;timeout&amp;apos;)
setTimeout(console.timeEnd, 100, &amp;apos;timeout&amp;apos;)
let n = 1e7
while (n--) Math.random()&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这个打印出的时间永远不会是100ms。它将是150ms到250ms之间的一个数字。  &lt;code&gt;setTimeoiut&lt;/code&gt; 调度了一个异步操作（console.timeEnd），但是当前执行的代码没有完成；下面有额外两行代码来做了一个循环。当前所执行的代码通常被叫做“Tick”。要完成这个 Tick，Math.random 需要被调用 1000 万次。如果这会花销 100ms，那么timeout触发时的总时间就是 200ms (再加上setTimeout函数实际推入队列时的延时，约几毫秒)&lt;/p&gt;
 &lt;blockquote&gt;译者注: 实际上这里作者的解释有点小问题。首先这个例子假如按他所说循环会耗费100毫秒，那么setTimeout触发时也是100ms而已，不会是两个时间相加。因为100毫秒的循环结束，setTimeout也要被触发了。  &lt;br /&gt;另外：你实际电脑测试时，很可能像我一样得到的结果是 100ms多一点，而不是作者的150-250之间。作者之所以得到 150ms，是因为它使用的电脑性能原因使得 while(n--) 这个循环所花费的时间是 150ms到250ms。而一旦性能好一点的电脑计算1e7次循环只需几十毫秒，完全不会阻塞100毫秒之后的setTimeout，这时得到的结果往往是103ms左右，其中的3ms是底层函数入队和调用花掉的时间(跟这里所说的问题无关)。因此，你自己在测试时可以把1e7改成1e8试试。总之让他的执行时间超过100毫秒。&lt;/blockquote&gt;
 &lt;p&gt;在服务器端上下文如果一个操作在当前 Tick 中执行时间很长，那么就会导致请求无法被处理，并且数据也无法获取（译者注：比如处理新的网络请求或处理读取文件的IO事件），因为异步代码在当前 Tick 完成之前无法执行。这意味着计算昂贵的代码将会让server所有交互都变得缓慢。所以建议你拆分资源敏感的任务到单独的进程里去，然后从main主server中去调用它，这能避免那些很少使用但资源敏感(译者注: 这里特指CPU敏感)的路由拖慢了那些经常访问但资源不敏感的路由的性能（译者注：就是不要让某个cpu密集的路径拖慢整个node应用）。&lt;/p&gt;
 &lt;p&gt;本文的例子server中有很多代码阻塞了事件循环，所以下一步我们来定位这个代码的具体位置所在。&lt;/p&gt;
 &lt;h2&gt;分析&lt;/h2&gt;
 &lt;p&gt;定位性能问题的代码的一个方法就是创建和分析“火焰图”。一个火焰图将函数表达为彼此叠加的块---不是随着时间的推移而是聚合。之所以叫火焰图是因为它用橘黄到红色的色阶来表示，越红的块则表示是个“热点”函数，意味着很可能会阻塞事件循环。获取火焰图的数据需要通过对CPU进行采样---即node中当前执行的函数及其堆栈的快照。而热量(heat)是由一个函数在分析期间处于栈顶执行所占用的时间百分比决定的。如果它不是当前栈中最后被调用的那个函数，那么他就很可能会阻塞事件循环。&lt;/p&gt;
 &lt;p&gt;让我们用   &lt;code&gt;clinic flame&lt;/code&gt; 来生成示例代码的火焰图：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic flame --on-port=’autocannon -c100 localhost:$PORT/seed/v1’ -- node index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;译者注: 也可以使用新版命令风格:&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;clinic flame --autocannon [ /seed/v1 -c200 -d 10 ] -- node index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;结果会自动展示在你的浏览器中：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Clinic&amp;#21487;&amp;#35270;&amp;#21270;&amp;#28779;&amp;#28976;&amp;#22270;" src="https://segmentfault.com/img/bVbuMyz" title="Clinic&amp;#21487;&amp;#35270;&amp;#21270;&amp;#28779;&amp;#28976;&amp;#22270;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;译者注: 新版变成下面这副样子了，功能更强大，但可能得学习下怎么看。。&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyB" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（译者注：下面分析时还是看原文的图）  &lt;br /&gt;块的宽度表示它花费了多少CPU时间。可以看到3个主要堆栈花费了大部分的时间，而其中   &lt;code&gt;server.on&lt;/code&gt; 这个是最红的。 实际上，这3个堆栈是相同的。他们之所以分开是因为在分析期间优化过的和未优化的函数会被视为不同的调用帧。带有   &lt;code&gt;*&lt;/code&gt; 前缀的是被JavaScript引擎优化过的函数，而带有   &lt;code&gt;~&lt;/code&gt; 前缀的是未优化的。如果是否优化对我们的分析不重要，我们可以点击   &lt;code&gt;Merge&lt;/code&gt; 按钮把它们合并。这时图像会变成这样：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyD" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从开始看，我们可以发现出问题的代码在   &lt;code&gt;util.js&lt;/code&gt; 里。这个过慢的函数也是一个 event handler：触发这个函数的来源是Node核心里的   &lt;code&gt;events&lt;/code&gt; 模块，而   &lt;code&gt;server.on&lt;/code&gt; 是event handler匿名函数的一个后备名称。我们可以看到这个代码跟实际处理本次request请求的代码并不在同一个 Tick 当中（译者注: 如果在同一个Tick就会用一个堆栈图竖向堆叠起来）。如果跟request处理在同一个 Tick中，那堆栈中应该是Node的    &lt;code&gt;http&lt;/code&gt; 模块、net和stream模块&lt;/p&gt;
 &lt;p&gt;如果你展开其他的更小的块你会看到这些Http的Node核心函数。比如尝试下右上角的search，搜索关键词   &lt;code&gt;send&lt;/code&gt;（restify和http内部方法都有send方法）。然后你可以发现他们在火焰图的右边（函数按字母排序）（译者注：右侧蓝色高亮的区域）&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#25628;&amp;#32034;http&amp;#22788;&amp;#29702;&amp;#20989;&amp;#25968;" src="https://segmentfault.com/img/bVbuMyG" title="&amp;#25628;&amp;#32034;http&amp;#22788;&amp;#29702;&amp;#20989;&amp;#25968;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;可以看到实际的 HTTP 处理块占用时间相对较少。&lt;/p&gt;
 &lt;p&gt;我们可以点击一个高亮的青色块来展开，看到里面   &lt;code&gt;http_outgoing.js&lt;/code&gt; 文件的 writeHead、write函数（Node核心http库中的一部分）&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyH" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们可以点击   &lt;code&gt;all stack&lt;/code&gt; 返回到主要视图。&lt;/p&gt;
 &lt;p&gt;这里的关键点是，尽管   &lt;code&gt;server.on&lt;/code&gt; 函数跟实际 request处理代码不在一个 Tick中，它依然能通过延迟其他正在执行的代码来影响了server的性能。&lt;/p&gt;
 &lt;h2&gt;Debuging 调试&lt;/h2&gt;
 &lt;p&gt;我们现在从火焰图知道了问题函数在 util.js 的   &lt;code&gt; server.on&lt;/code&gt; 这个eventHandler里。我们来瞅一眼：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;server.on(&amp;apos;after&amp;apos;, (req, res) =&amp;gt; {
  if (res.statusCode !== 200) return
  if (!res._body) return
  const key = crypto.createHash(&amp;apos;sha512&amp;apos;)
    .update(req.url)
    .digest()
    .toString(&amp;apos;hex&amp;apos;)
  const etag = crypto.createHash(&amp;apos;sha512&amp;apos;)
    .update(JSON.stringify(res._body))
    .digest()
    .toString(&amp;apos;hex&amp;apos;)
  if (cache[key] !== etag) cache[key] = etag
})&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;众所周知，加密过程都是很昂贵的cpu密集任务，还有序列化（JSON.stringify），但是为什么火焰图中看不到呢？实际上在采样过程中都已经被记录了，只是他们隐藏在   &lt;code&gt;cpp过滤器&lt;/code&gt; 内 （译者注：cpp就是c++类型的代码）。我们点击   &lt;code&gt;cpp 按钮&lt;/code&gt;就能看到如下的样子：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#35299;&amp;#24320;&amp;#24207;&amp;#21015;&amp;#21270;&amp;#21644;&amp;#21152;&amp;#23494;&amp;#30340;&amp;#22270;" src="https://segmentfault.com/img/bVbuMyI" title="&amp;#35299;&amp;#24320;&amp;#24207;&amp;#21015;&amp;#21270;&amp;#21644;&amp;#21152;&amp;#23494;&amp;#30340;&amp;#22270;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;与序列化和加密相关的内部V8指令被展示为最热的区域堆栈，并且花费了最多的时间。   &lt;code&gt;JSON.stringify&lt;/code&gt; 方法直接调用了 C++代码，这就是为什么我们看不到JavaScript 函数。在加密这里，   &lt;code&gt;createHash&lt;/code&gt; 和   &lt;code&gt;update&lt;/code&gt; 这样的函数都在数据中，而他们要么内联(合并并消失在merge视图)要么占用时间太小无法展示。&lt;/p&gt;
 &lt;p&gt;一旦我们开始推理etagger函数中的代码，很快就会发现它的设计很糟糕。为什么我们要从函数上下文中获取服务器实例？所有这些hash计算都是必要的吗？在实际场景中也没有If-None-Match头支持，如果用if-none-match这将减轻某些真实场景中的一些负载，因为客户端会发出头请求来确定资源的新鲜度。&lt;/p&gt;
 &lt;p&gt;让我们先忽略所有这些问题，先验证一下   &lt;code&gt;server.on&lt;/code&gt; 中的代码是否是导致问题的原因。我们可以把   &lt;code&gt;server.on&lt;/code&gt; 里面的代码做成空函数然后生成一个新的火焰图。&lt;/p&gt;
 &lt;p&gt;现在 etagger 函数变成这样：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;function etagger () {
  var cache = {}
  var afterEventAttached = false
  function attachAfterEvent (server) {
    if (attachAfterEvent === true) return
    afterEventAttached = true
    server.on(&amp;apos;after&amp;apos;, (req, res) =&amp;gt; {})
  }
  return function (req, res, next) {
    attachAfterEvent(this)
    const key = crypto.createHash(&amp;apos;sha512&amp;apos;)
      .update(req.url)
      .digest()
      .toString(&amp;apos;hex&amp;apos;)
    if (key in cache) res.set(&amp;apos;Etag&amp;apos;, cache[key])
    res.set(&amp;apos;Cache-Control&amp;apos;, &amp;apos;public, max-age=120&amp;apos;)
    next()
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;现在   &lt;code&gt;server.on&lt;/code&gt; 的事件监听函数是个以空函数 no-op.  让我们再次执行 clinic flame:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic flame --on-port=&amp;apos;autocannon -c100 localhost:$PORT/seed/v1&amp;apos; -- node index.js
Copy&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;会生成如下的火焰图：  &lt;br /&gt;  &lt;img alt="server.on&amp;#20026;&amp;#31354;&amp;#20989;&amp;#25968;&amp;#21518;&amp;#30340;&amp;#28779;&amp;#28976;&amp;#22270;" src="https://segmentfault.com/img/bVbuMyK" title="server.on&amp;#20026;&amp;#31354;&amp;#20989;&amp;#25968;&amp;#21518;&amp;#30340;&amp;#28779;&amp;#28976;&amp;#22270;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这看起来好一些，我们会看到每秒吞吐量有所增长。但是为什么 event emit 的代码这么红？ 我们期望的是此时 HTTP 处理要占用最多的CPU时间，毕竟 server.on 里面已经什么都没做了。&lt;/p&gt;
 &lt;p&gt;这种类型的瓶颈通常因为一个函数调用超出了一定期望的程度。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;util.js&lt;/code&gt; 顶部的这一句可疑的代码可能是一个线索：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;require(&amp;apos;events&amp;apos;).defaultMaxListeners = Infinity&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;让我们移除掉这句代码，然后启动我们的应用，带上   &lt;code&gt;--trace-warnings&lt;/code&gt; flag标记。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;node --trace-warnings index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果我们在下一个teminal中执行压测:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;autocannon -c100 localhost:3000/seed/v1&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;会看到我们的进程输出一些：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;(node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit
  at _addListener (events.js:280:19)
  at Server.addListener (events.js:297:10)
  at attachAfterEvent 
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14)
  at Server.
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7)
  at call
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9)
  at next
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9)
  at Chain.run
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5)
  at Server._runUse
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19)
  at Server._runRoute
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10)
  at Server._afterPre
    (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;Node 告诉我们有太多的事件添加到了 server 对象上。这很奇怪，因为我们有一句判断，如果   &lt;code&gt;after&lt;/code&gt; 事件已经绑定到了 server，则直接return。所以首次绑定之后，只有一个 no-op 函数绑到了 server上。&lt;/p&gt;
 &lt;p&gt;让我们看下   &lt;code&gt;attachAfterEvent&lt;/code&gt; 函数:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;var afterEventAttached = false
function attachAfterEvent (server) {
  if (attachAfterEvent === true) return
  afterEventAttached = true
  server.on(&amp;apos;after&amp;apos;, (req, res) =&amp;gt; {})
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们发现条件检查语句写错了！ 不应该是   &lt;code&gt;attachAfterEvent&lt;/code&gt; ,而是   &lt;code&gt;afterEventAttached&lt;/code&gt;. 这意味着每个请求都会往 server 对象上添加一个事件监听，然后每个请求的最后所有的之前绑定上的事件都要触发。唉呀妈呀！&lt;/p&gt;
 &lt;h2&gt;优化&lt;/h2&gt;
 &lt;p&gt;既然知道了问题所在，让我们看看如何让我们的server更快&lt;/p&gt;
 &lt;h3&gt;低端优化 (容易摘到的果子)&lt;/h3&gt;
 &lt;p&gt;让我们还原   &lt;code&gt;server.on &lt;/code&gt; 的代码（不让他是空函数了）然后条件语句中改成正确的 boolean 判断。现在我们的 etagger 函数这样：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;function etagger () {
  var cache = {}
  var afterEventAttached = false
  function attachAfterEvent (server) {
    if (afterEventAttached === true) return
    afterEventAttached = true
    server.on(&amp;apos;after&amp;apos;, (req, res) =&amp;gt; {
      if (res.statusCode !== 200) return
      if (!res._body) return
      const key = crypto.createHash(&amp;apos;sha512&amp;apos;)
        .update(req.url)
        .digest()
        .toString(&amp;apos;hex&amp;apos;)
      const etag = crypto.createHash(&amp;apos;sha512&amp;apos;)
        .update(JSON.stringify(res._body))
        .digest()
        .toString(&amp;apos;hex&amp;apos;)
      if (cache[key] !== etag) cache[key] = etag
    })
  }
  return function (req, res, next) {
    attachAfterEvent(this)
    const key = crypto.createHash(&amp;apos;sha512&amp;apos;)
      .update(req.url)
      .digest()
      .toString(&amp;apos;hex&amp;apos;)
    if (key in cache) res.set(&amp;apos;Etag&amp;apos;, cache[key])
    res.set(&amp;apos;Cache-Control&amp;apos;, &amp;apos;public, max-age=120&amp;apos;)
    next()
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;现在，我们再来执行一次 Profile(进程剖析，进程描述)。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;node index.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后用 autocanno 来profile 它:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;autocannon -c100 localhost:3000/seed/v1&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们看到结果显示有200倍的提升（持续10秒 100个并发）&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyL" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;平衡开发成本和潜在的服务器成本也非常重要。我们需要定义我们在优化时要走多远。否则我们很容易将80%的时间投入到20%的性能提高上。项目是否能承受？&lt;/p&gt;
 &lt;p&gt;在一些场景下，用   &lt;code&gt;低端优化&lt;/code&gt; 来花费一天提高200倍速度才被认为是合理的。而在某些情况下，我们可能希望不惜一切让我们的项目尽最大最大最大可能的快。这种抉择要取决于项目优先级。&lt;/p&gt;
 &lt;p&gt;控制资源支出的一种方法是设定目标。例如，提高10倍，或达到每秒4000次请求。基于业务需求的这一种方式最有意义。例如，如果服务器成本超出预算100％，我们可以设定2倍改进的目标&lt;/p&gt;
 &lt;h3&gt;更进一步&lt;/h3&gt;
 &lt;p&gt;如果我们再做一张火焰图，我们会看到：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbuMyN" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;事件监听器依然是一个瓶颈，它依然占用了 1/3 的CPU时间 （它的宽度大约是整行的三分之一）&lt;/p&gt;
 &lt;p&gt;(译者注: 在做优化之前可能每次都要做这样的思考:) 通过优化我们能获得哪些额外收益，以及这些改变（包括相关联的代码重构）是否值得？&lt;/p&gt;
 &lt;p&gt;==============&lt;/p&gt;
 &lt;p&gt;我们看最终终极优化(译者注：终极优化指的是作者在后文提到的另外一些方法)后能达到的性能特征（持续执行十秒   &lt;a href="http://localhost" rel="nofollow noreferrer"&gt;http://localhost&lt;/a&gt;:3000/seed/v1 --- 100个并发连接）&lt;/p&gt;
 &lt;p&gt;92k requests in 11s, 937.22 MB read[15]&lt;/p&gt;
 &lt;p&gt;尽管终极优化后 1.6倍 的性能提高已经很显著了，但与之付出的努力、改变、代码重构 是否有必要也是值得商榷的。尤其是与之前简单修复一个bug就能提升200倍的性能相比。&lt;/p&gt;
 &lt;p&gt;为了实现深度改进，需要使用同样的技术如：profile分析、生成火焰图、分析、debug、优化。最后完成优化后的服务器代码，可以在  &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/tree/master/fast" rel="nofollow noreferrer"&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
 &lt;p&gt;最后提高到 800/s 的吞吐量，使用了如下方法：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;不要先创建对象然后再序列化，不如   &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/blob/master/fast/index.js#L14" rel="nofollow noreferrer"&gt;创建时就直接创建为字符串&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;用一些其他的   &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/blob/master/fast/index.js#L13" rel="nofollow noreferrer"&gt;唯一的东西来标识etag&lt;/a&gt;，而不是   &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/blob/master/fast/util.js#L25-L26" rel="nofollow noreferrer"&gt;创建hash&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;不要对url进行hash，直接   &lt;a href="https://github.com/davidmarkclements/keepings-node.js-fast/blob/master/fast/util.js#L24" rel="nofollow noreferrer"&gt;用url当key&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这些更改稍微复杂一些，对代码库的破坏性稍大一些，并使etagger中间件的灵活性稍微降低，因为它会给路由带来负担以提供Etag值。但它在执行Profile的机器上每秒可多增加3000个请求。&lt;/p&gt;
 &lt;p&gt;让我们看看最终优化后的火焰图：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#25152;&amp;#26377;&amp;#20248;&amp;#21270;&amp;#20043;&amp;#21518;&amp;#30340;&amp;#20581;&amp;#24247;&amp;#28779;&amp;#28976;&amp;#22270;" src="https://segmentfault.com/img/bVbuMyR" title="&amp;#25152;&amp;#26377;&amp;#20248;&amp;#21270;&amp;#20043;&amp;#21518;&amp;#30340;&amp;#20581;&amp;#24247;&amp;#28779;&amp;#28976;&amp;#22270;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;图中最热点的地方是 Node core（node核心）的 net 模块。这是最期望的情况。&lt;/p&gt;
 &lt;h2&gt;防止性能问题&lt;/h2&gt;
 &lt;p&gt;完美一点，这里提供一些在部署之前防止性能问题的建议。&lt;/p&gt;
 &lt;p&gt;在开发期间使用性能工具作为非正式检查点可以避免把性能问题带入生产环境。建议将AutoCannon和Clinic（或其他类似的工具）作为日常开发工具的一部分。&lt;/p&gt;
 &lt;p&gt;购买或使用一个框架时，看看他的性能政策是什么（译者注：对开源框架就看看benchmark和文档中的性能建议）。如果框架没有指出性能相关的，那么就看看他是否与你的基础架构和业务目标一致。例如，Restify已明确（自版本7发布以来）将致力于提升性。但是，如果低成本和高速度是你绝对优先考虑的问题，请考虑使用  &lt;a href="https://github.com/fastify/benchmarks/pull/51#issue-180005454" rel="nofollow noreferrer"&gt;Fastify&lt;/a&gt;，Restify贡献者测得的速度提高17％。&lt;/p&gt;
 &lt;p&gt;在选择一些广泛流行的类库时要多加留意---尤其是留意日志。 在开发者修复issue的时候，他们可能会在代码中添加一些日志输出来帮助他们在未来debug问题。如果她用了一个性能差劲的 logger 组件，这可能会像   &lt;code&gt;温水煮青蛙&lt;/code&gt; 一样随着时间的推移扼杀性能。  &lt;a href="http://getpino.io/" rel="nofollow noreferrer"&gt;pino&lt;/a&gt; 日志组件是一个 Node.js 中可以用的速度最快的JSON换行日志组件。&lt;/p&gt;
 &lt;p&gt;最后，始终记住Event Loop是一个共享资源。 Node.js服务器的性能会受到最热路径中最慢的那个逻辑的约束。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript 压测 性能 node.js</category>
      <guid isPermaLink="true">https://itindex.net/detail/59801-node-js-%E9%80%9F%E5%BA%A6</guid>
      <pubDate>Sun, 07 Jul 2019 21:00:26 CST</pubDate>
    </item>
    <item>
      <title>使用Node.js爬取任意网页资源并输出高质量PDF文件到本地~</title>
      <link>https://itindex.net/detail/59698-node-js-%E7%BD%91%E9%A1%B5</link>
      <description>&lt;p&gt;  &lt;img alt="detail?ct=503316480&amp;z=0&amp;ipn=d&amp;word=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&amp;hs=2&amp;pn=0&amp;spn=0&amp;di=10120&amp;pi=0&amp;rn=1&amp;tn=baiduimagedetail&amp;is=0%2C0&amp;ie=utf-8&amp;oe=utf-8&amp;cl=2&amp;lm=-1&amp;cs=3590237416%2C2845421745&amp;os=3026828862%2C3835093178&amp;simid=0%2C0&amp;adpicid=0&amp;lpn=0&amp;ln=30&amp;fr=ala&amp;fm=&amp;sme=&amp;cg=&amp;bdtype=0&amp;oriquery=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&amp;objurl=http%3A%2F%2Fabc.2008php.com%2F2017_Website_appreciate%2F2017-10-09%2F20171009204205.jpg&amp;fromurl=ippr_z2C%24qAzdH3FAzdH3Fwkv_z%26e3Bdaabrir_z%26e3Bv54AzdH3Fp7h7AzdH3Fda80AzdH3F8aalAzdH3Flcblld_z%26e3Bip4s&amp;gsm=0&amp;islist=&amp;querylist=" src="https://segmentfault.com/img/bVbtVeV?w=3840&amp;h=2160" title="detail?ct=503316480&amp;z=0&amp;ipn=d&amp;word=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&amp;hs=2&amp;pn=0&amp;spn=0&amp;di=10120&amp;pi=0&amp;rn=1&amp;tn=baiduimagedetail&amp;is=0%2C0&amp;ie=utf-8&amp;oe=utf-8&amp;cl=2&amp;lm=-1&amp;cs=3590237416%2C2845421745&amp;os=3026828862%2C3835093178&amp;simid=0%2C0&amp;adpicid=0&amp;lpn=0&amp;ln=30&amp;fr=ala&amp;fm=&amp;sme=&amp;cg=&amp;bdtype=0&amp;oriquery=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&amp;objurl=http%3A%2F%2Fabc.2008php.com%2F2017_Website_appreciate%2F2017-10-09%2F20171009204205.jpg&amp;fromurl=ippr_z2C%24qAzdH3FAzdH3Fwkv_z%26e3Bdaabrir_z%26e3Bv54AzdH3Fp7h7AzdH3Fda80AzdH3F8aalAzdH3Flcblld_z%26e3Bip4s&amp;gsm=0&amp;islist=&amp;querylist="&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;本文适合无论是否有爬虫以及  &lt;code&gt;Node.js&lt;/code&gt;基础的朋友观看~&lt;/blockquote&gt;
 &lt;h5&gt;需求：&lt;/h5&gt;
 &lt;ul&gt;
  &lt;li&gt;使用   &lt;code&gt;Node.js&lt;/code&gt;爬取网页资源，开箱即用的配置&lt;/li&gt;
  &lt;li&gt;将爬取到的网页内容以   &lt;code&gt;PDF&lt;/code&gt;格式输出&lt;/li&gt;
&lt;/ul&gt;
 &lt;h5&gt;如果你是一名技术人员，那么可以看我接下来的文章，否则，请直接移步到我的  &lt;code&gt;github&lt;/code&gt;仓库，直接看文档使用即可&lt;/h5&gt;
 &lt;h4&gt;仓库地址:  &lt;a href="https://github.com/JinJieTan/puppeteer-pdf" rel="nofollow noreferrer"&gt;附带文档和源码&lt;/a&gt;,别忘了给个  &lt;code&gt;star&lt;/code&gt;哦&lt;/h4&gt;
 &lt;h4&gt;本需求使用到的技术：  &lt;code&gt;Node.js&lt;/code&gt;和  &lt;code&gt;puppeteer&lt;/code&gt;
&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;puppeteer&lt;/code&gt; 官网地址:    &lt;a href="https://zhaoqize.github.io/puppeteer-api-zh_CN/#/" rel="nofollow noreferrer"&gt;puppeteer地址&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;Node.js&lt;/code&gt;官网地址:   &lt;a href="http://nodejs.cn/" rel="nofollow noreferrer"&gt;链接描述&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;Puppeteer&lt;/code&gt;是谷歌官方出品的一个通过   &lt;code&gt;DevTools&lt;/code&gt;协议控制   &lt;code&gt;headless Chrome&lt;/code&gt;的   &lt;code&gt;Node&lt;/code&gt;库。可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。&lt;/li&gt;
  &lt;li&gt;环境和安装&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;Puppeteer&lt;/code&gt;本身依赖6.4以上的Node，但是为了异步超级好用的   &lt;code&gt;async/await&lt;/code&gt;，推荐使用7.6版本以上的Node。另外headless Chrome本身对服务器依赖的库的版本要求比较高，centos服务器依赖偏稳定，v6很难使用headless Chrome，提升依赖版本可能出现各种服务器问题（包括且不限于无法使用ssh），最好使用高版本服务器。（建议使用最新版本的   &lt;code&gt;Node.js&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;小试牛刀，爬取京东资源&lt;/h4&gt;
 &lt;pre&gt;  &lt;code&gt;const puppeteer = require(&amp;apos;puppeteer&amp;apos;); //  引入依赖  
(async () =&amp;gt; {   //使用async函数完美异步 
    const browser = await puppeteer.launch();  //打开新的浏览器
    const page = await browser.newPage();   // 打开新的网页 
    await page.goto(&amp;apos;https://www.jd.com/&amp;apos;);  //前往里面 &amp;apos;url&amp;apos; 的网页
    const result = await page.evaluate(() =&amp;gt; {   //这个result数组包含所有的图片src地址
        let arr = []; //这个箭头函数内部写处理的逻辑  
        const imgs = document.querySelectorAll(&amp;apos;img&amp;apos;);
        imgs.forEach(function (item) {
            arr.push(item.src)
        })
        return arr 
    });
    // &amp;apos;此时的result就是得到的爬虫数据，可以通过&amp;apos;fs&amp;apos;模块保存&amp;apos;
})()

  复制过去 使用命令行命令 ` node 文件名 ` 就可以运行获取爬虫数据了 
这个 puppeteer 的包 ，其实是替我们开启了另一个浏览器，重新去开启网页，获取它们的数据。
&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;  &lt;li&gt;上面只爬取了京东首页的图片内容，假设我的需求进一步扩大，需要爬取京东首页&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;中的所有  &lt;code&gt;&amp;lt;a&amp;gt; 标签对应的跳转网页中的所有 title的文字内容，最后放到一个数组中&lt;/code&gt;。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;我们的   &lt;code&gt;async&lt;/code&gt;函数上面一共分了五步， 只有    &lt;code&gt;puppeteer.launch()&lt;/code&gt; ,&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;code&gt;browser.newPage()&lt;/code&gt;,   &lt;code&gt; browser.close()&lt;/code&gt; 是固定的写法。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt; page.goto&lt;/code&gt; 指定我们去哪个网页爬取数据，可以更换内部url地址，也可以多次&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;调用这个方法。&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt; page.evaluate&lt;/code&gt; 这个函数，内部是处理我们进入想要爬取网页的数据逻辑&lt;/li&gt;&lt;/ul&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;code&gt;page.goto&lt;/code&gt;和   &lt;code&gt; page.evaluate&lt;/code&gt;两个方法，可以在   &lt;code&gt;async&lt;/code&gt;内部调用多次，&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;那意味着我们可以先进入京东网页，处理逻辑后，再次调用  &lt;code&gt;page.goto&lt;/code&gt;这个函数，&lt;/p&gt;
 &lt;blockquote&gt;注意，上面这一切逻辑，都是  &lt;code&gt;puppeteer&lt;/code&gt;这个包帮我们在看不见的地方开启了另外一个  &lt;br /&gt;浏览器，然后处理逻辑，所以最终要调用  &lt;code&gt;browser.close()&lt;/code&gt;方法关闭那个浏览器。&lt;/blockquote&gt;
 &lt;p&gt;这时候我们对上一篇的代码进行优化，爬取对应的资源。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt; const puppeteer = require(&amp;apos;puppeteer&amp;apos;);
(async () =&amp;gt; {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto(&amp;apos;https://www.jd.com/&amp;apos;);
    const hrefArr = await page.evaluate(() =&amp;gt; {
        let arr = [];
        const aNodes = document.querySelectorAll(&amp;apos;.cate_menu_lk&amp;apos;);
        aNodes.forEach(function (item) {
            arr.push(item.href)
        })
        return arr
    });
    let arr = [];
    for (let i = 0; i &amp;lt; hrefArr.length; i++) {
        const url = hrefArr[i];
        console.log(url) //这里可以打印 
        await page.goto(url);
        const result = await page.evaluate(() =&amp;gt; { //这个方法内部console.log无效 
            
              return  $(&amp;apos;title&amp;apos;).text();  //返回每个界面的title文字内容
        });
        arr.push(result)  //每次循环给数组中添加对应的值
    }
    console.log(arr)  //得到对应的数据  可以通过Node.js的 fs 模块保存到本地
    await browser.close()
})()
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;上面有天坑   page.evaluate函数内部的console.log不能打印，而且内部不能获取外部的变量,只能return返回，  &lt;br /&gt;使用的选择器必须先去对应界面的控制台实验过能不能选择DOM再使用，比如京东无法使用querySelector。这里由于  &lt;br /&gt;京东的分界面都使用了jQuery，所以我们可以用jQuery，总之他们开发能用的选择器，我们都可以用，否则就不可以。&lt;/blockquote&gt;
 &lt;h4&gt;接下来我们直接来爬取  &lt;code&gt;Node.js&lt;/code&gt;的官网首页然后直接生成  &lt;code&gt;PDF&lt;/code&gt;
&lt;/h4&gt;
 &lt;h5&gt;无论您是否了解Node.js和puppeteer的爬虫的人员都可以操作，请您一定万分仔细阅读本文档并按顺序执行每一步&lt;/h5&gt;
 &lt;blockquote&gt;本项目实现需求：给我们一个网页地址，爬取他的网页内容，然后输出成我们想要的PDF格式文档，请注意，是高质量的PDF文档&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;第一步，安装   &lt;code&gt;Node.js&lt;/code&gt; ,推荐   &lt;code&gt;http://nodejs.cn/download/&lt;/code&gt;，   &lt;code&gt;Node.js&lt;/code&gt;的中文官网下载对应的操作系统包&lt;/li&gt;
  &lt;li&gt;第二步，在下载安装完了   &lt;code&gt;Node.js&lt;/code&gt;后， 启动   &lt;code&gt;windows&lt;/code&gt;命令行工具(windows下启动系统搜索功能，输入cmd，回车，就出来了)&lt;/li&gt;
  &lt;li&gt;第三步 需要查看环境变量是否已经自动配置,在命令行工具中输入    &lt;code&gt;node -v&lt;/code&gt;，如果出现    &lt;code&gt;v10. ***&lt;/code&gt;字段，则说明成功安装   &lt;code&gt;Node.js&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;第四步 如果您在第三步发现输入   &lt;code&gt;node -v&lt;/code&gt;还是没有出现 对应的字段，那么请您重启电脑即可&lt;/li&gt;
  &lt;li&gt;第五步 打开本项目文件夹，打开命令行工具（windows系统中直接在文件的   &lt;code&gt;url&lt;/code&gt;地址栏输入   &lt;code&gt;cmd&lt;/code&gt;就可以打开了），输入    &lt;code&gt;npm i cnpm  nodemon -g &lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;第六步 下载   &lt;code&gt;puppeteer&lt;/code&gt;爬虫包，在完成第五步后，使用   &lt;code&gt;cnpm i puppeteer --save &lt;/code&gt;命令 即可下载&lt;/li&gt;
  &lt;li&gt;第七步 完成第六步下载后，打开本项目的   &lt;code&gt;url.js&lt;/code&gt;，将您需要爬虫爬取的网页地址替换上去(默认是   &lt;code&gt;http://nodejs.cn/&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;第八步 在命令行中输入    &lt;code&gt;nodemon index.js&lt;/code&gt; 即可爬取对应的内容，并且自动输出到当前文件夹下面的   &lt;code&gt;index.pdf&lt;/code&gt;文件中&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;code&gt;TIPS&lt;/code&gt;: 本项目设计思想就是一个网页一个  &lt;code&gt;PDF&lt;/code&gt;文件，所以每次爬取一个单独页面后，请把  &lt;code&gt;index.pdf&lt;/code&gt;拷贝出去，然后继续更换  &lt;code&gt;url&lt;/code&gt;地址，继续爬取，生成新的  &lt;code&gt;PDF&lt;/code&gt;文件，当然，您也可以通过循环编译等方式去一次性爬取多个网页生成多个  &lt;code&gt;PDF&lt;/code&gt;文件。  &lt;p&gt;对应像京东首页这样的开启了图片懒加载的网页，爬取到的部分内容是   &lt;code&gt;loading&lt;/code&gt;状态的内容，对于有一些反爬虫机制的网页，爬虫也会出现问题，但是绝大多数网站都是可以的&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;const puppeteer = require(&amp;apos;puppeteer&amp;apos;);
const url = require(&amp;apos;./url&amp;apos;);
(async () =&amp;gt; {
    const browser = await puppeteer.launch({ headless: true })
    const page = await browser.newPage()
    //选择要打开的网页  
    await page.goto(url, { waitUntil: &amp;apos;networkidle0&amp;apos; })
    //选择你要输出的那个PDF文件路径，把爬取到的内容输出到PDF中，必须是存在的PDF，可以是空内容，如果不是空的内容PDF，那么会覆盖内容
    let pdfFilePath = &amp;apos;./index.pdf&amp;apos;;
    //根据你的配置选项，我们这里选择A4纸的规格输出PDF，方便打印
    await page.pdf({
        path: pdfFilePath,
        format: &amp;apos;A4&amp;apos;,
        scale: 1,
        printBackground: true,
        landscape: false,
        displayHeaderFooter: false
    });
    await browser.close()
})()
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;文件解构设计&lt;/h4&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bVbtVfj?w=202&amp;h=229" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;数据在这个时代非常珍贵，按照网页的设计逻辑，选定特定的  &lt;code&gt;href&lt;/code&gt;的地址，可以先直接获取对应的资源，也可以通过再次使用   &lt;code&gt; page.goto&lt;/code&gt;方法进入，再调用  &lt;code&gt; page.evaluate()&lt;/code&gt; 处理逻辑，或者输出对应的  &lt;code&gt;PDF&lt;/code&gt;文件，当然也可以一口气输出多个  &lt;code&gt;PDF&lt;/code&gt;文件~  &lt;br /&gt;这里就不做过多介绍了，毕竟  &lt;code&gt; Node.js &lt;/code&gt;是可以上天的，或许未来它真的什么都能做。这么优质简短的教程，请收藏  &lt;br /&gt;或者转发给您的朋友，谢谢。&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>html5 html css node.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59698-node-js-%E7%BD%91%E9%A1%B5</guid>
      <pubDate>Fri, 14 Jun 2019 23:55:03 CST</pubDate>
    </item>
    <item>
      <title>让前端开发者失业的技术，Flutter Web初体验</title>
      <link>https://itindex.net/detail/59622-%E5%89%8D%E7%AB%AF-%E5%BC%80%E5%8F%91-%E5%A4%B1%E4%B8%9A</link>
      <description>&lt;blockquote&gt;Flutter是一种新型的“客户端”技术。它的最终目标是替代包含几乎所有平台的开发：iOS，Android，Web，桌面；做到了一次编写，多处运行。掌握Flutter web可能是Web前端开发者翻盘的唯一机会。&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZew?w=800&amp;h=492" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在前些日子举办的Google IO 2019 年度开发者大会上，Flutter web作为一个很亮眼的技术受到了开发者的追捧。这是继Flutter支持Android、IOS等设备之后，又一个里程碑式的版本，后续还会支持windows、linux、Macos、chroms等其他嵌入式设备。Flutter本身是一个类似于RN、WEEX、hHybrid等多端统一跨平台解决方案，真正做到了一次编写，多处运行，它的发展超出了很多人的想象，值得前端开发者去关注，今天我们来体验一下Flutter Web。&lt;/p&gt;
 &lt;h2&gt;概览&lt;/h2&gt;
 &lt;p&gt;先了解一下Flutter， 它是一个由谷歌开发的开源移动应用软件开发工具包，用于为Android和iOS开发应用，同时也将是Google Fuchsia下开发应用的主要工具。自从FLutter 1.5.4版本之后，支持了Web端的开发。它采用Dart语言来进行开发，与JavaScript相比，Dart在 JIT（即时编译）模式下，速度与 JavaScript基本持平。但是当Dart以 AOT模式运行时，Dart性能要高于JavaScript。&lt;/p&gt;
 &lt;p&gt;Flutter内置了UI界面，与Hybrid App、React Native这些跨平台技术不同，Flutter既没有使用WebView，也没有使用各个平台的原生控件，而是本身实现一个统一接口的渲染引擎来绘制UI，Dart直接编译成了二进制文件，这样做可以保证不同平台UI的一致性。它也可以复用Java、Kotlin、Swift或OC代码，访问Android和iOS上的原生系统功能，比如蓝牙、相机、WiFi等等。我们公司的Now直播、企鹅辅导等项目、阿里的闲鱼等商业化项目已经大量在使用。&lt;/p&gt;
 &lt;h2&gt;架构&lt;/h2&gt;
 &lt;p&gt;  &lt;img alt="Flutter &amp;#30340; Mobile &amp;#26550;&amp;#26500;" src="https://segmentfault.com/img/bVbsZeN?w=960&amp;h=540" title="Flutter &amp;#30340; Mobile &amp;#26550;&amp;#26500;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;Flutter的顶层是用drat编写的框架，包含Material（Android风格UI）和Cupertino（iOS风格）的UI界面，下面是通用的Widgets（组件），之后是一些动画、绘制、渲染、手势库等。  &lt;br /&gt;框架下面是引擎，主要用C / C ++编写，引擎包含三个核心库，Skia是Flutter的2D渲染引擎，它是Google的一个2D图形处理函数库，包含字型、坐标转换，以及点阵图，都有高效能且简洁的表现。Skia是跨平台的，并提供了非常友好的API。第二是Dart 运行时环境以及第三文本渲染布局引擎。  &lt;br /&gt;最底层的嵌入层，它所关心的是如何将图片组合到屏幕上，渲染变成像素。这一层的功能是用来解决跨平台的。&lt;/p&gt;
 &lt;p&gt;了解了FLutter 之后，我来说一下今天的重头戏，Flutter for Web。要想知道Flutter为什么能在web上运行，得先来看看它的架构。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Flutter &amp;#30340; web&amp;#26550;&amp;#26500;" src="https://segmentfault.com/img/bVbsZeU?w=800&amp;h=327" title="Flutter &amp;#30340; web&amp;#26550;&amp;#26500;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;通过对比，可以发现，web框架层和mobile的几乎一模一样。因此只需要重新实现一下引擎和嵌入层，不用变动Flutter API就可以完全可以将UI代码从Android / IOS Flutter App移植到Web。Dart能够使用Dart2Js编译器把Dart代码编译成Js代码。大多数原生App元素能够通过DOM实现，DOM实现不了的元素可以通过Canvas来实现。&lt;/p&gt;
 &lt;h2&gt;安装&lt;/h2&gt;
 &lt;p&gt;Flutter Web开发环境搭建，以我的windows环境为例进行讲解，其他环境类似，安装环境比较繁琐，需要耐心，有Android开发经验最好。&lt;/p&gt;
 &lt;h3&gt;1、在  &lt;strong&gt;Windows平台&lt;/strong&gt;开发的话，官方的环境要求是Windows 7 SP1或更高版本（64位）。&lt;/h3&gt;
 &lt;h3&gt;2、  &lt;strong&gt;Java环境&lt;/strong&gt;，安装Java 1.8 + 版本之上，并配置环境变量，因为android开发依赖Java环境。&lt;/h3&gt;
 &lt;p&gt;对于Java程序开发而言，主要会使用JDK的两个命令：javac.exe、java.exe。路径：C:Javajdk1.8.0_181bin。但是这些命令由于不属于windows自己的命令，所以要想使用，就需要进行路径配置。单击“计算机-属性-高级系统设置”，单击“环境变量”。在“系统变量”栏下单击“新建”，创建新的系统环境变量（或用户变量，等效）。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZfv?w=561&amp;h=529" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;(1)新建-&amp;gt;变量名&amp;quot;JAVA_HOME&amp;quot;，变量值&amp;quot;C:Javajdk1.8.0_181&amp;quot;（即JDK的安装路径）   &lt;br /&gt;(2)编辑-&amp;gt;变量名&amp;quot;Path&amp;quot;，在原变量值的最后面加上“;%JAVA_HOME%bin;%JAVA_HOME%jrebin”   &lt;br /&gt;(3)新建-&amp;gt;变量名“CLASSPATH”,变量值“.;%JAVA_HOME%lib;%JAVA_HOME%libdt.jar;%JAVA_HOME%libtools.jar”&lt;/p&gt;
 &lt;h3&gt;3、  &lt;strong&gt;Android Studio编辑器&lt;/strong&gt;，安装Android Studio, 3.0或更高版本。我们需要用它来导入Android license和管理Android SDK以及Android虚拟机。（默认安装即可）&lt;/h3&gt;
 &lt;p&gt;安装完成之后设置代理，左上角的File-》setting-》搜索proxy，设置公司代理，用来加速下载Android SDK。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZfL?w=470&amp;h=387" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;之后点击右上角方盒按钮（SDK Manager），用来选择安装SDK版本，最好选Android 9版本，API28，会有一个很长时间的下载过程。SDK是开发必须的代码库。默认情况下，Flutter使用的Android SDK版本是基于你的 adb （Android Debug Bridge，管理连接手机，已打包在SDK）工具版本。 如果您想让Flutter使用不同版本的Android SDK，则必须将该 ANDROID_HOME 环境变量设置为SDK安装目录。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZf9?w=514&amp;h=357" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;右上角有个小手机类型的按钮（AVD Manager），用来设置Android模拟器，创建一个虚拟机。如果你有一台安卓手机，也可以连接USB接口，替代虚拟机。这个过程是调试必须的。安装完成之后，在 AVD (Android Virtual Device Manager) 中，点击工具栏的 Run。模拟器启动并显示所选操作系统版本或设备的启动画面。代表了正确安装。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZgr?w=508&amp;h=344" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;4、  &lt;strong&gt;安装Flutter SDK&lt;/strong&gt;
&lt;/h3&gt;
 &lt;p&gt;下载Flutter SDK有多种方法，看看哪种更适合自己：  &lt;br /&gt;Flutter官网下载最新Beta版本的进行安装：  &lt;a href="https://flutter.dev/docs/development/tools/sdk/releases" rel="nofollow noreferrer"&gt;https://flutter.dev/docs/deve...&lt;/a&gt;  &lt;br /&gt;也可Flutter github项目中去下载，地址为：  &lt;a href="https://github.com/flutter/flutter/releases" rel="nofollow noreferrer"&gt;https://github.com/flutter/fl...&lt;/a&gt;  &lt;br /&gt;版本越新越好，不要低于1.5.4。&lt;/p&gt;
 &lt;p&gt;将安装包zip解压到你想安装Flutter SDK的路径（如：C:srcflutter；注意，不要将flutter安装到需要一些高权限的路径如C:Program Files）。记住，之后往环境变量的path中添加；C:srcflutterbin，以便于你能在命令行中使用flutter。&lt;/p&gt;
 &lt;p&gt;使用镜像  &lt;br /&gt;由于在国内安装Flutter相关的依赖可能会受到限制，Flutter官方为中国开发者搭建了临时镜像，大家可以将如下环境变量加入到用户环境变量中：  &lt;br /&gt;  &lt;code&gt;PUB_HOSTED_URL：https://pub.flutter-io.cn&lt;/code&gt;  &lt;br /&gt;  &lt;code&gt;FLUTTER_STORAGE_BASE_URL： https://storage.flutter-io.cn&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZgW?w=394&amp;h=406" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;5、安装Dart与Pub。安装webdev、stagehand&lt;/h3&gt;
 &lt;p&gt;Pub是Dart的包管理工具，类似npm，捆绑安装。  &lt;br /&gt;Dart安装版地址：  &lt;a href="http://www.gekorm.com/dart-windows/" rel="nofollow noreferrer"&gt;http://www.gekorm.com/dart-wi...&lt;/a&gt;  &lt;br /&gt;默认安装即可，安装之后记住Dart的路径，并且配置到环境变量path中，以便于可以在命令行中使用dart与pub，默认的路径是：C:Program FilesDartdart-sdkbin  &lt;br /&gt;先安装stagehand，stagehand是创建项目必须的工具。查看一下  &lt;code&gt;C:\Users\chunpengliu\AppData\Roaming\Pub\Cache\bin&lt;/code&gt;目录下是否包含stagehand和webdev，如果有，添加到环境变量的path里面，如果没有，按下面方法安装：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;pub global activate stagehand&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;webdev是一个类似于Koa的web服务器，执行以下命令安装&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;pub global activate webdev
# or
flutter packages pub global activate webdev&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;6、配置编辑器安装Flutter和Dart插件&lt;/h3&gt;
 &lt;p&gt;Flutter插件是用来支持Flutter开发工作流 (运行、调试、热重载等)。  &lt;br /&gt;Dart插件 提供代码分析 (输入代码时进行验证、代码补全等)。Android Studio的设置在File-》setting-》plugins-》搜索Flutter和Dart，安装之后重启。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZh7?w=754&amp;h=462" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;VS code的设置在extension-》搜索Flutter和Dart，安装之后重启。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZh8?w=511&amp;h=365" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;7、运行 flutter doctor&lt;/h3&gt;
 &lt;p&gt;打开一个新的命令提示符或PowerShell窗口并运行以下命令以查看是否需要安装任何依赖项来完成安装：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;flutter doctor&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt; 这是一个漫长的过程，flutter会检测你的环境，并安装所有的依赖，直至：No issues found！，如果有缺失，会就会再那一项前面打x。你需要一一解决。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZiA?w=652&amp;h=201" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;一切就绪！&lt;/p&gt;
 &lt;h2&gt;创建应用&lt;/h2&gt;
 &lt;h3&gt;1、启动 VS Code&lt;/h3&gt;
 &lt;p&gt;调用 View&amp;gt;Command Palette…（快捷键ctrl+shift+p）  &lt;br /&gt;输入 ‘flutter’, 然后选择 ‘Flutter: New web Project’ &lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZiT?w=322&amp;h=158" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;输入 Project 名称 (如flutterweb), 然后按回车键  &lt;br /&gt;指定放置项目的位置，然后按蓝色的确定按钮  &lt;br /&gt;等待项目创建继续，并显示main.dart文件。到此，一个Demo创建完成。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZiW?w=954&amp;h=448" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们看到了熟悉的HTML文件以及项目入口文件main.dart。  &lt;br /&gt;web目录下的index.html是项目的入口文件。main.dart初始化文件，图片相关资源放在此目录。  &lt;br /&gt;lib目录下的main.dart，是主程序代码所在的地方。  &lt;br /&gt;每个pub包或者Flutter项目都包含一个pubspec.yaml。它包含与此项目相关的依赖项和元数据。  &lt;br /&gt;analysis_options.yaml是配置项目的lint规则。  &lt;br /&gt;/dart_tool 是项目打包运行编译生成的文件，页面主程序main.dart.js就在其中。&lt;/p&gt;
 &lt;h3&gt;2、调试Demo，打开命令行，进入到项目根目录，执行：&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;webdev flutterweb&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt; 编译、打包完成之后，自动启动（或者按F5）默认浏览器，看一下转换后的HTML页面结构：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZjv?w=736&amp;h=955" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;lib/main.dart是主程序，源码非常简单，整个页面用widgets堆叠而成，区别于传统的html和css。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import &amp;apos;package:flutter_web/material.dart&amp;apos;;

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &amp;apos;Flutter Demo&amp;apos;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: &amp;apos;Flutter Demo Home Page&amp;apos;),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &amp;lt;Widget&amp;gt;[
            Text(
              &amp;apos;Hello, World!&amp;apos;,
            ),
          ],
        ),
      ), 
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;区别与flutter App应用，我们导入的是flutter_web/material.dart库而非flutter/material.dart，这是因为目前App的接口并非和Web的完全通用，不过随着谷歌开发的继续，它们最终会被合并到一块。  &lt;br /&gt;打开pubspec.yaml（类似于package.json）,可以看到只有两个依赖包flutter_web和flutter_web_ui，这两个都已在github上开源。dev的依赖页非常少，两个编译相关的包，和一个静态文件分析包。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;name: flutterweb
description: An app built using Flutter for web
environment:
  # You must be using Flutter &amp;gt;=1.5.0 or Dart &amp;gt;=2.3.0
  sdk: &amp;apos;&amp;gt;=2.3.0-dev.0.1 &amp;lt;3.0.0&amp;apos;
dependencies:
  flutter_web: any
  flutter_web_ui: any
dev_dependencies:
  build_runner: ^1.4.0
  build_web_compilers: ^2.0.0
  pedantic: ^1.0.0
dependency_overrides:
  flutter_web:
    git:
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web
  flutter_web_ui:
    git:
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web_ui&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;实战&lt;/h2&gt;
 &lt;p&gt;接下来，我们创建一个具有图文功能的下载，根据实例来学习flutter，我们将实现下图的页面。它是一个上下两栏的布局，下栏又分为左右两栏。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbsZki?w=1258&amp;h=666" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;第一步：更改主应用内容，打开lib/main.dart文件，替换class MyApp，首先是根组件MyApp，它是一个类组件继承自无状态组件，是项目的主题配置，在home属性中调用了Home组件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class MyApp extends StatelessWidget {
  // 应用的根组件
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &amp;apos;腾讯新闻客户端下载页&amp;apos;, //meta 里的titile
      debugShowCheckedModeBanner: false, // 关闭调试bar
      theme: ThemeData(
        primarySwatch: Colors.blue, // 页面主题 Material风格
      ),
      home: Home(), // 启动首页
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;第二步，在Home类中，是我们要渲染的页面顶导，运用了AppBar组件，它包括了一个居中的页面标题和居右的搜索按钮。文本可以像css一样设置外观样式。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        backgroundColor: Colors.white,
        elevation: 0.0,
        centerTitle: true,
        title: Text( // 中心文本
          &amp;quot;下载页&amp;quot;,
          style:
              TextStyle(color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.w500),
        ),
// 搜索图标及特性
        actions: &amp;lt;Widget&amp;gt;[ 
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20.0),
            child: Icon(
              Icons.search,
              color: Colors.black,
            ),
          )
        ],
      ),
//调用body渲染类，此处可以添加多个方法调用
      body: Stack(
        children: [
            Body() 
        ],
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt; 第三步，创建页面主体内容，一张图加多个文本，使用了文本组件和图片组件，页面结构采用了flex布局，由于两个Expanded的Flex值均为1，因此将在两个组件之间平均分配空间。SizedBox组件相当于一个空盒子，用来设置margin的距离&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class Body extends StatelessWidget {
  const Body({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: &amp;lt;Widget&amp;gt;[
        Expanded( // 左侧
          flex: 1,
          child: Image.asset(// 图片组件
            &amp;quot;background-image.jpg&amp;quot;, // 这是一张在web/asserts/下的背景图
            fit: BoxFit.contain,
          ),
        ),
        const SizedBox(width: 90.0),
        Expanded( // 右侧
          flex:1,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: &amp;lt;Widget&amp;gt;[
              Text( // 文本组件
                &amp;quot;腾讯新闻&amp;quot;,
                style: TextStyle(
                    color: Colors.black, fontWeight: FontWeight.w600, fontSize: 50.0, fontFamily: &amp;apos;Merriweather&amp;apos;),
              ),
              const SizedBox(height: 14.0),// SizedBox用来增加间距
              Text(
                &amp;quot;腾讯新闻是腾讯公司为用户打造的一款全天候、全方位、及时报道的新闻产品，为用户提供高效优质的资讯、视频和直播服务。资讯超新超全，内容独家优质，话题评论互动。&amp;quot;,
                style: TextStyle(
                    color: Colors.black, fontWeight: FontWeight.w400, fontSize: 24.0, fontFamily: &amp;quot;Microsoft Yahei&amp;quot;),
                textAlign: TextAlign.justify,
              ),
              const SizedBox(height: 20.0), 
              FlatButton(
                onPressed: () {}, // 下载按钮的响应事件
                color: Color(0xFFCFE8E4),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(16.0),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Text(&amp;quot;点击下载&amp;quot;, style: TextStyle(fontFamily: &amp;quot;Open Sans&amp;quot;)),
                ),
              ),
            ],
          ),
        ),
        const SizedBox(width: 100.0),
      ],
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt; 到此，页面创建结束，保存，运行webdev serve，就可以看到效果了。&lt;/p&gt;
 &lt;h2&gt;总结&lt;/h2&gt;
 &lt;p&gt;FLutter web是Flutter 的一个分支，在开发完App之后，UI层面的FLutter代码在不修改的情况下可以直接编译为Web版，基本可以做到代码100%复用，体验还不错。目前Flutter web作为预览版无论从性能上、易用上还是布局上都超出了预期，触摸体验挺好，虽然体验比APP差一些，但是比传统的web要好很多。试想一下 Flutter 开发iOS 和Android的App 还免费赠送一份Web版，并且比传统的web开发出来的体验还好。Write once ，Run anywhere。何乐而不为？&lt;/p&gt;
 &lt;p&gt;我觉得随着谷歌的持续优化，等到正式版发布之后，开发体验越来越好，Flutter开发者会吃掉H5很大一部分份额。Flutter 可能会给目前客户端的开发模式带来一些变革以及分工的变化， Flutter目前的开发体验不是很好， 但是潜力很大，值得前端人员去学习。&lt;/p&gt;
 &lt;p&gt;但是目前还是有一部分问题，Flutter web是为客户端开发（尤其是安卓）人员开发准备的，对于前端理解来说学习成本有点高。目前FLutter web和 flutter 还是两个项目，编译环境也是分开的，需要在代码里面修改Flutter相关库的引用为Flutter_web，组件还不能达到完全通用，这个谷歌承诺正在解决中，谷歌的最终目标是Web、移动App、桌面端win mac linux、以及嵌入式版的Flutter代码库之间保持100%的代码可移植性。&lt;/p&gt;
 &lt;p&gt;个人感觉，开发体验还不太好，还有很多坑要去踩，版本变更很快。还有社区资源稀少的问题，需要一定长期的积累。兼容性问题，代码转换后大量使用了web components，除了chrome之外，兼容性还是有些问题。&lt;/p&gt;
 &lt;h2&gt;安利时间&lt;/h2&gt;
 &lt;p&gt;我们在web开发过程中，都见过或者使用过一些奇技淫巧，这种技术我们统称为黑魔法，这些黑魔法散落在各个角落，为了方便大家查阅和学习，我们做了收集、整理和归类，并在github上做了一个项目——  &lt;a href="https://github.com/Tnfe/awesome-blackmagic" rel="nofollow noreferrer"&gt;awesome-blackmargic&lt;/a&gt;，希望各位爱钻研的开发者能够喜欢，也希望大家可以把自己的独门绝技分享出来，如果有兴趣可以给我们发pr。&lt;/p&gt;
 &lt;p&gt;如果你对Flutter感兴趣，想进一步了解Flutter，加入我们的QQ群（784383520）吧！&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript flutter</category>
      <guid isPermaLink="true">https://itindex.net/detail/59622-%E5%89%8D%E7%AB%AF-%E5%BC%80%E5%8F%91-%E5%A4%B1%E4%B8%9A</guid>
      <pubDate>Wed, 22 May 2019 17:26:18 CST</pubDate>
    </item>
    <item>
      <title>基于socket.io快速实现一个实时通讯应用</title>
      <link>https://itindex.net/detail/59501-socket-io-%E5%AE%9E%E6%97%B6%E9%80%9A%E8%AE%AF</link>
      <description>&lt;p&gt;随着web技术的发展，使用场景和需求也越来越复杂，客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于各个领域。&lt;/p&gt;
 &lt;p&gt;HTTP是最常用的客户端与服务端的通信技术，但是HTTP通信只能由客户端发起，无法及时获取服务端的数据改变。只能依靠定期轮询来获取最新的状态。时效性无法保证，同时更多的请求也会增加服务器的负担。&lt;/p&gt;
 &lt;p&gt;WebSocket技术应运而生。&lt;/p&gt;
 &lt;h1&gt;WebSocket概念&lt;/h1&gt;
 &lt;p&gt;不同于HTTP半双工协议，WebSocket是基于TCP 连接的全双工协议，支持客户端服务端双向通信。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;WebSocket&lt;/code&gt;使得客户端和服务器之间的数据交换变得更加简单，允许服务端主动向客户端推送数据。在 WebSocket API 中，浏览器和服务器只需要完成一次握手，两者之间就直接可以创建持久性的连接，并进行双向数据传输。&lt;/p&gt;
 &lt;p&gt;在  &lt;code&gt;WebSocket API&lt;/code&gt;中，浏览器和服务器只需要做一个握手的动作，然后，浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="HTTP&amp;#19982;websocket&amp;#23545;&amp;#27604;" src="https://segmentfault.com/img/remote/1460000018944637" title="HTTP&amp;#19982;websocket&amp;#23545;&amp;#27604;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h1&gt;实现&lt;/h1&gt;
 &lt;h2&gt;原生实现&lt;/h2&gt;
 &lt;p&gt;WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror。&lt;/p&gt;
 &lt;h3&gt;建立连接&lt;/h3&gt;
 &lt;p&gt;通过javascript可以快速的建立一个WebSocket连接：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    var Socket = new WebSocket(url, [protocol] );&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;以上代码中的第一个参数  &lt;code&gt;url&lt;/code&gt;, 指定连接的URL。第二个参数   &lt;code&gt;protocol&lt;/code&gt;是可选的，指定了可接受的子协议。&lt;/p&gt;
 &lt;p&gt;同http协议使用  &lt;code&gt;http://&lt;/code&gt;开头一样，WebSocket协议的URL使用  &lt;code&gt;ws://&lt;/code&gt;开头，另外安全的WebSocket协议使用  &lt;code&gt;wss://&lt;/code&gt;开头。&lt;/p&gt;
 &lt;ol&gt;  &lt;li&gt;当Browser和WebSocketServer连接成功后，会触发onopen消息。&lt;/li&gt;&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;    Socket.onopen = function(evt) {};&lt;/code&gt;&lt;/pre&gt;
 &lt;ol&gt;  &lt;li&gt;如果连接失败，发送、接收数据失败或者处理数据出现错误，browser会触发onerror消息。&lt;/li&gt;&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;    Socket.onerror = function(evt) { };&lt;/code&gt;&lt;/pre&gt;
 &lt;ol&gt;  &lt;li&gt;当Browser接收到WebSocketServer端发送的关闭连接请求时，就会触发onclose消息。&lt;/li&gt;&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;    Socket.onclose = function(evt) { };&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;收发消息&lt;/h3&gt;
 &lt;ol&gt;  &lt;li&gt;当Browser接收到WebSocketServer发送过来的数据时，就会触发onmessage消息，参数evt中包含server传输过来的数据。&lt;/li&gt;&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;    Socket.onmessage = function(evt) { };&lt;/code&gt;&lt;/pre&gt;
 &lt;ol&gt;  &lt;li&gt;send用于向服务端发送消息。&lt;/li&gt;&lt;/ol&gt;
 &lt;pre&gt;  &lt;code&gt;    Socket.send();&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;socket&lt;/h2&gt;
 &lt;p&gt;WebSocket是跟随HTML5一同提出的，所以在兼容性上存在问题，这时一个非常好用的库就登场了——  &lt;a href="https://socket.io" rel="nofollow noreferrer"&gt;Socket.io&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;socket.io封装了websocket，同时包含了其它的连接方式，你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库，如果在浏览器中使用了socket.io的js，服务端也必须同样适用。&lt;/p&gt;
 &lt;p&gt;socket.io是基于 Websocket 的Client-Server 实时通信库。&lt;/p&gt;
 &lt;p&gt;socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中，不支持Websocket，为了兼容使用长轮询(polling)替代。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="engine.io" src="https://segmentfault.com/img/remote/1460000018944638" title="engine.io"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://socket.io/docs/server-api" rel="nofollow noreferrer"&gt;API文档&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;Socket.io允许你触发或响应自定义的事件，除了connect，message，disconnect这些事件的名字不能使用之外，你可以触发任何自定义的事件名称。&lt;/p&gt;
 &lt;h3&gt;建立连接&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;    const socket = io(&amp;quot;ws://0.0.0.0:port&amp;quot;); // port为自己定义的端口号
    let io = require(&amp;quot;socket.io&amp;quot;)(http);
    io.on(&amp;quot;connection&amp;quot;, function(socket) {})&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;消息收发&lt;/h3&gt;
 &lt;p&gt;一、发送数据&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    socket.emit(自定义发送的字段, data);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;二、接收数据&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    socket.on(自定义发送的字段, function(data) {
        console.log(data);
    })&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;断开连接&lt;/h3&gt;
 &lt;p&gt;一、全部断开连接&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    let io = require(&amp;quot;socket.io&amp;quot;)(http);
    io.close();&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;二、某个客户端断开与服务端的链接&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    // 客户端
    socket.emit(&amp;quot;close&amp;quot;, {});&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;    // 服务端
    socket.on(&amp;quot;close&amp;quot;, data =&amp;gt; {
        socket.disconnect(true);
    });&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;room和namespace&lt;/h3&gt;
 &lt;p&gt;有时候websocket有如下的使用场景：1.服务端发送的消息有分类，不同的客户端需要接收的分类不同；2.服务端并不需要对所有的客户端都发送消息，只需要针对某个特定群体发送消息；&lt;/p&gt;
 &lt;p&gt;针对这种使用场景，socket中非常实用的namespace和room就上场了。&lt;/p&gt;
 &lt;p&gt;先来一张图看看namespace与room之间的关系：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="namespace&amp;#19982;room&amp;#30340;&amp;#20851;&amp;#31995;" src="https://segmentfault.com/img/remote/1460000018944639" title="namespace&amp;#19982;room&amp;#30340;&amp;#20851;&amp;#31995;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;namespace&lt;/h4&gt;
 &lt;p&gt;服务端&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    io.of(&amp;quot;/post&amp;quot;).on(&amp;quot;connection&amp;quot;, function(socket) {
        socket.emit(&amp;quot;new message&amp;quot;, { mess: `这是post的命名空间` });
    });
    
    io.of(&amp;quot;/get&amp;quot;).on(&amp;quot;connection&amp;quot;, function(socket) {
        socket.emit(&amp;quot;new message&amp;quot;, { mess: `这是get的命名空间` });
    });&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;客户端&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    // index.js
    const socket = io(&amp;quot;ws://0.0.0.0:****/post&amp;quot;);
    socket.on(&amp;quot;new message&amp;quot;, function(data) {
        console.log(&amp;apos;index&amp;apos;,data);
    }
    
    //message.js
    const socket = io(&amp;quot;ws://0.0.0.0:****/get&amp;quot;);
    socket.on(&amp;quot;new message&amp;quot;, function(data) {
        console.log(&amp;apos;message&amp;apos;,data);
    }&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;room&lt;/h4&gt;
 &lt;p&gt;客户端&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    //可用于客户端进入房间;
    socket.join(&amp;apos;room one&amp;apos;);
    //用于离开房间;
    socket.leave(&amp;apos;room one&amp;apos;);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;服务端&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    io.sockets.on(&amp;apos;connection&amp;apos;,function(socket){
        //提交者会被排除在外（即不会收到消息）
        socket.broadcast.to(&amp;apos;room one&amp;apos;).emit(&amp;apos;new messages&amp;apos;, data);
        // 向所有用户发送消息
        io.sockets.to(data).emit(&amp;quot;recive message&amp;quot;, &amp;quot;hello,房间中的用户&amp;quot;);      
    }&lt;/code&gt;&lt;/pre&gt;
 &lt;h1&gt;用socket.io实现一个实时接收信息的例子&lt;/h1&gt;
 &lt;p&gt;终于来到应用的阶段啦，服务端用  &lt;code&gt;node.js&lt;/code&gt;模拟了服务端接口。以下的例子都在本地服务器中实现。&lt;/p&gt;
 &lt;h2&gt;服务端&lt;/h2&gt;
 &lt;p&gt;先来看看服务端，先来开启一个服务，安装  &lt;code&gt;express&lt;/code&gt;和  &lt;code&gt;socket.io&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;安装依赖&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;    npm install --Dev express
    npm install --Dev socket.io&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;构建node服务器&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;    let app = require(&amp;quot;express&amp;quot;)();
    let http = require(&amp;quot;http&amp;quot;).createServer(handler);
    let io = require(&amp;quot;socket.io&amp;quot;)(http);
    let fs = require(&amp;quot;fs&amp;quot;);
    
    http.listen(port); //port:输入需要的端口号
    
    function handler(req, res) {
      fs.readFile(__dirname + &amp;quot;/index.html&amp;quot;, function(err, data) {
        if (err) {
          res.writeHead(500);
          return res.end(&amp;quot;Error loading index.html&amp;quot;);
        }
    
        res.writeHead(200);
        res.end(data);
      });
    }
    
    io.on(&amp;quot;connection&amp;quot;, function(socket) {
        console.log(&amp;apos;连接成功&amp;apos;);
        //连接成功之后发送消息
        socket.emit(&amp;quot;new message&amp;quot;, { mess: `初始消息` });
        
    });&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;客户端&lt;/h2&gt;
 &lt;p&gt;核心代码——index.html（向服务端发送数据）&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    &amp;lt;div&amp;gt;发送信息&amp;lt;/div&amp;gt;
    &amp;lt;input placeholder=&amp;quot;请输入要发送的信息&amp;quot; /&amp;gt;
    &amp;lt;button onclick=&amp;quot;postMessage()&amp;quot;&amp;gt;发送&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;    // 接收到服务端传来的name匹配的消息
    socket.on(&amp;quot;new message&amp;quot;, function(data) {
      console.log(data);
    });
    
    function postMessage() {
      socket.emit(&amp;quot;recive message&amp;quot;, {
        message: content,
        time: new Date()
      });
      messList.push({
        message: content,
        time: new Date()
      });
    }&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;核心代码——message.html（从服务端接收数据）&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    socket.on(&amp;quot;new message&amp;quot;, function(data) {
      console.log(data);
    });&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;效果&lt;/h2&gt;
 &lt;p&gt;实时通讯效果  &lt;br /&gt;  &lt;img alt="&amp;#23454;&amp;#26102;&amp;#36890;&amp;#35759;&amp;#25928;&amp;#26524;" src="https://segmentfault.com/img/remote/1460000018944640" title="&amp;#23454;&amp;#26102;&amp;#36890;&amp;#35759;&amp;#25928;&amp;#26524;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;客户端全部断开连接  &lt;br /&gt;  &lt;img alt="&amp;#20840;&amp;#37096;&amp;#26029;&amp;#24320;" src="https://segmentfault.com/img/remote/1460000018944641" title="&amp;#20840;&amp;#37096;&amp;#26029;&amp;#24320;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;某客户端断开连接  &lt;br /&gt;  &lt;img alt="&amp;#26576;&amp;#23458;&amp;#25143;&amp;#31471;&amp;#26029;&amp;#24320;&amp;#36830;&amp;#25509;" src="https://segmentfault.com/img/remote/1460000018944642" title="&amp;#26576;&amp;#23458;&amp;#25143;&amp;#31471;&amp;#26029;&amp;#24320;&amp;#36830;&amp;#25509;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;namespace应用  &lt;br /&gt;  &lt;img alt="namespace" src="https://segmentfault.com/img/remote/1460000018944643" title="namespace"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;加入房间  &lt;br /&gt;  &lt;img alt="&amp;#21152;&amp;#20837;&amp;#25151;&amp;#38388;" src="https://segmentfault.com/img/remote/1460000018944644" title="&amp;#21152;&amp;#20837;&amp;#25151;&amp;#38388;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;离开房间  &lt;br /&gt;  &lt;img alt="&amp;#31163;&amp;#24320;&amp;#25151;&amp;#38388;" src="https://segmentfault.com/img/remote/1460000018944645" title="&amp;#31163;&amp;#24320;&amp;#25151;&amp;#38388;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;框架中的应用&lt;/h2&gt;
 &lt;p&gt;  &lt;code&gt;npm install socket.io-client&lt;/code&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    const socket = require(&amp;apos;socket.io-client&amp;apos;)(&amp;apos;http://localhost:port&amp;apos;);

    componentDidMount() {
        socket.on(&amp;apos;login&amp;apos;, (data) =&amp;gt; {
            console.log(data)
        });
        socket.on(&amp;apos;add user&amp;apos;, (data) =&amp;gt; {
            console.log(data)
        });
        socket.on(&amp;apos;new message&amp;apos;, (data) =&amp;gt; {
            console.log(data)
        });
    }&lt;/code&gt;&lt;/pre&gt;
 &lt;h1&gt;分析webSocket协议&lt;/h1&gt;
 &lt;h2&gt;Headers&lt;/h2&gt;
 &lt;p&gt;  &lt;img alt="Headers" src="https://segmentfault.com/img/remote/1460000018944646" title="Headers"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;请求包&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cache-Control: no-cache
    Connection: Upgrade
    Cookie: MEIQIA_VISIT_ID=1IcBRlE1mZhdVi1dEFNtGNAfjyG; token=0b81ffd758ea4a33e7724d9c67efbb26; io=ouI5Vqe7_WnIHlKnAAAG
    Host: 0.0.0.0:2699
    Origin: http://127.0.0.1:5500
    Pragma: no-cache
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: PJS0iPLxrL0ueNPoAFUSiA==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;请求包说明：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;必须是有效的http request 格式；&lt;/li&gt;
  &lt;li&gt;HTTP request method 必须是GET，协议应不小于1.1 如： Get / HTTP/1.1；&lt;/li&gt;
  &lt;li&gt;必须包括Upgrade头域，并且其值为“websocket”，用于告诉服务器此连接需要升级到websocket;&lt;/li&gt;
  &lt;li&gt;必须包括”Connection” 头域，并且其值为“Upgrade”;&lt;/li&gt;
  &lt;li&gt;必须包括”Sec-WebSocket-Key”头域，其值采用base64编码的随机16字节长的字符序列;&lt;/li&gt;
  &lt;li&gt;如果请求来自浏览器客户端，还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击，服务器可以从Origin决定是否接受该WebSocket连接;&lt;/li&gt;
  &lt;li&gt;必须包括“Sec-webSocket-Version”头域，是当前使用协议的版本号，当前值必须是13;&lt;/li&gt;
  &lt;li&gt;可能包括“Sec-WebSocket-Protocol”，表示client（应用程序）支持的协议列表，server选择一个或者没有可接受的协议响应之;&lt;/li&gt;
  &lt;li&gt;可能包括“Sec-WebSocket-Extensions”， 协议扩展， 某类协议可能支持多个扩展，通过它可以实现协议增强;&lt;/li&gt;
  &lt;li&gt;可能包括任意其他域，如cookie.&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;应答包&lt;/h3&gt;
 &lt;p&gt;应答包说明：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    Connection: Upgrade
    Sec-WebSocket-Accept: I4jyFwm0r1J8lrnD3yN+EvxTABQ=
    Sec-WebSocket-Extensions: permessage-deflate
    Upgrade: websocket&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;必须包括Upgrade头域，并且其值为“websocket”;&lt;/li&gt;
  &lt;li&gt;必须包括Connection头域，并且其值为“Upgrade”;&lt;/li&gt;
  &lt;li&gt;必须包括Sec-WebSocket-Accept头域，其值是将请求包“Sec-WebSocket-Key”的值，与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接，然后对拼接后的字符串进行sha-1运算，再进行base64编码，就是“Sec-WebSocket-Accept”的值；&lt;/li&gt;
  &lt;li&gt;应答包中冒号后面有一个空格；&lt;/li&gt;
  &lt;li&gt;最后需要两个空行作为应答包结束。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;请求数据&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;    EIO: 3
    transport: websocket
    sid: 8Uehk2UumXoHVJRzAAAA&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;EIO:3 表示使用的是engine.io协议版本3&lt;/li&gt;
  &lt;li&gt;transport 表示传输采用的类型&lt;/li&gt;
  &lt;li&gt;sid: session id （String）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;Frames&lt;/h2&gt;
 &lt;p&gt;WebSocket协议使用帧（Frame）收发数据,在控制台-&amp;gt;Frames中可以查看发送的帧数据。&lt;/p&gt;
 &lt;p&gt;其中帧数据前的数字代表什么意思呢？&lt;/p&gt;
 &lt;p&gt;这是 Engine.io协议，其中的数字是数据包编码：&lt;/p&gt;
 &lt;p&gt;&amp;lt;Packet type id&amp;gt; [&amp;lt;data&amp;gt;]&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;0 open——在打开新传输时从服务器发送（重新检查）&lt;/li&gt;
  &lt;li&gt;1 close——请求关闭此传输，但不关闭连接本身。&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;2 ping——由客户端发送。服务器应该用包含相同数据的乓包应答&lt;/p&gt;
   &lt;blockquote&gt;客户端发送：2probe探测帧&lt;/blockquote&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;3 pong——由服务器发送以响应ping数据包。&lt;/p&gt;
   &lt;blockquote&gt;服务器发送：3probe,响应客户端&lt;/blockquote&gt;
&lt;/li&gt;
  &lt;li&gt;4 message——实际消息，客户端和服务器应该使用数据调用它们的回调。&lt;/li&gt;
  &lt;li&gt;5 upgrade——在engine.io切换传输之前，它测试，如果服务器和客户端可以通过这个传输进行通信。如果此测试成功，客户端发送升级数据包，请求服务器刷新其在旧传输上的缓存并切换到新传输。&lt;/li&gt;
  &lt;li&gt;6 noop——noop数据包。主要用于在接收到传入WebSocket连接时强制轮询周期。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;实例&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#21457;&amp;#36865;&amp;#25968;&amp;#25454;" src="https://segmentfault.com/img/remote/1460000018944647" title="&amp;#21457;&amp;#36865;&amp;#25968;&amp;#25454;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#25509;&amp;#25910;&amp;#25968;&amp;#25454;" src="https://segmentfault.com/img/remote/1460000018944648" title="&amp;#25509;&amp;#25910;&amp;#25968;&amp;#25454;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;以上的截图是上述例子中数据传输的实例，分析一下大概过程就是：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;connect握手成功&lt;/li&gt;
  &lt;li&gt;客户端会发送2 probe探测帧&lt;/li&gt;
  &lt;li&gt;服务端发送响应帧3probe&lt;/li&gt;
  &lt;li&gt;客户端会发送内容为5的Upgrade帧&lt;/li&gt;
  &lt;li&gt;服务端回应内容为6的noop帧&lt;/li&gt;
  &lt;li&gt;探测帧检查通过后，客户端停止轮询请求，将传输通道转到websocket连接，转到websocket后，接下来就开始定期(默认是25秒)的 ping/pong&lt;/li&gt;
  &lt;li&gt;客户端、服务端收发数据，4表示的是engine.io的message消息，后面跟随收发的消息内容&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;为了知道Client和Server链接是否正常，项目中使用的ClientSocket和ServerSocket都有一个心跳的线程，这个线程主要是为了检测Client和Server是否正常链接，Client和Server是否正常链接主要是用ping pong流程来保证的。&lt;/p&gt;
 &lt;p&gt;该心跳定期发送的间隔是socket.io默认设定的25m，在上图中也可观察发现。该间隔可通过  &lt;a href="https://socket.io/docs/server-api/#new-Server-httpServer-options" rel="nofollow noreferrer"&gt;配置&lt;/a&gt;修改。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="socket&amp;#36890;&amp;#20449;&amp;#27969;&amp;#31243;" src="https://segmentfault.com/img/remote/1460000018944649" title="socket&amp;#36890;&amp;#20449;&amp;#27969;&amp;#31243;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;参考  &lt;a href="https://github.com/socketio/engine.io-protocol/blob/master/README.md" rel="nofollow noreferrer"&gt;engine.io-protocol&lt;/a&gt;&lt;/p&gt;
 &lt;h1&gt;参考文章&lt;/h1&gt;
 &lt;p&gt;  &lt;a href="https://mp.weixin.qq.com/s/fnRAqxA1JCWppFGBAHwreA" rel="nofollow noreferrer"&gt;Web 实时推送技术的总结&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://blog.csdn.net/u013243347/article/details/86661778" rel="nofollow noreferrer"&gt;engine.io 原理详解&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;广而告之&lt;/h2&gt;
 &lt;p&gt;本文发布于  &lt;a href="https://github.com/BooheeFE/weekly" rel="nofollow noreferrer"&gt;薄荷前端周刊&lt;/a&gt;，欢迎Watch &amp;amp; Star ★，转载请注明出处。&lt;/p&gt;
 &lt;h3&gt;欢迎讨论，点个赞再走吧  ｡◕‿◕｡ ～&lt;/h3&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>socket node.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59501-socket-io-%E5%AE%9E%E6%97%B6%E9%80%9A%E8%AE%AF</guid>
      <pubDate>Mon, 22 Apr 2019 11:12:14 CST</pubDate>
    </item>
    <item>
      <title>前端性能优化不完全手册</title>
      <link>https://itindex.net/detail/59441-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E5%AE%8C%E5%85%A8</link>
      <description>&lt;h4&gt;性能优化是一门大学问，本文仅对个人一些积累知识的阐述，欢迎下面补充。&lt;/h4&gt;
 &lt;blockquote&gt;抛出一个问题，从输入  &lt;code&gt;url&lt;/code&gt;地址栏到所有内容显示到界面上做了哪些事？&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;1.浏览器向   &lt;code&gt; DNS &lt;/code&gt;服务器请求解析该 URL 中的域名所对应的    &lt;code&gt;IP&lt;/code&gt; 地址;&lt;/li&gt;
  &lt;li&gt;2.建立   &lt;code&gt;TCP&lt;/code&gt;连接（三次握手）;&lt;/li&gt;
  &lt;li&gt;3.浏览器发出读取文件(   &lt;code&gt;URL&lt;/code&gt; 中域名后面部分对应的文件)的   &lt;code&gt;HTTP&lt;/code&gt; 请求，该请求报文作为    &lt;code&gt;TCP&lt;/code&gt; 三次握手的第三个报文的数据发送给服务器;&lt;/li&gt;
  &lt;li&gt;4.服务器对浏览器请求作出响应，并把对应的 html 文本发送给浏览器;&lt;/li&gt;
  &lt;li&gt;5.浏览器将该    &lt;code&gt;html&lt;/code&gt; 文本并显示内容;&lt;/li&gt;
  &lt;li&gt;6.释放    &lt;code&gt;TCP&lt;/code&gt;连接（四次挥手）;&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;上面这个问题是一个面试官非常喜欢问的问题，我们下面把这6个步骤分解，逐步细谈优化。&lt;/blockquote&gt;
 &lt;h4&gt;一、  &lt;code&gt;DNS&lt;/code&gt; 解析&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;p&gt;DNS`解析:将域名解析为ip地址 ,由上往下匹配，只要命中便停止&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;走缓存&lt;/li&gt;
    &lt;li&gt;浏览器DNS缓存&lt;/li&gt;
    &lt;li&gt;本机DNS缓存&lt;/li&gt;
    &lt;li&gt;路由器DNS缓存&lt;/li&gt;
    &lt;li&gt;网络运营商服务器DNS缓存 （80%的DNS解析在这完成的）&lt;/li&gt;
    &lt;li&gt;递归查询&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
 &lt;blockquote&gt;优化策略：尽量允许使用浏览器的缓存，能给我们节省大量时间。&lt;/blockquote&gt;
 &lt;h4&gt;二、  &lt;code&gt;TCP&lt;/code&gt;的三次握手&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;p&gt;SYN （同步序列编号）ACK（确认字符）&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;第一次握手：Client将标志位SYN置为1，随机产生一个值seq=J，并将该数据包发送给Server，Client进入SYN_SENT状态，等 待Server确认。&lt;/li&gt;
    &lt;li&gt;第二次握手：Server收到数据包后由标志位SYN=1知道Client请求建立连接，Server将标志位SYN和ACK都置为1，ack=J+1，随机产生一个值seq=K，并将该数据包发送给Client以确认连接请求，Server进入SYN_RCVD状态。&lt;/li&gt;
    &lt;li&gt;第三次握手：Client收到确认后，检查ack是否为J+1，ACK是否为1，如果正确则将标志位ACK置为1，ack=K+1，并将该数据包发送给Server，Server检查ack是否为K+1，ACK是否为1，如果正确则连接建立成功，Client和Server进入ESTABLISHED状态，完成三次握手，随后Client与Server之间可以开始传输数据了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
 &lt;h4&gt;三、浏览器发送请求&lt;/h4&gt;
 &lt;blockquote&gt;优化策略:&lt;/blockquote&gt;
 &lt;li&gt;  &lt;ul&gt;
   &lt;li&gt;1.    &lt;code&gt;HTTP&lt;/code&gt;协议通信最耗费时间的是建立    &lt;code&gt;TCP&lt;/code&gt;连接的过程，那我们就可以使用    &lt;code&gt;HTTP Keep-Alive&lt;/code&gt;，在    &lt;code&gt;HTTP &lt;/code&gt;早期，每个    &lt;code&gt;HTTP &lt;/code&gt;请求都要求打开一个    &lt;code&gt;TCP socket&lt;/code&gt;连接，并且使用一次之后就断开这个    &lt;code&gt;TCP&lt;/code&gt;连接。 使用    &lt;code&gt;keep-alive&lt;/code&gt;可以改善这种状态，即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用    &lt;code&gt;keep-alive&lt;/code&gt;机制，可以减少    &lt;code&gt;TCP&lt;/code&gt;连接建立次数，也意味着可以减少    &lt;code&gt;TIME_WAIT&lt;/code&gt;状态连接，以此提高性能和提高    &lt;code&gt;http&lt;/code&gt;服务器的吞吐率(更少的    &lt;code&gt;tcp&lt;/code&gt;连接意味着更少的系统内核调用&lt;/li&gt;
   &lt;li&gt;2.但是，    &lt;code&gt;keep-alive&lt;/code&gt;并不是免费的午餐,长时间的    &lt;code&gt;TCP&lt;/code&gt;连接容易导致系统资源无效占用。配置不当的    &lt;code&gt;keep-alive&lt;/code&gt;，有时比重复利用连接带来的损失还更大。所以，正确地设置    &lt;code&gt;keep-alive timeout&lt;/code&gt;时间非常重要。(这个    &lt;code&gt;keep-alive_timout&lt;/code&gt;时间值意味着：一个    &lt;code&gt;http&lt;/code&gt;产生的    &lt;code&gt;tcp&lt;/code&gt;连接在传送完最后一个响应后，还需要    &lt;code&gt;hold&lt;/code&gt;住    &lt;code&gt;keepalive_timeout&lt;/code&gt;秒后，才开始关闭这个连接)，如果想更详细了解可以看这篇文章    &lt;a href="https://www.cnblogs.com/freefish12/p/5394876.html" rel="nofollow noreferrer"&gt;keep-alve性能优化的测试结果&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
 &lt;ul&gt;
  &lt;li&gt;3.使用   &lt;code&gt;webScoket&lt;/code&gt;通信协议，仅一次   &lt;code&gt;TCP&lt;/code&gt;握手就一直保持连接，而且他对二进制数据的传输有更好的支持，可以应用于即时通信，海量高并发场景。   &lt;a href="https://www.cnblogs.com/fuqiang88/p/5956363.html" rel="nofollow noreferrer"&gt;webSocket的原理以及详解&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;4.减少   &lt;code&gt;HTTP&lt;/code&gt;请求次数，每次   &lt;code&gt;HTTP&lt;/code&gt;请求都会有请求头，返回响应都会有响应头，多次请求不仅浪费时间而且会让网络传输很多无效的资源，使用前端模块化技术    &lt;code&gt;AMD CMD commonJS ES6等模块化方案&lt;/code&gt;将多个文件压缩打包成一个，当然也不能都放在一个文件中，因为这样传输起来可能会很慢，权衡取一个中间值&lt;/li&gt;
  &lt;li&gt;5.配置使用懒加载，对于一些用户不立刻使用到的文件到特定的事件触发再请求，也许用户只是想看到你首页上半屏的内容，但是你却请求了整个页面的所有图片，如果用户量很大，那么这是一种极大的浪费&lt;/li&gt;
  &lt;li&gt;6.服务器资源的部署尽量使用同源策略&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;四、服务器返回响应，浏览器接受到响应数据&lt;/h4&gt;
 &lt;h4&gt;五、浏览器解析数据，绘制渲染页面的过程&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;先预解析（将需要发送请求的标签的请求发出去）&lt;/li&gt;
  &lt;li&gt;从上到下解析   &lt;code&gt;html&lt;/code&gt;文件&lt;/li&gt;
  &lt;li&gt;遇到HTML标签，调用html解析器将其解析   &lt;code&gt;DOM&lt;/code&gt;树&lt;/li&gt;
  &lt;li&gt;遇到   &lt;code&gt;css&lt;/code&gt;标记，调用css解析器将其解析   &lt;code&gt;CSSOM&lt;/code&gt;树&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;link&lt;/code&gt; 阻塞 - 为了解决闪屏，所有解决闪屏的样式&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;style&lt;/code&gt; 非阻塞，与闪屏的样式不相关的&lt;/li&gt;
  &lt;li&gt;将   &lt;code&gt;DOM&lt;/code&gt;树和   &lt;code&gt;CSSOM&lt;/code&gt;树结合在一起，形成   &lt;code&gt;render&lt;/code&gt;树&lt;/li&gt;
  &lt;li&gt;layout布局 render渲染&lt;/li&gt;
  &lt;li&gt;遇到   &lt;code&gt;script&lt;/code&gt;标签，阻塞，调用   &lt;code&gt;js&lt;/code&gt;解析器解析   &lt;code&gt;js&lt;/code&gt;代码，可能会修改   &lt;code&gt;DOM&lt;/code&gt;树，也可能会修改   &lt;code&gt;CSSOM&lt;/code&gt;树&lt;/li&gt;
  &lt;li&gt;将   &lt;code&gt;DOM&lt;/code&gt;树和   &lt;code&gt;CSSOM&lt;/code&gt;树结合在一起，形成   &lt;code&gt;render&lt;/code&gt;树&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;layout&lt;/code&gt;布局    &lt;code&gt;render&lt;/code&gt;渲染（重排重绘）&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;code&gt;script&lt;/code&gt;标签的属性&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;async 异步 谁先回来谁就先解析，不阻塞&lt;/li&gt;
    &lt;li&gt;defer 异步 按照先后顺序（defer）解析，不阻塞&lt;/li&gt;
    &lt;li&gt;script标签放在body下，放置多次重排重绘，能够操作dom&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;性能优化策略：&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;需要阻塞的样式使用   &lt;code&gt;link&lt;/code&gt;引入，不需要的使用   &lt;code&gt;style&lt;/code&gt;标签(具体是否需要阻塞看业务场景)&lt;/li&gt;
  &lt;li&gt;图片比较多的时候，一定要使用懒加载，图片是最需要优化的，   &lt;code&gt;webpack4&lt;/code&gt;中也要配置图片压缩，能极大压缩图片大小，对于新版本浏览器可以使用   &lt;code&gt;webp格式图片&lt;/code&gt;   &lt;a href="https://baike.baidu.com/item/webp%E6%A0%BC%E5%BC%8F/4077671?fr=aladdin" rel="nofollow noreferrer"&gt;webP详解&lt;/a&gt;，图片优化对性能提升最大。&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;webpack4&lt;/code&gt;配置 代码分割，提取公共代码成单独模块。方便缓存&lt;/li&gt;
&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;    /*
    runtimeChunk 设置为 true, webpack 就会把 chunk 文件名全部存到一个单独的 chunk 中，
    这样更新一个文件只会影响到它所在的 chunk 和 runtimeChunk，避免了引用这个 chunk 的文件也发生改变。
    */
    runtimeChunk: true, 
    splitChunks: {
      chunks: &amp;apos;all&amp;apos;  // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了
    }
  }
    //因为是单入口文件配置，所以没有考虑多入口的情况，多入口是应该分别进行处理。&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;对于需要事件驱动的   &lt;code&gt;webpack4&lt;/code&gt;配置懒加载的，可以看这篇   &lt;a href="https://segmentfault.com/a/1190000018535749"&gt;webpack4优化教程&lt;/a&gt;,写得非常全面&lt;/li&gt;
  &lt;li&gt;一些原生   &lt;code&gt;javaScript&lt;/code&gt;的   &lt;code&gt;DOM&lt;/code&gt;操作等优化会在下面总结&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;六、  &lt;code&gt;TCP&lt;/code&gt;的四次挥手，断开连接&lt;/h4&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h4&gt;终结篇：性能只是 load 时间或者 DOMContentLoaded 时间的问题吗？&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;
   &lt;p&gt;    &lt;code&gt;RAIL&lt;/code&gt;&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;
     &lt;code&gt;Responce&lt;/code&gt; 响应，研究表明，100ms内对用户的输入操作进行响应，通常会被人类认为是立即响应。时间再长，操作与反应之间的连接就会中断，人们就会觉得它的操作有延迟。例如：当用户点击一个按钮，如果100ms内给出响应，那么用户就会觉得响应很及时，不会察觉到丝毫延迟感。&lt;/li&gt;
    &lt;li&gt;
     &lt;code&gt;Animaton&lt;/code&gt; 现如今大多数设备的屏幕刷新频率是60Hz，也就是每秒钟屏幕刷新60次；因此网页动画的运行速度只要达到60FPS，我们就会觉得动画很流畅。&lt;/li&gt;
    &lt;li&gt;
     &lt;code&gt;Idle&lt;/code&gt; RAIL规定，空闲周期内运行的任务不得超过50ms，当然不止RAIL规定，W3C性能工作组的Longtasks标准也规定了超过50毫秒的任务属于长任务，那么50ms这个数字是怎么得来的呢？浏览器是单线程的，这意味着同一时间主线程只能处理一个任务，如果一个任务执行时间过长，浏览器则无法执行其他任务，用户会感觉到浏览器被卡死了，因为他的输入得不到任何响应。为了达到100ms内给出响应，将空闲周期执行的任务限制为50ms意味着，即使用户的输入行为发生在空闲任务刚开始执行，浏览器仍有剩余的50ms时间用来响应用户输入，而不会产生用户可察觉的延迟。&lt;/li&gt;
    &lt;li&gt;
     &lt;code&gt;Load&lt;/code&gt;如果不能在1秒钟内加载网页并让用户看到内容，用户的注意力就会分散。用户会觉得他要做的事情被打断，如果10秒钟还打不开网页，用户会感到失望，会放弃他们想做的事，以后他们或许都不会再回来。&lt;/li&gt;
&lt;/ul&gt;
   &lt;blockquote&gt;如何使网页更丝滑？&lt;/blockquote&gt;
   &lt;ul&gt;
    &lt;li&gt;
     &lt;p&gt;使用requestAnimationFrame&lt;/p&gt;
     &lt;ul&gt;      &lt;li&gt;即便你能保证每一帧的总耗时都小于16ms，也无法保证一定不会出现丢帧的情况，这取决于触发JS执行的方式。假设使用 setTimeout 或 setInterval 来触发JS执行并修改样式从而导致视觉变化；那么会有这样一种情况，因为setTimeout 或 setInterval没有办法保证回调函数什么时候执行，它可能在每一帧的中间执行，也可能在每一帧的最后执行。所以会导致即便我们能保障每一帧的总耗时小于16ms，但是执行的时机如果在每一帧的中间或最后，最后的结果依然是没有办法每隔16ms让屏幕产生一次变化，也就是说，即便我们能保证每一帧总体时间小于16ms，但如果使用定时器触发动画，那么由于定时器的触发时机不确定，所以还是会导致动画丢帧。现在整个Web只有一个API可以解决这个问题，那就是requestAnimationFrame，它可以保证回调函数稳定的在每一帧最开始触发。&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;避免      &lt;code&gt;FSL &lt;/code&gt;&lt;/p&gt;
     &lt;ul&gt;      &lt;li&gt;
       &lt;p&gt;先执行        &lt;code&gt;JS&lt;/code&gt;，然后在        &lt;code&gt;JS&lt;/code&gt;中修改了样式从而导致样式计算，然后样式的改动触发了布局、绘制、合成。但        &lt;code&gt;JavaScript&lt;/code&gt;可以强制浏览器将布局提前执行，这就叫 强制同步布局        &lt;code&gt;FSL&lt;/code&gt;。&lt;/p&gt;
       &lt;pre&gt;        &lt;code&gt;  //读取offsetWidth的值会导致重绘
 const newWidth = container.offsetWidth;
   
  //设置width的值会导致重排，但是for循环内部
  代码执行速度极快，当上面的查询操作导致的重绘
  还没有完成，下面的代码又会导致重排，而且这个重
  排会强制结束上面的重绘，直接重排，这样对性能影响
  非常大。所以我们一般会在循环外部定义一个变量，这里
  面使用变量代替container.offsetWidth;
 boxes[i].style.width = newWidth + &amp;apos;px&amp;apos;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
    &lt;li&gt;使用     &lt;code&gt;transform&lt;/code&gt;属性去操作动画，这个属性是由合成器单独处理的，所以使用这个属性可以避免布局与绘制。&lt;/li&gt;
    &lt;li&gt;使用     &lt;code&gt;translateZ(0)&lt;/code&gt;开启图层，减少重绘重排。特别在移动端，尽量使用     &lt;code&gt;transform&lt;/code&gt;代替     &lt;code&gt;absolute&lt;/code&gt;。创建图层的最佳方式是使用will-change，但某些不支持这个属性的浏览器可以使用3D 变形（transform: translateZ(0)）来强制创建一个新层。&lt;/li&gt;
    &lt;li&gt;有兴趣的可以看看这篇文字      &lt;a href="https://juejin.im/post/5c860282e51d45531330e10e#heading-12" rel="nofollow noreferrer"&gt;前端页面优化&lt;/a&gt;
&lt;/li&gt;
    &lt;li&gt;样式的切换最好提前定义好     &lt;code&gt;class&lt;/code&gt;，通过     &lt;code&gt;class&lt;/code&gt;的切换批量修改样式，避免多次重绘重排&lt;/li&gt;
    &lt;li&gt;可以先切换     &lt;code&gt;display:none&lt;/code&gt;再修改样式&lt;/li&gt;
    &lt;li&gt;多次的     &lt;code&gt;append &lt;/code&gt;操作可以先插入到一个新生成的元素中，再一次性插入到页面中。&lt;/li&gt;
    &lt;li&gt;代码复用，函数柯里化，封装高阶函数，将多次复用代码封装成普通函数（俗称方法），     &lt;code&gt;React&lt;/code&gt;中封装成高阶组件，     &lt;code&gt;ES6&lt;/code&gt;中可以使用继承，     &lt;code&gt;TypeScript&lt;/code&gt;中接口继承，类继承，接口合并，类合并。&lt;/li&gt;
    &lt;li&gt;强力推荐阅读:     &lt;a href="http://es6.ruanyifeng.com/#docs/promise" rel="nofollow noreferrer"&gt;阮一峰ES6教程&lt;/a&gt;
&lt;/li&gt;
    &lt;li&gt;以及     &lt;a href="https://ts.xcatliu.com/introduction/what-is-typescript.html" rel="nofollow noreferrer"&gt;什么是TypeScript以及入门&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
 &lt;blockquote&gt;以上都是根据本人的知识点总结得出，后期还会有  &lt;code&gt;React&lt;/code&gt;的性能优化方案等出来，路过点个赞收藏收藏~，欢迎提出问题补充~&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript node.js typescript css html5</category>
      <guid isPermaLink="true">https://itindex.net/detail/59441-%E5%89%8D%E7%AB%AF-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96-%E5%AE%8C%E5%85%A8</guid>
      <pubDate>Thu, 11 Apr 2019 00:06:56 CST</pubDate>
    </item>
    <item>
      <title>Javascript 面试中经常被问到的三个问题！</title>
      <link>https://itindex.net/detail/59320-javascript-%E9%9D%A2%E8%AF%95-%E9%97%AE%E9%A2%98</link>
      <description>&lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVboH5x?w=1000&amp;h=750" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;本文不是讨论最新的 JavaScript 库、常见的开发实践或任何新的 ES6 函数。相反，在讨论 JavaScript 时，面试中通常会提到三件事。我自己也被问到这些问题，我的朋友们告诉我他们也被问到这些问题。&lt;/p&gt;
 &lt;p&gt;然，这些并不是你在面试之前应该学习的唯一三件事 - 你可以通过  &lt;a href="http://jstherightway.org/#getting-started" rel="nofollow noreferrer"&gt;多种&lt;/a&gt;  &lt;a href="https://medium.com/javascript-scene/10-interview-questions-every-javascript-developer-should-know-6fa6bdf5ad95#.7fty5p61c" rel="nofollow noreferrer"&gt;方式&lt;/a&gt;更好地为即将到来的面试做准备 - 但面试官可能会问到下面是三个问题，来判断你对   &lt;code&gt;JavaScript&lt;/code&gt; 语言的理解和   &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction" rel="nofollow noreferrer"&gt;DOM&lt;/a&gt; 的掌握程度。&lt;/p&gt;
 &lt;p&gt;让我们开始吧！注意，我们将在下面的示例中使用原生的 JavaScript，因为面试官通常希望了解你在没有 jQuery 等库的帮助下对JavaScript 和 DOM 的理解程度。&lt;/p&gt;
 &lt;h3&gt;问题 1: 事件委托代理&lt;/h3&gt;
 &lt;p&gt;在构建应用程序时，有时需要将事件绑定到页面上的按钮、文本或图像，以便在用户与元素交互时执行某些操作。&lt;/p&gt;
 &lt;p&gt;如果我们以一个简单的待办事项列表为例，面试官可能会告诉你，当用户点击列表中的一个列表项时执行某些操作。他们希望你用 JavaScript 实现这个功能，假设有如下 HTML 代码:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;ul id=&amp;quot;todo-app&amp;quot;&amp;gt;
  &amp;lt;li class=&amp;quot;item&amp;quot;&amp;gt;Walk the dog&amp;lt;/li&amp;gt;
  &amp;lt;li class=&amp;quot;item&amp;quot;&amp;gt;Pay bills&amp;lt;/li&amp;gt;
  &amp;lt;li class=&amp;quot;item&amp;quot;&amp;gt;Make dinner&amp;lt;/li&amp;gt;
  &amp;lt;li class=&amp;quot;item&amp;quot;&amp;gt;Code for one hour&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;你可能想要做如下操作来将事件绑定到元素:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;document.addEventListener(&amp;apos;DOMContentLoaded&amp;apos;, function() {
  let app = document.getElementById(&amp;apos;todo-app&amp;apos;);
  let times = app.getElementsByClassName(&amp;apos;item&amp;apos;);

  for (let item of items) {
    item.addEventListener(&amp;apos;click&amp;apos;, function(){
      alert(&amp;apos;you clicked on item: &amp;apos; + item.innerHTML);
    })
  }
})
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;虽然这在技术上是可行的，但问题是要将事件分别绑定到每个项。这对于目前   &lt;code&gt;4&lt;/code&gt; 个元素来说，没什么大问题，但是如果在待办事项列表中添加了   &lt;code&gt;10,000&lt;/code&gt; 项(他们可能有很多事情要做)怎么办?然后，函数将创建 10,000 个独立的事件侦听器，并将每个事件监听器绑定到 DOM ，这样代码执行的效率非常低下。&lt;/p&gt;
 &lt;p&gt;在面试中，最好先问面试官用户可以输入的最大元素数量是多少。例如，如果它不超过   &lt;code&gt;10&lt;/code&gt;，那么上面的代码就可以很好地工作。但是如果用户可以输入的条目数量没有限制，那么你应该使用一个更高效的解决方案。&lt;/p&gt;
 &lt;p&gt;如果你的应用程序最终可能有数百个事件侦听器，那么更有效的解决方案是将一个事件侦听器实际绑定到整个容器，然后在单击它时能够访问每个列表项， 这称为   &lt;strong&gt;   &lt;a href="https://davidwalsh.name/event-delegate" rel="nofollow noreferrer"&gt;事件委托&lt;/a&gt;&lt;/strong&gt;，它比附加单独的事件处理程序更有效。&lt;/p&gt;
 &lt;p&gt;下面是事件委托的代码:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;document.addEventListener(&amp;apos;DOMContentLoaded&amp;apos;, function() {
  let app = document.getElementById(&amp;apos;todo-app&amp;apos;);

  app.addEventListener(&amp;apos;click&amp;apos;, function(e) {
    if (e.target &amp;amp;&amp;amp; e.target.nodeName === &amp;apos;LI&amp;apos;) {
      let item = e.target;
      alert(&amp;apos;you clicked on item: &amp;apos; + item.innerHTML)
    }
  })
})

&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;问题 2： 在循环中使用闭包&lt;/h2&gt;
 &lt;p&gt;闭包常常出现在面试中，以便面试官衡量你对 JS 的熟悉程度，以及你是否知道何时使用闭包。&lt;/p&gt;
 &lt;p&gt;闭包基本上是内部函数可以访问其范围之外的变量。 闭包可用于实现隐私和创建函数工厂， 闭包常见的面试题如下：&lt;/p&gt;
 &lt;blockquote&gt;编写一个函数，该函数将遍历整数列表，并在延迟3秒后打印每个元素的索引。&lt;/blockquote&gt;
 &lt;p&gt;经常不正确的写法是这样的：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const arr = [10, 12, 15, 21];
for (var i = 0; i &amp;lt; arr.length; i++) {
  setTimeout(function() {
    console.log(&amp;apos;The index of this number is: &amp;apos; + i);
  }, 3000);
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果运行上面代码，  &lt;code&gt;3&lt;/code&gt; 秒延迟后你会看到，实际上每次打印输出是   &lt;code&gt;4&lt;/code&gt;，而不是期望的   &lt;code&gt;0，1，2，3&lt;/code&gt; 。&lt;/p&gt;
 &lt;p&gt;为了正确理解为什么会发生这种情况，了解为什么会在 JavaScript 中发生这种情况将非常有用，这正是面试官试图测试的内容。&lt;/p&gt;
 &lt;p&gt;原因是因为   &lt;code&gt;setTimeout&lt;/code&gt; 函数创建了一个可以访问其外部作用域的函数（闭包），该作用域是包含索引   &lt;code&gt;i&lt;/code&gt; 的循环。 经过   &lt;code&gt;3&lt;/code&gt; 秒后，执行该函数并打印出   &lt;code&gt;i&lt;/code&gt; 的值，该值在循环结束时为   &lt;code&gt;4&lt;/code&gt;，因为它循环经过  &lt;code&gt;0,1,2,3,4&lt;/code&gt;并且循环最终停止在   &lt;code&gt;4&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;实际上有  &lt;a href="https://stackoverflow.com/questions/3572480/please-explain-the-use-of-javascript-closures-in-loops" rel="nofollow noreferrer"&gt;多处&lt;/a&gt;方法来正确的解这道题：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const arr = [10, 12, 15, 21];

for (var i = 0; i &amp;lt; arr.length; i++) {
  setTimeout(function(i_local){
    return function () {
      console.log(&amp;apos;The index of this number is: &amp;apos; + i_local);
    }
  }(i), 3000)
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;br /&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const arr = [10, 12, 15, 21];
for (let i = 0; i &amp;lt; arr.length; i++) {
  setTimeout(function() {
    console.log(&amp;apos;The index of this number is: &amp;apos; + i);
  }, 3000);
}


&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;问题 3：事件的节流（throttle）与防抖（debounce）&lt;/h2&gt;
 &lt;p&gt;有些浏览器事件可以在短时间内快速触发多次，比如调整窗口大小或向下滚动页面。例如，监听页面窗口滚动事件，并且用户持续快速地向下滚动页面，那么滚动事件可能在 3 秒内触发数千次，这可能会导致一些严重的性能问题。&lt;/p&gt;
 &lt;p&gt;如果在面试中讨论构建应用程序，出现滚动、窗口大小调整或按下键等事件请务必提及   &lt;strong&gt;防抖(Debouncing)&lt;/strong&gt; 和   &lt;strong&gt;函数节流（Throttling）&lt;/strong&gt;来提升页面速度和性能。这两兄弟的本质都是以  &lt;strong&gt;闭包&lt;/strong&gt;的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息，最后用 setTimeout 来控制事件的触发频率。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;Throttle： 第一个人说了算&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;throttle 的主要思想在于：在某段时间内，不管你触发了多少次回调，都只认第一次，并在计时结束时给予响应。&lt;/p&gt;
 &lt;p&gt;这个故事里，‘裁判’ 就是我们的节流阀， 他控制参赛者吃东西的时机， “参赛者吃东西”就是我们频繁操作事件而不断涌入的回调任务，它受 “裁判” 的控制,而计时器，就是上文提到的以自由变量形式存在的时间信息，它是 “裁判” 决定是否停止比赛的依据，最后，等待比赛结果就对应到回调函数的执行。&lt;/p&gt;
 &lt;p&gt;总结下来，所谓的“节流”，是通过在一段时间内无视后来产生的回调请求来实现的。只要 裁判宣布比赛开始，裁判就会开启计时器，在这段时间内，参赛者就尽管不断的吃，谁也无法知道最终结果。&lt;/p&gt;
 &lt;p&gt;对应到实际的交互上是一样一样的：每当用户触发了一次 scroll 事件，我们就为这个触发操作开启计时器。一段时间内，后续所有的 scroll 事件都会被当作“参赛者吃东西——它们无法触发新的 scroll 回调。直到“一段时间”到了，第一次触发的 scroll 事件对应的回调才会执行，而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。&lt;/p&gt;
 &lt;p&gt;现在一起实现一个 throttle：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// fn 是我们需要包装的事件回调, interval 是时间间隔的阈值
function throttle(fn, interval) {
  // last 为上一次触发回调时间
  let last = 0

  // 将 throttle 处理结果当然函数返回
  return function () {
    // 保留调用时的 this 上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()

    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last &amp;gt;= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值，则执行回调
      last = fn.apply(context, args)
    }
  }
}

// 用 throttle来包装 scroll 的回调
const better_scroll = throttle(() =&amp;gt; { console.log(&amp;apos;触发了滚动事件&amp;apos;)}, 1000)
document.addEventListener(&amp;apos;scroll&amp;apos;, better_scroll)
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;  &lt;strong&gt;Debounce： 最后一个参赛者说了算&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;防抖的主要思想在于：我会等你到底。在某段时间内，不管你触发了多少次回调，我都只认最后一次。&lt;/p&gt;
 &lt;p&gt;继续大胃王比赛故事，这次换了一种比赛方式，时间不限，参赛者吃到不能吃为止，当每个参赛都吃不下的时候，后面10分钟如果没有人在吃，比赛结束，如果有人在10分钟内还能吃，则比赛继续，直到下一次10分钟内无人在吃时为止。&lt;/p&gt;
 &lt;p&gt;对比 throttle 来理解 debounce： 在 throttle 的逻辑里， ‘裁判’ 说了算，当比赛时间到时，就执行回调函数。而 debounce 认为最后一个参赛者说了算，只要还能吃的，就重新设定新的定时器。&lt;/p&gt;
 &lt;p&gt;现在一起实现一个 debounce：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// fn 是我们需要包装事件回调，delay 是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null

  // 将 debounce 处理结果当作函数返回
  return function () {
    // 保留调用时的 this 上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时，都去清除之前的旧定时器
    if (timer) {
      clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function() {
      fn.apply(context, args)
    }, delay)
  }
}

// 用 debounce 来包装 scroll 的回调
const better_scroll = debounce(() =&amp;gt; { console.log(&amp;apos;发了滚动事件&amp;apos;)}, 1000)
document.addEventListener(&amp;apos;scroll&amp;apos;, better_scroll)
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;  &lt;strong&gt;用 Throttle 来优化 Debounce&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;debounce 的问题在于它“太有耐心了”。试想，如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作，于是每次 debounce 都为该用户重新生成定时器，回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应，用户同样会产生“这个页面卡死了”的观感。&lt;/p&gt;
 &lt;p&gt;为了避免弄巧成拙，我们需要借力 throttle 的思想，打造一个“有底线”的 debounce——等你可以，但我有我的原则：delay 时间内，我可以为你重新生成定时器；但只要delay的时间到了，我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路，已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last &amp;lt; delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值，则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值，那就不等了，无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() =&amp;gt; console.log(&amp;apos;触发了滚动事件&amp;apos;), 1000)

document.addEventListener(&amp;apos;scroll&amp;apos;, better_scroll)






&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;参考:&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://codeburst.io/throttling-and-debouncing-in-javascript-b01cad5c8edf#.ly8uqz8v4" rel="nofollow noreferrer"&gt;Throttling and Debouncing in JavaScript&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://css-tricks.com/the-difference-between-throttling-and-debouncing/" rel="nofollow noreferrer"&gt;The Difference Between Throttling and Debouncing&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://css-tricks.com/debouncing-throttling-explained-examples/" rel="nofollow noreferrer"&gt;Examples of Throttling and Debouncing&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://remysharp.com/2010/07/21/throttling-function-calls" rel="nofollow noreferrer"&gt;Remy Sharp’s blog post on Throttling function calls&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://juejin.im/book/5b936540f265da0a9624b04b/section/5bb6212be51d451a3f4c3570#heading-4" rel="nofollow noreferrer"&gt;前端性能优化原理与实践&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;原文：&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://medium.freecodecamp.org/3-questions-to-watch-out-for-in-a-javascript-interview-725012834ccb" rel="nofollow noreferrer"&gt;https://medium.freecodecamp.o...&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;你的点赞是我持续分享好东西的动力，欢迎点赞！&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;一个笨笨的码农，我的世界只能终身学习！&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;更多内容请关注公众号   &lt;a href="http://shiyuaniot.com:8888/daqian/daqian.jpg" rel="nofollow noreferrer"&gt;《大迁世界》&lt;/a&gt;！&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端 程序员 javascript 面试</category>
      <guid isPermaLink="true">https://itindex.net/detail/59320-javascript-%E9%9D%A2%E8%AF%95-%E9%97%AE%E9%A2%98</guid>
      <pubDate>Thu, 28 Feb 2019 18:30:00 CST</pubDate>
    </item>
    <item>
      <title>Puppeteer前端自动化测试实践</title>
      <link>https://itindex.net/detail/59298-puppeteer-%E5%89%8D%E7%AB%AF-%E8%87%AA%E5%8A%A8%E5%8C%96</link>
      <description>&lt;blockquote&gt;本篇内容将记录并介绍使用Puppeteer进行自动化网页测试，并依靠约定来避免反复修改测试用例的方案。主要解决页面众多时，修改代码导致的牵连错误无法被发现的运行时问题。文章首发于  &lt;a href="http://callmedadaxin.github.io/2019/02/18/puppeteer-auto-test/#more" rel="nofollow noreferrer"&gt;个人博客&lt;/a&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;起因&lt;/h2&gt;
 &lt;p&gt;目前我们在持续开发着一个几十个页面，十万+行代码的项目，随着产品的更迭，总会出现这样的问题。在对某些业务逻辑或者功能进行添加或者修改的时候（尤其是通用逻辑），这些通用的逻辑或者组件往往会牵扯到一些其他地方的问题。由于测试人员受限，我们很难在完成一个模块单元后，对所有功能重新测试一遍。  &lt;br /&gt;同时，由于环境及数据的区别，（以及在开发过程中对代码完备性的疏忽），代码会在某些特殊数据的解析和和展示上出现问题，在开发和测试中很难去发现。总的来说，我们希望有一个这样的工具，帮我们解决上述几个问题：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;在进行代码和功能改动后，能够自动访问各个功能的页面，检测问题&lt;/li&gt;
  &lt;li&gt;针对大量的数据内容，进行批量访问，检测对于不同数据的展示是否存在问题&lt;/li&gt;
  &lt;li&gt;测试与代码功能尽量不耦合，避免每次上新功能都需要对测试用例进行修改，维护成本太大&lt;/li&gt;
  &lt;li&gt;定期的测试任务，及时发现数据平台针对新数据的展示完备性&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;其中，最重要的问题，就是将测试代码与功能解耦，避免每次迭代和修改都需要追加新的测试用例。我们如何做到这一点呢？首先我们来梳理下测试平台的功能。&lt;/p&gt;
 &lt;h2&gt;功能设定&lt;/h2&gt;
 &lt;p&gt;由于我们的平台主要是进行数据展示，所以我们在测试过程中，主要以日常的展示数据为重心即可，针对一些复杂的表单操作先不予处理。针对上述的几个问题，我们针对自动化测试工具的功能如下：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;依次访问各个页面&lt;/li&gt;
  &lt;li&gt;访问各个页面的具体内容，如时间切换、选项卡切换、分页切换、表格展开行等等&lt;/li&gt;
  &lt;li&gt;针对数据表格中的详情链接，选择前100条进行访问，并进行下钻页的继续测试&lt;/li&gt;
  &lt;li&gt;捕获在页面中的错误请求&lt;/li&gt;
  &lt;li&gt;对错误信息进行捕获，统计和上报&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;根据以上的梳理，我们可以把整个应用分为几个测试单元&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;页面单元，检测各功能页面访问的稳定性&lt;/li&gt;
  &lt;li&gt;详情页单元，根据页面的数据列表，进行批量的详情页跳转，检测不同参数下详情页的稳定性&lt;/li&gt;
  &lt;li&gt;功能单元，用于检测页面和详情页各种展示类型点击切换后是否产生错误&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVboCE0?w=1171&amp;h=801" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;通过这样的划分，我们  &lt;strong&gt;   &lt;em&gt;针对各个单元进行具体的测试逻辑书写用例&lt;/em&gt;&lt;/strong&gt;，这样就可以避免再添加新功能和页面时，频繁对测试用例进行修改了。&lt;/p&gt;
 &lt;h2&gt;Puppeteer&lt;/h2&gt;
 &lt;p&gt;带着上面我们的需求，我们来看下Puppeteer的功能和特性，是否能够满足我们的要求。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://zhaoqize.github.io/puppeteer-api-zh_CN/#/" rel="nofollow noreferrer"&gt;文档地址&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;Puppeteer是一个Node库，它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行，但是可以通过修改配置文件运行“有头”模式。&lt;/p&gt;
 &lt;p&gt;我们可以使用Puppeteer完成以下工作:&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;访问页面，进行截图&lt;/li&gt;
  &lt;li&gt;自动进行键盘输入，提交表单&lt;/li&gt;
  &lt;li&gt;模拟点击等用户操作&lt;/li&gt;
  &lt;li&gt;等等等等。。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;我们来通过一些小案例，来介绍他们的基本功能：&lt;/p&gt;
 &lt;h3&gt;访问一个带有ba认证的网站&lt;/h3&gt;
 &lt;p&gt;puppeteer可以创建page实例，并使用goto方法进行页面访问，page包含一系列方法，可以对页面进行各种操作。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;(async () =&amp;gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // ba认证
  await page.authenticate({
    username,
    password
  });
  // 访问页面
  await page.goto(&amp;apos;https://example.com&amp;apos;);
  // 进行截图
  await page.screenshot({path: &amp;apos;example.png&amp;apos;});

  await browser.close();
})();&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;访问登陆页面，并进行登录&lt;/h3&gt;
 &lt;p&gt;首先，对于SPA(单页面应用)，我们都知道，当页面进入后，客户端代码才开始进行渲染工作。我们需要等到页面内容渲染完成后，再进行对应的操作。我们有以下几种方法来使用&lt;/p&gt;
 &lt;h4&gt;waitUntil&lt;/h4&gt;
 &lt;p&gt;puppeteer针对页面的访问，切换等，提供了waitUntil参数，来确定满足什么条件才认为页面跳转完成。包括以下事件：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;load - 页面的load事件触发时&lt;/li&gt;
  &lt;li&gt;domcontentloaded - 页面的DOMContentLoaded事件触发时&lt;/li&gt;
  &lt;li&gt;networkidle0 - 不再有网络连接时触发（至少500毫秒后）&lt;/li&gt;
  &lt;li&gt;networkidle2 - 只有2个网络连接时触发（至少500毫秒后）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;通过waitUnitl，我们可以当页面请求都完成之后，确定页面已经访问完成。&lt;/p&gt;
 &lt;h4&gt;waitFor&lt;/h4&gt;
 &lt;p&gt;waitFor方法可以在指定动作完成后才进行resolve&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// wait for selector
await page.waitFor(&amp;apos;.foo&amp;apos;);
// wait for 1 second
await page.waitFor(1000);
// wait for predicate
await page.waitFor(() =&amp;gt; !!document.querySelector(&amp;apos;.foo&amp;apos;));&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们可以利用waitForSelector方法，当登录框渲染成功后，才进行登录操作&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 等待密码输入框渲染
await page.waitFor(&amp;apos;#password&amp;apos;);
// 输入用户名
await page.type(&amp;apos;input#username&amp;apos;, &amp;quot;username&amp;quot;);
// 输入密码
await page.type(&amp;apos;input#password&amp;apos;, &amp;quot;testpass&amp;quot;);

// 点击登录按钮
await Promise.all([
  page.waitForNavigation(), // 等跳转完成后resolve
  page.click(&amp;apos;button.login-button&amp;apos;), // 点击该链接将间接导致导航(跳转)
]);

await page.waitFor(2000)

// 获取cookies
const cookies = await page.cookies()&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;针对列表内容里的链接进行批量访问&lt;/h3&gt;
 &lt;p&gt;主要利用到page实例的选择器功能&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const table = await page.$(&amp;apos;.table&amp;apos;)
const links = await table.$$eval(&amp;apos;a.link-detail&amp;apos;, links =&amp;gt;
  links.map(link =&amp;gt; link.href)
);

// 循环访问links
...&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;进行错误和访问监听&lt;/h3&gt;
 &lt;p&gt;puppeteer可以监听在页面访问过程中的报错，请求等等，这样我们就可以捕获到页面的访问错误并进行上报啦，这也是我们进行测试需要的基本功能~&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 当发生页面js代码没有捕获的异常时触发。
page.on(&amp;apos;pagerror&amp;apos;, () =&amp;gt; {})
// 当页面崩溃时触发。
page.on(&amp;apos;error&amp;apos;, () =&amp;gt; {})
// 当页面发送一个请求时触发
page.on(&amp;apos;request&amp;apos;)
// 当页面的某个请求接收到对应的 response 时触发。
page.on(&amp;apos;response&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;通过以上的几个小案例，我们发现Puppeteer的功能非常强大，完全能够满足我们以上的对页面进行自动访问的需求。接下来，我们针对我们的测试单元进行个单元用例的书写&lt;/p&gt;
 &lt;h2&gt;最终功能&lt;/h2&gt;
 &lt;p&gt;通过我们上面对测试单元的规划，我们可以规划一下我们的测试路径&lt;/p&gt;
 &lt;p&gt;访问网站 -&amp;gt; 登陆 -&amp;gt; 访问页面1 -&amp;gt; 进行基本单元测试 -&amp;gt; 获取详情页跳转链接 -&amp;gt; 依次访问详情页 -&amp;gt; 进行基本单元测试&lt;/p&gt;
 &lt;p&gt;-&amp;gt; 访问页面2 ...&lt;/p&gt;
 &lt;p&gt;所以，我们可以拆分出几个大类，和几个测试单元，来进行各项测试&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 包含基本的测试方法，log输出等
class Base {}

// 详情页单元，进行一些基本的单元测试
class PageDetal extends Base {}

// 页面单元，进行基本的单元测试，并获取并依次访问详情页
class Page extends PageDetal {}

// 进行登录等操作，并依次访问页面单元进行测试
class Root extends Base {}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;同时，我们如何在功能页面变化时，跟踪到测试的变化呢，我们可以针对我们测试的功能，为其添加自定义标签test-role，测试时，根据自定义标签进行测试逻辑的编写。&lt;/p&gt;
 &lt;p&gt;例如针对时间切换单元，我们做一下简单的介绍：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 1. 获取测试单元的元素
const timeSwitch = await page.$(&amp;apos;[test-role=&amp;quot;time-switch&amp;quot;]&amp;apos;);

// 若页面没有timeSwitch, 则不用进行测试
if (!timeSwitch) return

// 2. time switch的切换按钮
const buttons = timeSwitch.$$(&amp;apos;.time-switch-button&amp;apos;)

// 3. 对按钮进行循环点击
for (let i = 0; i &amp;lt; buttons.length; i++) {
  const button = buttons[i]

  // 点击按钮
  await button.click()

  // 重点！ 等待对应的内容出现时，才认定页面访问成功
  try {
    await page.waitFor(&amp;apos;[test-role=&amp;quot;time-switch-content&amp;quot;]&amp;apos;)
  } catch (error) {
    reportError (error)
  }

  // 截图
  await page.screenshot()
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;上面只是进行了一个简单的访问内容测试，我们可以根据我们的用例单元书写各自的测试逻辑，在我们日常开发时，只需要对需要测试的内容，加上对应的test-role即可。&lt;/p&gt;
 &lt;h2&gt;总结&lt;/h2&gt;
 &lt;p&gt;根据以上的功能划分，我们很好的将一整个应用拆分成各个测试单元进行单元测试。需要注意的是，我们目前仅仅是对页面的可访问性进行测试，仅仅验证当用户进行各种操作，访问各个页面单元时页面是否会出错。并没有对页面的具体展示效果进行测试，这样会和页面的功能内容耦合起来，就需要单独的测试用例的编写了。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript puppeteer 前端工程化 测试环境搭建</category>
      <guid isPermaLink="true">https://itindex.net/detail/59298-puppeteer-%E5%89%8D%E7%AB%AF-%E8%87%AA%E5%8A%A8%E5%8C%96</guid>
      <pubDate>Wed, 20 Feb 2019 17:33:33 CST</pubDate>
    </item>
    <item>
      <title>常见六大Web 安全攻防解析</title>
      <link>https://itindex.net/detail/59265-%E5%B8%B8%E8%A7%81-web-%E5%AE%89%E5%85%A8</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt;
 &lt;p&gt;在互联网时代，数据安全与个人隐私受到了前所未有的挑战，各种新奇的攻击技术层出不穷。如何才能更好地保护我们的数据？本文主要侧重于分析几种常见的攻击的类型以及防御的方法。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;想阅读更多优质原创文章请猛戳   &lt;a href="https://github.com/ljianshu/Blog" rel="nofollow noreferrer"&gt;GitHub博客&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;h2&gt;一、XSS&lt;/h2&gt;
 &lt;p&gt;XSS (Cross-Site Scripting)，跨站脚本攻击，因为缩写和 CSS重叠，所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。&lt;/p&gt;
 &lt;p&gt;跨站脚本攻击有可能造成以下影响:&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;利用虚假输入表单骗取用户个人信息。&lt;/li&gt;
  &lt;li&gt;利用脚本窃取用户的Cookie值，被害者在不知情的情况下，帮助攻击者发送恶意请求。&lt;/li&gt;
  &lt;li&gt;显示伪造的文章或图片。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码，当用户浏览该页之时，嵌入其中 Web 里面的脚本代码会被执行，从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;XSS 的攻击方式千变万化，但还是可以大致细分为几种类型。&lt;/p&gt;
 &lt;h3&gt;1.非持久型 XSS（反射型 XSS ）&lt;/h3&gt;
 &lt;p&gt;非持久型 XSS 漏洞，一般是通过给别人发送  &lt;strong&gt;带有恶意脚本代码参数的 URL&lt;/strong&gt;，当 URL 地址被打开时，特有的恶意代码参数被 HTML 解析、执行。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073848?w=732&amp;h=149" title=""&gt;&lt;/img&gt;  &lt;br /&gt;举一个例子，比如页面中包含有以下代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;select&amp;gt;
    &amp;lt;script&amp;gt;
        document.write(&amp;apos;&amp;apos;
            + &amp;apos;&amp;lt;option value=1&amp;gt;&amp;apos;
            +     location.href.substring(location.href.indexOf(&amp;apos;default=&amp;apos;) + 8)
            + &amp;apos;&amp;lt;/option&amp;gt;&amp;apos;
        );
        document.write(&amp;apos;&amp;lt;option value=2&amp;gt;English&amp;lt;/option&amp;gt;&amp;apos;);
    &amp;lt;/script&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;攻击者可以直接通过 URL (类似：  &lt;code&gt;https://xxx.com/xxx?default=&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;&lt;/code&gt;) 注入可执行的脚本代码。不过一些浏览器如Chrome其内置了一些XSS过滤器，可以防止大部分反射型XSS攻击。&lt;/p&gt;
 &lt;p&gt;非持久型 XSS 漏洞攻击有以下几点特征：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;即时性，不经过服务器存储，直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击，拿到用户隐私数据。&lt;/li&gt;
  &lt;li&gt;攻击者需要诱骗点击,必须要通过用户点击链接才能发起&lt;/li&gt;
  &lt;li&gt;反馈率低，所以较难发现和响应修复&lt;/li&gt;
  &lt;li&gt;盗取用户敏感保密信息&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;为了防止出现非持久型 XSS 漏洞，需要确保这么几件事情：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。&lt;/li&gt;
  &lt;li&gt;尽量不要从    &lt;code&gt;URL&lt;/code&gt;，   &lt;code&gt;document.referrer&lt;/code&gt;，   &lt;code&gt;document.forms&lt;/code&gt; 等这种 DOM API 中获取数据直接渲染。&lt;/li&gt;
  &lt;li&gt;尽量不要使用    &lt;code&gt;eval&lt;/code&gt;,    &lt;code&gt;new Function()&lt;/code&gt;，   &lt;code&gt;document.write()&lt;/code&gt;，   &lt;code&gt;document.writeln()&lt;/code&gt;，   &lt;code&gt;window.setInterval()&lt;/code&gt;，   &lt;code&gt;window.setTimeout()&lt;/code&gt;，   &lt;code&gt;innerHTML&lt;/code&gt;，   &lt;code&gt;document.createElement()&lt;/code&gt; 等可执行字符串的方法。&lt;/li&gt;
  &lt;li&gt;如果做不到以上几点，也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。&lt;/li&gt;
  &lt;li&gt;前端渲染的时候对任何的字段都需要做 escape 转义编码。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;2.持久型 XSS（存储型 XSS）&lt;/h3&gt;
 &lt;p&gt;持久型 XSS 漏洞，一般存在于 Form 表单提交等交互功能，如文章留言，提交文本信息等，黑客利用的 XSS 漏洞，将内容经正常功能提交进入数据库持久保存，当前端页面获得后端从数据库中读出的注入代码时，恰好将其渲染执行。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073849?w=595&amp;h=185" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;举个例子，对于评论功能来说，就得防范持久型 XSS 攻击，因为我可以在评论中输入以下内容&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073850?w=652&amp;h=114" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;主要注入页面方式和非持久型 XSS 漏洞类似，只不过持久型的不是来源于 URL，referer，forms 等，而是来源于  &lt;strong&gt;后端从数据库中读出来的数据&lt;/strong&gt; 。持久型 XSS 攻击不需要诱骗点击，黑客只需要在提交表单的地方完成注入即可，但是这种 XSS 攻击的成本相对还是很高。&lt;/p&gt;
 &lt;p&gt;攻击成功需要同时满足以下几个条件：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;POST 请求提交表单后端没做转义直接入库。&lt;/li&gt;
  &lt;li&gt;后端从数据库中取出数据没做转义直接输出给前端。&lt;/li&gt;
  &lt;li&gt;前端拿到后端数据没做转义直接渲染成 DOM。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;持久型 XSS 有以下几个特点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;持久性，植入在数据库中&lt;/li&gt;
  &lt;li&gt;盗取用户敏感私密信息&lt;/li&gt;
  &lt;li&gt;危害面广&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;3.如何防御&lt;/h3&gt;
 &lt;p&gt;对于 XSS 攻击来说，通常有两种方式可以用来防御。&lt;/p&gt;
 &lt;h4&gt;1) CSP&lt;/h4&gt;
 &lt;p&gt;CSP 本质上就是建立白名单，开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则，如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。&lt;/p&gt;
 &lt;p&gt;通常可以通过两种方式来开启 CSP：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;设置 HTTP Header 中的 Content-Security-Policy&lt;/li&gt;
  &lt;li&gt;设置 meta 标签的方式 &amp;lt;meta http-equiv=&amp;quot;Content-Security-Policy&amp;quot;&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这里以设置 HTTP Header 来举例：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;只允许加载本站资源&lt;/li&gt;&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;Content-Security-Policy: default-src &amp;apos;self&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;  &lt;li&gt;只允许加载 HTTPS 协议图片&lt;/li&gt;&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;Content-Security-Policy: img-src https://*&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;  &lt;li&gt;允许加载任何来源框架&lt;/li&gt;&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;Content-Security-Policy: child-src &amp;apos;none&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如需了解更多属性，请查看  &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy" rel="nofollow noreferrer"&gt;Content-Security-Policy文档&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;对于这种方式来说，只要开发者配置了正确的规则，那么即使网站存在漏洞，攻击者也不能执行它的攻击代码，并且 CSP 的兼容性也不错。&lt;/p&gt;
 &lt;h4&gt;2) 转义字符&lt;/h4&gt;
 &lt;p&gt;用户的输入永远不可信任的，最普遍的做法就是转义输入输出的内容，对于引号、尖括号、斜杠进行转义&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;function escape(str) {
  str = str.replace(/&amp;amp;/g, &amp;apos;&amp;amp;amp;&amp;apos;)
  str = str.replace(/&amp;lt;/g, &amp;apos;&amp;amp;lt;&amp;apos;)
  str = str.replace(/&amp;gt;/g, &amp;apos;&amp;amp;gt;&amp;apos;)
  str = str.replace(/&amp;quot;/g, &amp;apos;&amp;amp;quto;&amp;apos;)
  str = str.replace(/&amp;apos;/g, &amp;apos;&amp;amp;#39;&amp;apos;)
  str = str.replace(/`/g, &amp;apos;&amp;amp;#96;&amp;apos;)
  str = str.replace(/\//g, &amp;apos;&amp;amp;#x2F;&amp;apos;)
  return str
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;但是对于显示富文本来说，显然不能通过上面的办法来转义所有字符，因为这样会把需要的格式也过滤掉。对于这种情况，通常采用白名单过滤的办法，当然也可以通过黑名单过滤，但是考虑到需要过滤的标签和标签属性实在太多，更加推荐使用白名单的方式。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const xss = require(&amp;apos;xss&amp;apos;)
let html = xss(&amp;apos;&amp;lt;h1 id=&amp;quot;title&amp;quot;&amp;gt;XSS Demo&amp;lt;/h1&amp;gt;&amp;lt;script&amp;gt;alert(&amp;quot;xss&amp;quot;);&amp;lt;/script&amp;gt;&amp;apos;)
// -&amp;gt; &amp;lt;h1&amp;gt;XSS Demo&amp;lt;/h1&amp;gt;&amp;amp;lt;script&amp;amp;gt;alert(&amp;quot;xss&amp;quot;);&amp;amp;lt;/script&amp;amp;gt;
console.log(html)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;以上示例使用了 js-xss 来实现，可以看到在输出中保留了 h1 标签且过滤了 script 标签。&lt;/p&gt;
 &lt;h4&gt;3) HttpOnly Cookie。&lt;/h4&gt;
 &lt;p&gt;这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时，将其属性设为HttpOnly，就可以避免该网页的cookie被客户端恶意JavaScript窃取，保护用户cookie信息。&lt;/p&gt;
 &lt;h2&gt;二、CSRF&lt;/h2&gt;
 &lt;p&gt;CSRF(Cross Site Request Forgery)，即跨站请求伪造，是一种常见的Web攻击，它利用用户已登录的身份，在用户毫不知情的情况下，以用户的名义完成非法操作。&lt;/p&gt;
 &lt;h3&gt;1.CSRF攻击的原理&lt;/h3&gt;
 &lt;p&gt;下面先介绍一下CSRF攻击的原理：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073851?w=432&amp;h=303" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;完成 CSRF 攻击必须要有三个条件：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;用户已经登录了站点 A，并在本地记录了 cookie&lt;/li&gt;
  &lt;li&gt;在用户没有登出站点 A 的情况下（也就是 cookie 生效的情况下），访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。&lt;/li&gt;
  &lt;li&gt;站点 A 没有做任何 CSRF 防御&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;我们来看一个例子： 当我们登入转账页面后，突然眼前一亮  &lt;strong&gt;惊现&amp;quot;XXX隐私照片，不看后悔一辈子&amp;quot;的链接&lt;/strong&gt;，耐不住内心躁动，立马点击了该危险的网站（页面代码如下图所示），但当这页面一加载，便会执行  &lt;code&gt;submitForm&lt;/code&gt;这个方法来提交转账请求，从而将10块转给黑客。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073852?w=548&amp;h=388" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;2.如何防御&lt;/h3&gt;
 &lt;p&gt;防范 CSRF 攻击可以遵循以下几种规则：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Get 请求不对数据进行修改&lt;/li&gt;
  &lt;li&gt;不让第三方网站访问到用户 Cookie&lt;/li&gt;
  &lt;li&gt;阻止第三方网站请求接口&lt;/li&gt;
  &lt;li&gt;请求时附带验证信息，比如验证码或者 Token&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;1) SameSite&lt;/h4&gt;
 &lt;p&gt;可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送，可以很大程度减少 CSRF 的攻击，但是该属性目前并不是所有浏览器都兼容。&lt;/p&gt;
 &lt;h4&gt;2) Referer Check&lt;/h4&gt;
 &lt;p&gt;HTTP Referer是header的一部分，当浏览器向web服务器发送请求时，一般会带上Referer信息告诉服务器是从哪个页面链接过来的，服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律，如在提交表单的referer必定是在该页面发起的请求。所以  &lt;strong&gt;通过检查http包头referer的值是不是这个页面，来判断是不是CSRF攻击&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;但在某些情况下如从https跳转到http，浏览器处于安全考虑，不会发送referer，服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞，那么攻击者可以在其他网站注入恶意脚本，受害者进入了此类同域的网址，也会遭受攻击。出于以上原因，无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。&lt;/p&gt;
 &lt;h4&gt;3)  Anti CSRF Token&lt;/h4&gt;
 &lt;p&gt;目前比较完善的解决方案是加入Anti-CSRF-Token。即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token，并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值，会进行校验该请求当中的token和cookie当中的token值是否都存在且相等，才认为这是合法的请求。否则认为这次请求是违法的，拒绝该次服务。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;这种方法相比Referer检查要安全很多&lt;/strong&gt;，token可以在用户登陆后产生并放于session或cookie中，然后在每次请求时服务器把token从session或cookie中拿出，与本次请求中的token 进行比对。由于token的存在，攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时，当某个页面消耗掉token后，其他页面的表单保存的还是被消耗掉的那个token，其他页面的表单提交时会出现token错误。&lt;/p&gt;
 &lt;h4&gt;4) 验证码&lt;/h4&gt;
 &lt;p&gt;应用程序和用户进行交互过程中，特别是账户交易这种核心步骤，强制用户输入验证码，才能完成最终请求。在通常情况下，验证码够很好地遏制CSRF攻击。  &lt;strong&gt;但增加验证码降低了用户的体验，网站不能给所有的操作都加上验证码&lt;/strong&gt;。所以只能将验证码作为一种辅助手段，在关键业务点设置验证码。&lt;/p&gt;
 &lt;h2&gt;三、点击劫持&lt;/h2&gt;
 &lt;p&gt;点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中，并将 iframe 设置为透明，在页面中透出一个按钮诱导用户点击。&lt;/p&gt;
 &lt;h3&gt;1. 特点&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;隐蔽性较高，骗取用户操作&lt;/li&gt;
  &lt;li&gt;&amp;quot;UI-覆盖攻击&amp;quot;&lt;/li&gt;
  &lt;li&gt;利用iframe或者其它标签的属性&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;2. 点击劫持的原理&lt;/h3&gt;
 &lt;p&gt;用户在登陆 A 网站的系统后，被攻击者诱惑打开第三方网站，而第三方网站通过 iframe 引入了 A 网站的页面内容，用户在第三方网站中点击某个按钮（被装饰的按钮），实际上是点击了 A 网站的按钮。  &lt;br /&gt;接下来我们举个例子：我在优酷发布了很多视频，想让更多的人关注它，就可以通过点击劫持来实现&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;iframe {
width: 1440px;
height: 900px;
position: absolute;
top: -0px;
left: -0px;
z-index: 2;
-moz-opacity: 0;
opacity: 0;
filter: alpha(opacity=0);
}
button {
position: absolute;
top: 270px;
left: 1150px;
z-index: 1;
width: 90px;
height:40px;
}
&amp;lt;/style&amp;gt;
......
&amp;lt;button&amp;gt;点击脱衣&amp;lt;/button&amp;gt;
&amp;lt;img src=&amp;quot;http://pic1.win4000.com/wallpaper/2018-03-19/5aaf2bf0122d2.jpg&amp;quot;&amp;gt;
&amp;lt;iframe src=&amp;quot;http://i.youku.com/u/UMjA0NTg4Njcy&amp;quot; scrolling=&amp;quot;no&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073853?w=1259&amp;h=283" title=""&gt;&lt;/img&gt;  &lt;br /&gt;从上图可知，攻击者通过图片作为页面背景，隐藏了用户操作的真实界面，当你按耐不住好奇点击按钮以后，真正的点击的其实是隐藏的那个页面的订阅按钮，然后就会在你不知情的情况下订阅了。  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073854?w=643&amp;h=276" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;3. 如何防御&lt;/h3&gt;
 &lt;h4&gt;1）X-FRAME-OPTIONS&lt;/h4&gt;
 &lt;p&gt;  &lt;code&gt;X-FRAME-OPTIONS&lt;/code&gt;是一个 HTTP 响应头，在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。&lt;/p&gt;
 &lt;p&gt;该响应头有三个值可选，分别是&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;DENY，表示页面不允许通过 iframe 的方式展示&lt;/li&gt;
  &lt;li&gt;SAMEORIGIN，表示页面可以在相同域名下通过 iframe 的方式展示&lt;/li&gt;
  &lt;li&gt;ALLOW-FROM，表示页面可以在指定来源的 iframe 中展示&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;2）JavaScript 防御&lt;/h4&gt;
 &lt;p&gt;对于某些远古浏览器来说，并不能支持上面的这种方式，那我们只有通过 JS 的方式来防御点击劫持了。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;style id=&amp;quot;click-jack&amp;quot;&amp;gt;
    html {
      display: none !important;
    }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;script&amp;gt;
    if (self == top) {
      var style = document.getElementById(&amp;apos;click-jack&amp;apos;)
      document.body.removeChild(style)
    } else {
      top.location = self.location
    }
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;以上代码的作用就是当通过 iframe 的方式加载页面时，攻击者的网页直接不显示所有内容了。&lt;/p&gt;
 &lt;h2&gt;四、URL跳转漏洞&lt;/h2&gt;
 &lt;p&gt;定义：借助未验证的URL跳转，将应用程序引导到不安全的第三方区域，从而导致的安全问题。&lt;/p&gt;
 &lt;h3&gt;1.URL跳转漏洞原理&lt;/h3&gt;
 &lt;p&gt;黑客利用URL跳转漏洞来诱导安全意识低的用户点击，导致用户信息泄露或者资金的流失。其原理是黑客构建恶意链接(链接需要进行伪装,尽可能迷惑),发在QQ群或者是浏览量多的贴吧/论坛中。  &lt;br /&gt;安全意识低的用户点击后,经过服务器或者浏览器解析后，跳到恶意的网站中。  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073855?w=741&amp;h=134" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;恶意链接需要进行伪装,经常的做法是熟悉的链接后面加上一个恶意的网址，这样才迷惑用户。  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073856?w=865&amp;h=165" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;诸如伪装成像如下的网址，你是否能够识别出来是恶意网址呢？&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;http://gate.baidu.com/index?act=go&amp;amp;url=http://t.cn/RVTatrd
http://qt.qq.com/safecheck.html?flag=1&amp;amp;url=http://t.cn/RVTatrd
http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;2.实现方式：&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;Header头跳转&lt;/li&gt;
  &lt;li&gt;Javascript跳转&lt;/li&gt;
  &lt;li&gt;META标签跳转&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这里我们举个Header头跳转实现方式：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;?php
$url=$_GET[&amp;apos;jumpto&amp;apos;];
header(&amp;quot;Location: $url&amp;quot;);
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;http://www.wooyun.org/login.php?jumpto=http://www.evil.com&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这里用户会认为  &lt;code&gt;www.wooyun.org&lt;/code&gt;都是可信的，但是点击上述链接将导致用户最终访问  &lt;code&gt;www.evil.com&lt;/code&gt;这个恶意网址。&lt;/p&gt;
 &lt;h3&gt;3.如何防御&lt;/h3&gt;
 &lt;h4&gt;1)referer的限制&lt;/h4&gt;
 &lt;p&gt;如果确定传递URL参数进入的来源，我们可以通过该方式实现安全限制，保证该URL的有效性，避免恶意用户自己生成跳转链接&lt;/p&gt;
 &lt;h4&gt;2)加入有效性验证Token&lt;/h4&gt;
 &lt;p&gt;我们保证所有生成的链接都是来自于我们可信域的，通过在生成的链接里加入用户不可控的Token对生成的链接进行校验，可以避免用户生成自己的恶意链接从而被利用，但是如果功能本身要求比较开放，可能导致有一定的限制。&lt;/p&gt;
 &lt;h2&gt;五、SQL注入&lt;/h2&gt;
 &lt;p&gt;SQL注入是一种常见的Web安全漏洞，攻击者利用这个漏洞，可以访问或修改数据，或者利用潜在的数据库漏洞进行攻击。&lt;/p&gt;
 &lt;h3&gt;1.SQL注入的原理&lt;/h3&gt;
 &lt;p&gt;我们先举一个万能钥匙的例子来说明其原理：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073857?w=827&amp;h=323" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;form action=&amp;quot;/login&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;
    &amp;lt;p&amp;gt;Username: &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;username&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Password: &amp;lt;input type=&amp;quot;password&amp;quot; name=&amp;quot;password&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;input type=&amp;quot;submit&amp;quot; value=&amp;quot;登陆&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;后端的 SQL 语句可能是如下这样的：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;let querySQL = `
    SELECT *
    FROM user
    WHERE username=&amp;apos;${username}&amp;apos;
    AND psw=&amp;apos;${password}&amp;apos;
`;
// 接下来就是执行 sql 语句...&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这是我们经常见到的登录页面，但如果有一个恶意攻击者输入的用户名是   &lt;code&gt;admin&amp;apos; --&lt;/code&gt;，密码随意输入，就可以直接登入系统了。why! ----这就是SQL注入&lt;/p&gt;
 &lt;p&gt;我们之前预想的SQL 语句是:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;SELECT * FROM user WHERE username=&amp;apos;admin&amp;apos; AND psw=&amp;apos;password&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;SELECT * FROM user WHERE username=&amp;apos;admin&amp;apos; --&amp;apos; AND psw=&amp;apos;xxxx&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在 SQL 中,  &lt;code&gt;&amp;apos; --&lt;/code&gt;是闭合和注释的意思，-- 是注释后面的内容的意思，所以查询语句就变成了：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;SELECT * FROM user WHERE username=&amp;apos;admin&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;所谓的万能密码,本质上就是SQL注入的一种利用方式。&lt;/p&gt;
 &lt;p&gt;一次SQL注入的过程包括以下几个过程：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;获取用户请求参数&lt;/li&gt;
  &lt;li&gt;拼接到代码当中&lt;/li&gt;
  &lt;li&gt;SQL语句按照我们构造参数的语义执行成功&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;**SQL注入的必备条件：  &lt;br /&gt;1.可以控制输入的数据  &lt;br /&gt;2.服务器要执行的代码拼接了控制的数据**。  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073858?w=716&amp;h=179" title=""&gt;&lt;/img&gt;  &lt;br /&gt;我们会发现SQL注入流程中与正常请求服务器类似，只是黑客控制了数据，构造了SQL查询，而正常的请求不会SQL查询这一步，  &lt;strong&gt;SQL注入的本质:数据和代码未分离，即数据当做了代码来执行。&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;2.危害&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;获取数据库信息&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;管理员后台用户名和密码&lt;/li&gt;
    &lt;li&gt;获取其他数据库敏感信息：用户名、密码、手机号码、身份证、银行卡信息……&lt;/li&gt;
    &lt;li&gt;整个数据库：脱裤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;获取服务器权限&lt;/li&gt;
  &lt;li&gt;植入Webshell，获取服务器后门&lt;/li&gt;
  &lt;li&gt;读取服务器敏感文件&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;3.如何防御&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;strong&gt;严格限制Web应用的数据库的操作权限&lt;/strong&gt;，给此用户提供仅仅能够满足其工作的最低权限，从而最大限度的减少注入攻击对数据库的危害&lt;/li&gt;
  &lt;li&gt;
   &lt;strong&gt;后端代码检查输入的数据是否符合预期&lt;/strong&gt;，严格限制变量的类型，例如使用正则表达式进行一些匹配处理。&lt;/li&gt;
  &lt;li&gt;
   &lt;strong&gt;对进入数据库的特殊字符（&amp;apos;，&amp;quot;，，&amp;lt;，&amp;gt;，&amp;amp;，*，; 等）进行转义处理，或编码转换&lt;/strong&gt;。基本上所有的后端语言都有对字符串进行转义处理的方法，比如 lodash 的 lodash._escapehtmlchar 库。&lt;/li&gt;
  &lt;li&gt;
   &lt;strong&gt;所有的查询语句建议使用数据库提供的参数化查询接口&lt;/strong&gt;，参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中，即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;六、OS命令注入攻击&lt;/h2&gt;
 &lt;p&gt;OS命令注入和SQL注入差不多，只不过SQL注入是针对数据库的，而OS命令注入是针对操作系统的。OS命令注入攻击指通过Web应用，执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏，就可以执行插入的非法命令。&lt;/p&gt;
 &lt;p&gt;命令注入攻击可以向Shell发送命令，让Windows或Linux操作系统的命令行启动程序。也就是说，通过命令注入攻击可执行操作系统上安装着的各种程序。&lt;/p&gt;
 &lt;h3&gt;1.原理&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000018073859?w=657&amp;h=154" title=""&gt;&lt;/img&gt;  &lt;br /&gt;黑客构造命令提交给web应用程序，web应用程序提取黑客构造的命令，拼接到被执行的命令中，因黑客注入的命令打破了原有命令结构，导致web应用执行了额外的命令，最后web应用程序将执行的结果输出到响应页面中。&lt;/p&gt;
 &lt;p&gt;我们通过一个例子来说明其原理，假如需要实现一个需求：用户提交一些内容到服务器，然后在服务器执行一些系统命令去返回一个结果给用户&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 以 Node.js 为例，假如在接口中需要从 github 下载用户指定的 repo
const exec = require(&amp;apos;mz/child_process&amp;apos;).exec;
let params = {/* 用户输入的参数 */};
exec(`git clone ${params.repo} /some/path`);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果   &lt;code&gt;params.repo&lt;/code&gt; 传入的是   &lt;code&gt;https://github.com/admin/admin.github.io.git&lt;/code&gt; 确实能从指定的 git repo 上下载到想要的代码。  &lt;br /&gt;但是如果   &lt;code&gt;params.repo&lt;/code&gt; 传入的是   &lt;code&gt;https://github.com/xx/xx.git &amp;amp;&amp;amp; rm -rf /* &amp;amp;&amp;amp;&lt;/code&gt; 恰好你的服务是用 root 权限起的就糟糕了。&lt;/p&gt;
 &lt;h3&gt;2.如何防御&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;后端对前端提交内容进行规则限制（比如正则表达式）。&lt;/li&gt;
  &lt;li&gt;在调用系统命令前对所有传入参数进行命令行参数转义过滤。&lt;/li&gt;
  &lt;li&gt;不要直接拼接命令语句，借助一些工具做拼接、转义预处理，例如 Node.js 的    &lt;code&gt;shell-escape npm&lt;/code&gt;包&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;给大家推荐一个好用的BUG监控工具   &lt;a href="https://www.fundebug.com/?utm_source=liao" rel="nofollow noreferrer"&gt;Fundebug&lt;/a&gt;，欢迎免费试用！&lt;/strong&gt;&lt;/p&gt;
 &lt;h2&gt;参考资料&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://zoumiaojiang.com/article/common-web-security/" rel="nofollow noreferrer"&gt;常见Web 安全攻防总结&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://juejin.im/book/5bdc715fe51d454e755f75ef" rel="nofollow noreferrer"&gt;前端面试之道&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/25863515/" rel="nofollow noreferrer"&gt;图解Http&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://wetest.qq.com/lab/view/136.html" rel="nofollow noreferrer"&gt;Web安全知多少&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://blog.csdn.net/qq_32523587/article/details/79613768" rel="nofollow noreferrer"&gt;web安全之点击劫持(clickjacking)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://drops.xmd5.com/static/drops/papers-58.html" rel="nofollow noreferrer"&gt;URL重定向/跳转漏洞&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://mooc.study.163.com/smartSpec/detail/1001227001.htm" rel="nofollow noreferrer"&gt;网易web白帽子&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>node.js java javascript 前端 程序员</category>
      <guid isPermaLink="true">https://itindex.net/detail/59265-%E5%B8%B8%E8%A7%81-web-%E5%AE%89%E5%85%A8</guid>
      <pubDate>Thu, 31 Jan 2019 09:06:34 CST</pubDate>
    </item>
    <item>
      <title>Node.js 指南（HTTP事务的剖析）</title>
      <link>https://itindex.net/detail/59109-node-js-http</link>
      <description>&lt;h1&gt;HTTP事务的剖析&lt;/h1&gt;
 &lt;p&gt;本指南的目的是让你充分了解Node.js HTTP处理的过程，我们假设你在一般意义上知道HTTP请求的工作方式，无论语言或编程环境如何，我们还假设你对Node.js   &lt;code&gt;EventEmitters&lt;/code&gt;和  &lt;code&gt;Streams&lt;/code&gt;有点熟悉，如果你对它们不太熟悉，那么值得快速阅读每个API文档。&lt;/p&gt;
 &lt;h2&gt;创建服务器&lt;/h2&gt;
 &lt;p&gt;任何节点Web服务器应用程序在某些时候都必须创建Web服务器对象，这是通过使用  &lt;code&gt;createServer&lt;/code&gt;完成的。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

const server = http.createServer((request, response) =&amp;gt; {
  // magic happens here!
});&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;传递给  &lt;code&gt;createServer&lt;/code&gt;的函数对于针对该服务器发出的每个HTTP请求都会调用一次，因此它被称为请求处理程序，实际上，  &lt;code&gt;createServer&lt;/code&gt;返回的  &lt;code&gt;Server&lt;/code&gt;对象是一个  &lt;code&gt;EventEmitter&lt;/code&gt;，我们这里只是创建  &lt;code&gt;server&lt;/code&gt;对象的简写，然后稍后添加监听器。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const server = http.createServer();
server.on(&amp;apos;request&amp;apos;, (request, response) =&amp;gt; {
  // the same kind of magic happens here!
});&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;当HTTP请求命中服务器时，node使用一些方便的对象调用请求处理函数来处理事务、  &lt;code&gt;request&lt;/code&gt;和  &lt;code&gt;response&lt;/code&gt;，我们很快就会讲到。&lt;/p&gt;
 &lt;p&gt;为了实际处理请求，需要在  &lt;code&gt;server&lt;/code&gt;对象上调用  &lt;code&gt;listen&lt;/code&gt;方法，在大多数情况下，你需要传递给  &lt;code&gt;listen&lt;/code&gt;的是你希望服务器监听的端口号，还有一些其他选项，请参阅API参考。&lt;/p&gt;
 &lt;h2&gt;方法、URL和Headers&lt;/h2&gt;
 &lt;p&gt;处理请求时，你可能要做的第一件事就是查看方法和URL，以便采取适当的措施，Node通过将方便的属性放在  &lt;code&gt;request&lt;/code&gt;对象上来使这相对轻松。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const { method, url } = request;&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;注意：  &lt;code&gt;request&lt;/code&gt;对象是  &lt;code&gt;IncomingMessage&lt;/code&gt;的一个实例。&lt;/blockquote&gt;
 &lt;p&gt;这里的  &lt;code&gt;method&lt;/code&gt;将始终是普通的HTTP方法/动作，  &lt;code&gt;url&lt;/code&gt;是没有服务器、协议或端口的完整URL，对于典型的URL，这意味着包括第三个正斜杠后的所有内容。&lt;/p&gt;
 &lt;p&gt;Headers也不远，它们在自己的  &lt;code&gt;request&lt;/code&gt;对象中，被称为  &lt;code&gt;headers&lt;/code&gt;。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const { headers } = request;
const userAgent = headers[&amp;apos;user-agent&amp;apos;];&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这里需要注意的是，无论客户端实际发送它们的方式如何，所有headers都仅以小写字母表示，这简化了为任何目的解析headers的任务。&lt;/p&gt;
 &lt;p&gt;如果重复某些headers，则它们的值将被覆盖或以逗号分隔的字符串连接在一起，具体取决于header，在某些情况下，这可能会有问题，因此  &lt;code&gt;rawHeaders&lt;/code&gt;也可用。&lt;/p&gt;
 &lt;h2&gt;请求体&lt;/h2&gt;
 &lt;p&gt;收到  &lt;code&gt;POST&lt;/code&gt;或  &lt;code&gt;PUT&lt;/code&gt;请求时，请求体可能对你的应用程序很重要，获取body数据比访问请求headers更复杂一点，传递给处理程序的  &lt;code&gt;request&lt;/code&gt;对象实现了  &lt;code&gt;ReadableStream&lt;/code&gt;接口，就像任何其他流一样，可以在其他地方监听或传输此流，我们可以通过监听流的  &lt;code&gt;&amp;apos;data&amp;apos;&lt;/code&gt;和  &lt;code&gt;&amp;apos;end&amp;apos;&lt;/code&gt;事件来直接从流中获取数据。&lt;/p&gt;
 &lt;p&gt;每个  &lt;code&gt;&amp;apos;data&amp;apos;&lt;/code&gt;事件中发出的块是一个  &lt;code&gt;Buffer&lt;/code&gt;，如果你知道它将是字符串数据，那么最好的方法是在数组中收集数据，然后在  &lt;code&gt;&amp;apos;end&amp;apos;&lt;/code&gt;，连接并对其进行字符串化。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;let body = [];
request.on(&amp;apos;data&amp;apos;, (chunk) =&amp;gt; {
  body.push(chunk);
}).on(&amp;apos;end&amp;apos;, () =&amp;gt; {
  body = Buffer.concat(body).toString();
  // at this point, `body` has the entire request body stored in it as a string
});&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;注意：这看起来有点单调乏味，而且在很多情况下确实如此，幸运的是，在  &lt;a href="https://www.npmjs.com/" rel="nofollow noreferrer"&gt;npm&lt;/a&gt;上有像  &lt;a href="https://www.npmjs.com/package/concat-stream" rel="nofollow noreferrer"&gt;concat-stream&lt;/a&gt;和  &lt;a href="https://www.npmjs.com/package/body" rel="nofollow noreferrer"&gt;body&lt;/a&gt;这样的模块可以帮助隐藏一些逻辑，在走这条路之前，要很好地了解正在发生的事情，这就是为什么你在这里！&lt;/blockquote&gt;
 &lt;h2&gt;关于错误的简单介绍&lt;/h2&gt;
 &lt;p&gt;由于  &lt;code&gt;request&lt;/code&gt;对象是一个  &lt;code&gt;ReadableStream&lt;/code&gt;，它也是一个  &lt;code&gt;EventEmitter&lt;/code&gt;，发生错误时的行为与此类似。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;request&lt;/code&gt;流中的错误通过在流上发出  &lt;code&gt;&amp;apos;error&amp;apos;&lt;/code&gt;事件来呈现，如果你没有该事件的侦听器，则会抛出错误，这可能会导致Node.js程序崩溃。因此，你应该在请求流上添加  &lt;code&gt;&amp;apos;error&amp;apos;&lt;/code&gt;侦听器，即使你只是记录它并继续前进（虽然最好发送某种HTTP错误响应，稍后会详细介绍）。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;request.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
  // This prints the error message and stack trace to `stderr`.
  console.error(err.stack);
});&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;还有其他方法可以处理这些错误，例如其他抽象和工具，但始终要注意错误可能并且确实会发生，并且你将不得不处理它们。&lt;/p&gt;
 &lt;h2&gt;到目前为止我们已经得到了什么&lt;/h2&gt;
 &lt;p&gt;此时，我们已经介绍了如何创建服务器，并从请求中获取方法、URL、headers和body，当我们将它们放在一起时，它可能看起来像这样：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  const { headers, method, url } = request;
  let body = [];
  request.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
    console.error(err);
  }).on(&amp;apos;data&amp;apos;, (chunk) =&amp;gt; {
    body.push(chunk);
  }).on(&amp;apos;end&amp;apos;, () =&amp;gt; {
    body = Buffer.concat(body).toString();
    // At this point, we have the headers, method, url and body, and can now
    // do whatever we need to in order to respond to this request.
  });
}).listen(8080); // Activates this server, listening on port 8080.&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果我们运行此示例，我们将能够接收请求，但不会响应它们，实际上，如果你在Web浏览器中请求此示例，则你的请求将超时，因为没有任何内容被发送回客户端。&lt;/p&gt;
 &lt;p&gt;到目前为止，我们还没有涉及响应对象，它是  &lt;code&gt;ServerResponse&lt;/code&gt;的一个实例，它是一个  &lt;code&gt;WritableStream&lt;/code&gt;，它包含许多用于将数据发送回客户端的有用方法，接下来我们将介绍。&lt;/p&gt;
 &lt;h2&gt;HTTP状态码&lt;/h2&gt;
 &lt;p&gt;如果不设置它，响应中的HTTP状态码始终为  &lt;code&gt;200&lt;/code&gt;，当然，并非每个HTTP响应都保证这一点，并且在某些时候你肯定希望发送不同的状态码，为此，你可以设置  &lt;code&gt;statusCode&lt;/code&gt;属性。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;response.statusCode = 404; // Tell the client that the resource wasn&amp;apos;t found.&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;还有其他一些快捷方式，我们很快就会看到。&lt;/p&gt;
 &lt;h2&gt;设置响应Headers&lt;/h2&gt;
 &lt;p&gt;Headers是通过一个名为  &lt;code&gt;setHeader&lt;/code&gt;的方便方法设置的。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;response.setHeader(&amp;apos;Content-Type&amp;apos;, &amp;apos;application/json&amp;apos;);
response.setHeader(&amp;apos;X-Powered-By&amp;apos;, &amp;apos;bacon&amp;apos;);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在响应上设置headers时，大小写对其名称不敏感，如果重复设置标题，则设置的最后一个值是发送的值。&lt;/p&gt;
 &lt;h2&gt;显式发送Header数据&lt;/h2&gt;
 &lt;p&gt;我们已经讨论过的设置headers和状态码的方法假设你正在使用“隐式headers”，这意味着在开始发送body数据之前，你需要依赖node在正确的时间为你发送headers。&lt;/p&gt;
 &lt;p&gt;如果需要，可以将headers显式写入响应流，为此，有一个名为  &lt;code&gt;writeHead&lt;/code&gt;的方法，它将状态码和headers写入流。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;response.writeHead(200, {
  &amp;apos;Content-Type&amp;apos;: &amp;apos;application/json&amp;apos;,
  &amp;apos;X-Powered-By&amp;apos;: &amp;apos;bacon&amp;apos;
});&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;一旦设置了headers（隐式或显式），你就可以开始发送响应数据了。&lt;/p&gt;
 &lt;h2&gt;发送响应体&lt;/h2&gt;
 &lt;p&gt;由于  &lt;code&gt;response&lt;/code&gt;对象是  &lt;code&gt;WritableStream&lt;/code&gt;，因此将响应体写入客户端只需使用常用的流方法即可。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;response.write(&amp;apos;&amp;lt;html&amp;gt;&amp;apos;);
response.write(&amp;apos;&amp;lt;body&amp;gt;&amp;apos;);
response.write(&amp;apos;&amp;lt;h1&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;&amp;apos;);
response.write(&amp;apos;&amp;lt;/body&amp;gt;&amp;apos;);
response.write(&amp;apos;&amp;lt;/html&amp;gt;&amp;apos;);
response.end();&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;流上的  &lt;code&gt;end&lt;/code&gt;函数也可以接收一些可选数据作为流上的最后一位数据发送，因此我们可以如下简化上面的示例。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;response.end(&amp;apos;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;apos;);&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;注意：在开始向body写入数据块之前设置状态和headers很重要，这是有道理的，因为headers在HTTP响应中位于body之前。&lt;/blockquote&gt;
 &lt;h2&gt;关于错误的另一件事&lt;/h2&gt;
 &lt;p&gt;  &lt;code&gt;response&lt;/code&gt;流也可以发出  &lt;code&gt;&amp;apos;error&amp;apos;&lt;/code&gt;事件，在某些时候你也必须处理它，所有关于  &lt;code&gt;request&lt;/code&gt;流错误的建议仍然适用于此处。&lt;/p&gt;
 &lt;h2&gt;把它放在一起&lt;/h2&gt;
 &lt;p&gt;现在我们已经了解了如何进行HTTP响应，让我们把它们放在一起，在前面的示例的基础上，我们将创建一个服务器，用于发回用户发送给我们的所有数据，我们将使用  &lt;code&gt;JSON.stringify&lt;/code&gt;将该数据格式化为JSON。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  const { headers, method, url } = request;
  let body = [];
  request.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
    console.error(err);
  }).on(&amp;apos;data&amp;apos;, (chunk) =&amp;gt; {
    body.push(chunk);
  }).on(&amp;apos;end&amp;apos;, () =&amp;gt; {
    body = Buffer.concat(body).toString();
    // BEGINNING OF NEW STUFF

    response.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
      console.error(err);
    });

    response.statusCode = 200;
    response.setHeader(&amp;apos;Content-Type&amp;apos;, &amp;apos;application/json&amp;apos;);
    // Note: the 2 lines above could be replaced with this next one:
    // response.writeHead(200, {&amp;apos;Content-Type&amp;apos;: &amp;apos;application/json&amp;apos;})

    const responseBody = { headers, method, url, body };

    response.write(JSON.stringify(responseBody));
    response.end();
    // Note: the 2 lines above could be replaced with this next one:
    // response.end(JSON.stringify(responseBody))

    // END OF NEW STUFF
  });
}).listen(8080);&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;Echo服务器示例&lt;/h2&gt;
 &lt;p&gt;让我们简化前面的示例来进行一个简单的echo服务器，它只是在响应中发送请求中收到的任何数据，我们需要做的就是从请求流中获取数据并将该数据写入响应流，类似于我们之前所做的。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  let body = [];
  request.on(&amp;apos;data&amp;apos;, (chunk) =&amp;gt; {
    body.push(chunk);
  }).on(&amp;apos;end&amp;apos;, () =&amp;gt; {
    body = Buffer.concat(body).toString();
    response.end(body);
  });
}).listen(8080);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;现在让我们调整一下，我们只想在以下条件下发送echo：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;请求方法是   &lt;code&gt;POST&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;URL是/echo。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;在任何其他情况下，我们只想响应  &lt;code&gt;404&lt;/code&gt;。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  if (request.method === &amp;apos;POST&amp;apos; &amp;amp;&amp;amp; request.url === &amp;apos;/echo&amp;apos;) {
    let body = [];
    request.on(&amp;apos;data&amp;apos;, (chunk) =&amp;gt; {
      body.push(chunk);
    }).on(&amp;apos;end&amp;apos;, () =&amp;gt; {
      body = Buffer.concat(body).toString();
      response.end(body);
    });
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;注意：通过这种方式检查URL，我们正在做一种“路由”的形式，其他形式的路由可以像  &lt;code&gt;switch&lt;/code&gt;语句一样简单，也可以像  &lt;a href="https://www.npmjs.com/package/express" rel="nofollow noreferrer"&gt;express&lt;/a&gt;这样的整个框架一样复杂，如果你正在寻找可以进行路由的东西，请尝试使用  &lt;a href="https://www.npmjs.com/package/router" rel="nofollow noreferrer"&gt;router&lt;/a&gt;。&lt;/blockquote&gt;
 &lt;p&gt;现在让我们来简化一下吧，请记住，  &lt;code&gt;request&lt;/code&gt;对象是  &lt;code&gt;ReadableStream&lt;/code&gt;，  &lt;code&gt;response&lt;/code&gt;对象是  &lt;code&gt;WritableStream&lt;/code&gt;，这意味着我们可以使用  &lt;code&gt;pipe&lt;/code&gt;将数据从一个引导到另一个，这正是我们想要的echo服务器！&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  if (request.method === &amp;apos;POST&amp;apos; &amp;amp;&amp;amp; request.url === &amp;apos;/echo&amp;apos;) {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们还没有完成，正如本指南中多次提到的，错误可以而且确实会发生，我们需要处理它们。&lt;/p&gt;
 &lt;p&gt;为了处理请求流上的错误，我们将错误记录到  &lt;code&gt;stderr&lt;/code&gt;并发送  &lt;code&gt;400&lt;/code&gt;状态码以指示  &lt;code&gt;Bad Request&lt;/code&gt;，但是，在实际应用程序中，我们需要检查错误以确定正确的状态码和消息是什么，与通常的错误一样，你应该查阅错误文档。&lt;/p&gt;
 &lt;p&gt;在响应中，我们只是将错误记录到  &lt;code&gt;stderr&lt;/code&gt;。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

http.createServer((request, response) =&amp;gt; {
  request.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
    console.error(err);
    response.statusCode = 400;
    response.end();
  });
  response.on(&amp;apos;error&amp;apos;, (err) =&amp;gt; {
    console.error(err);
  });
  if (request.method === &amp;apos;POST&amp;apos; &amp;amp;&amp;amp; request.url === &amp;apos;/echo&amp;apos;) {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们现在已经介绍了处理HTTP请求的大部分基础知识，此时，你应该能够：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;使用请求处理程序函数实例化HTTP服务器，并让它侦听端口。&lt;/li&gt;
  &lt;li&gt;从   &lt;code&gt;request&lt;/code&gt;对象中获取headers、URL、方法和body数据。&lt;/li&gt;
  &lt;li&gt;根据   &lt;code&gt;request&lt;/code&gt;对象中的URL和/或其他数据做出路由决策。&lt;/li&gt;
  &lt;li&gt;通过   &lt;code&gt;response&lt;/code&gt;对象发送headers、HTTP状态码和body数据。&lt;/li&gt;
  &lt;li&gt;从   &lt;code&gt;request&lt;/code&gt;对象和   &lt;code&gt;response&lt;/code&gt;对象管道数据。&lt;/li&gt;
  &lt;li&gt;处理   &lt;code&gt;request&lt;/code&gt;和   &lt;code&gt;response&lt;/code&gt;流中的流错误。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;从这些基础知识中，可以构建用于许多典型用例的Node.js HTTP服务器，这些API提供了许多其他功能，因此请务必阅读有关  &lt;code&gt;EventEmitters&lt;/code&gt;、  &lt;code&gt;Streams&lt;/code&gt;和  &lt;code&gt;HTTP&lt;/code&gt;的API文档。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h4&gt;  &lt;a href="https://segmentfault.com/a/1190000017460939"&gt;上一篇：Node.js中的定时器&lt;/a&gt;&lt;/h4&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>node.js javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/59109-node-js-http</guid>
      <pubDate>Fri, 21 Dec 2018 16:02:13 CST</pubDate>
    </item>
    <item>
      <title>你不知道的Node.js性能优化，读了之后水平直线上升</title>
      <link>https://itindex.net/detail/59051-%E7%9F%A5%E9%81%93-node-js</link>
      <description>&lt;blockquote&gt;本文由云+社区发表  &lt;p&gt;“当我第一次知道要这篇文章的时候，其实我是拒绝的，因为我觉得，你不能叫我写马上就写，我要有干货才行，写一些老生常谈的然后加上好多特技，那个 Node.js 性能啊好像 Duang~ 的一下就上去了，那读者一定会骂我，Node.js 根本没有这样搞性能优化的，都是假的。” ------ 斯塔克·成龙·王&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;1、使用最新版本的 Node.js&lt;/h2&gt;
 &lt;p&gt;仅仅是简单的升级 Node.js 版本就可以轻松地获得性能提升，因为几乎任何新版本的 Node.js 都会比老版本性能更好，为什么？&lt;/p&gt;
 &lt;p&gt;Node.js 每个版本的性能提升主要来自于两个方面：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;V8 的版本更新；&lt;/li&gt;
  &lt;li&gt;Node.js 内部代码的更新优化。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;例如最新的 V8 7.1 中，就优化了某些情形下闭包的逃逸分析，让 Array 的一些方法得到了性能提升：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300756" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;Node.js 的内部代码，随着版本的升级，也会有明显的优化，比如下面这个图就是   &lt;code&gt;require&lt;/code&gt; 的性能随着 Node.js 版本升级的变化：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300757?w=1702&amp;h=1090" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;每个提交到 Node.js 的 PR 都会在 review 的时候考虑会不会对当前性能造成衰退。同时也有专门的 benchmarking 团队来监控性能变化，你可以在这里看到 Node.js 的每个版本的性能变化：&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://benchmarking.nodejs.org/" rel="nofollow noreferrer"&gt;https://benchmarking.nodejs.org/&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;所以，你可以完全对新版本 Node.js 的性能放心，如果发现了任何在新版本下的性能衰退，欢迎提交一个 issue。&lt;/p&gt;
 &lt;h3&gt;如何选择 Node.js 的版本？&lt;/h3&gt;
 &lt;p&gt;这里就要科普一下 Node.js 的版本策略：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Node.js 的版本主要分为 Current 和 LTS；&lt;/li&gt;
  &lt;li&gt;Current 就是当前最新的、依然处于开发中的 Node.js 版本；&lt;/li&gt;
  &lt;li&gt;LTS 就是稳定的、会长期维护的版本；&lt;/li&gt;
  &lt;li&gt;Node.js 每六个月（每年的四月和十月）会发布一次大版本升级，大版本会带来一些不兼容的升级；&lt;/li&gt;
  &lt;li&gt;每年四月发布的版本（版本号为偶数，如 v10）是 LTS 版本，即长期支持的版本，社区会从发布当年的十月开始，继续维护 18 + 12 个月（Active LTS + Maintaince LTS）；&lt;/li&gt;
  &lt;li&gt;每年十月发布的版本（版本号为奇数，例如现在的 v11）只有 8 个月的维护期。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;举个例子，现在（2018年11月），Node.js Current 的版本是 v11，LTS 版本是 v10 和 v8。更老的 v6 处于 Maintenace LTS，从明年四月起就不再维护了。去年十月发布的 v9 版本在今年六月结束了维护。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300758?w=1820&amp;h=658" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;对于生产环境而言，Node.js 官方推荐使用最新的 LTS 版本，现在是 v10.13.0。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;2、使用   &lt;a href="https://github.com/fastify/fast-json-stringify" rel="nofollow noreferrer"&gt;fast-json-stringify&lt;/a&gt; 加速 JSON 序列化&lt;/h2&gt;
 &lt;p&gt;在 JavaScript 中，生成 JSON 字符串是非常方便的：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const json = JSON.stringify(obj)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;但很少人会想到这里竟然也存在性能优化的空间，那就是使用   &lt;a href="http://json-schema.org/" rel="nofollow noreferrer"&gt;JSON Schema&lt;/a&gt; 来加速序列化。&lt;/p&gt;
 &lt;p&gt;在 JSON 序列化时，我们需要识别大量的字段类型，比如对于 string 类型，我们就需要在两边加上   &lt;code&gt;&amp;quot;&lt;/code&gt;，对于数组类型，我们需要遍历数组，把每个对象序列化后，用   &lt;code&gt;,&lt;/code&gt; 隔开，然后在两边加上   &lt;code&gt;[&lt;/code&gt; 和   &lt;code&gt;]&lt;/code&gt;，诸如此类等等。&lt;/p&gt;
 &lt;p&gt;但  &lt;strong&gt;如果已经提前通过 Schema 知道每个字段的类型，那么就不需要遍历、识别字段类型&lt;/strong&gt;，而可以直接用序列化对应的字段，这就大大减少了计算开销，这就是   &lt;a href="https://github.com/fastify/fast-json-stringify" rel="nofollow noreferrer"&gt;fast-json-stringfy&lt;/a&gt; 的原理。&lt;/p&gt;
 &lt;p&gt;根据项目中的跑分，在某些情况下甚至可以比   &lt;code&gt;JSON.stringify&lt;/code&gt; 快接近 10 倍！&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300759?w=1000&amp;h=566" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;一个简单的示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const fastJson = require(&amp;apos;fast-json-stringify&amp;apos;)
const stringify = fastJson({
    title: &amp;apos;Example Schema&amp;apos;,
    type: &amp;apos;object&amp;apos;,
    properties: {
        name: { type: &amp;apos;string&amp;apos; },
        age: { type: &amp;apos;integer&amp;apos; },
        books: {
            type: &amp;apos;array&amp;apos;,
            items: {
                type: &amp;apos;string&amp;apos;,
                uniqueItems: true
            }
        }
    }
})

console.log(stringify({
    name: &amp;apos;Starkwang&amp;apos;,
    age: 23,
    books: [&amp;apos;C++ Primier&amp;apos;, &amp;apos;響け！ユーフォニアム～&amp;apos;]
}))
//=&amp;gt; {&amp;quot;name&amp;quot;:&amp;quot;Starkwang&amp;quot;,&amp;quot;age&amp;quot;:23,&amp;quot;books&amp;quot;:[&amp;quot;C++ Primier&amp;quot;,&amp;quot;響け！ユーフォニアム～&amp;quot;]}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在 Node.js 的中间件业务中，通常会有很多数据使用 JSON 进行，并且这些 JSON 的结构是非常相似的（如果你使用了 TypeScript，更是这样），这种场景就非常适合使用 JSON Schema 来优化。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;3、提升 Promise 的性能&lt;/h2&gt;
 &lt;p&gt;Promise 是解决回调嵌套地狱的灵丹妙药，特别是当自从 async/await 全面普及之后，它们的组合无疑成为了 JavaScript 异步编程的终极解决方案，现在大量的项目都已经开始使用这种模式。&lt;/p&gt;
 &lt;p&gt;但是优雅的语法后面也隐藏着性能损耗，我们可以使用   &lt;a href="https://github.com/kyrylkov/promise-async-performance" rel="nofollow noreferrer"&gt;github 上一个已有的跑分项目&lt;/a&gt;进行测试，以下是测试结果：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;file                               time(ms)  memory(MB)
callbacks-baseline.js                   380       70.83
promises-bluebird.js                    554       97.23
promises-bluebird-generator.js          585       97.05
async-bluebird.js                       593      105.43
promises-es2015-util.promisify.js      1203      219.04
promises-es2015-native.js              1257      227.03
async-es2017-native.js                 1312      231.08
async-es2017-util.promisify.js         1550      228.74

Platform info:
Darwin 18.0.0 x64
Node.JS 11.1.0
V8 7.0.276.32-node.7
Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们可以从结果中看到，原生 async/await + Promise 的性能比 callback 要差很多，并且内存占用也高得多。对于大量异步逻辑的中间件项目而言，这里的性能开销还是不能忽视的。&lt;/p&gt;
 &lt;p&gt;通过对比可以发现，性能损耗主要来自于 Promise 对象自身的实现，V8 原生实现的 Promise 比 bluebird 这样第三方实现的 Promise 库要慢很多。而 async/await 语法并不会带来太多的性能损失。&lt;/p&gt;
 &lt;p&gt;所以对于大量异步逻辑、轻量计算的中间件项目而言，可以在代码中把全局的 Promise 换为 bluebird 的实现：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;global.Promise = require(&amp;apos;bluebird&amp;apos;);&lt;/code&gt;&lt;/pre&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;4、正确地编写异步代码&lt;/h2&gt;
 &lt;p&gt;使用 async/await 之后，项目的异步代码会非常好看：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;但因此，有时我们也会忘记使用 Promise 给我们带来的其它能力，比如   &lt;code&gt;Promise.all()&lt;/code&gt; 的并行能力：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// bad
async function getUserInfo(id) {
    const profile = await getUserProfile(id);
    const repo = await getUserRepo(id)
    return { profile, repo }
}

// good
async function getUserInfo(id) {
    const [profile, repo] = await Promise.all([
        getUserProfile(id),
        getUserRepo(id)
    ])
    return { profile, repo }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;还有比如   &lt;a href="http://bluebirdjs.com/docs/api/promise.any.html" rel="nofollow noreferrer"&gt;   &lt;code&gt;Promise.any()&lt;/code&gt;&lt;/a&gt;（此方法不在ES6 Promise标准中，也可以使用标准的   &lt;code&gt;Promise.race()&lt;/code&gt; 代替），我们可以用它轻松实现更加可靠快速的调用：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;async function getServiceIP(name) {
    // 从 DNS 和 ZooKeeper 获取服务 IP，哪个先成功返回用哪个
    // 与 Promise.race 不同的是，这里只有当两个调用都 reject 时，才会抛出错误
    return await Promise.any([
        getIPFromDNS(name),
        getIPFromZooKeeper(name)
    ])
} &lt;/code&gt;&lt;/pre&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;5、优化 V8 GC&lt;/h2&gt;
 &lt;p&gt;关于 V8 的垃圾回收机制，已经有很多类似的文章了，这里就不再重复介绍。推荐两篇文章：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://yq.aliyun.com/articles/592878" rel="nofollow noreferrer"&gt;解读 V8 GC Log（一）: Node.js 应用背景与 GC 基础知识&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://yq.aliyun.com/articles/592880" rel="nofollow noreferrer"&gt;解读 V8 GC Log（二）: 堆内外内存的划分与 GC 算法&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;我们在日常开发代码的时候，比较容易踩到下面几个坑：&lt;/p&gt;
 &lt;h3&gt;坑一：使用大对象作为缓存，导致老生代（Old Space）的垃圾回收变慢&lt;/h3&gt;
 &lt;p&gt;示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const cache = {}
async function getUserInfo(id) {
    if (!cache[id]) {
        cache[id] = await getUserInfoFromDatabase(id)
    }
    return cache[id]
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这里我们使用了一个变量   &lt;code&gt;cache&lt;/code&gt; 作为缓存，加速用户信息的查询，进行了很多次查询后，  &lt;code&gt;cache&lt;/code&gt; 对象会进入老生代，并且会变得无比庞大，而老生代是使用三色标记 + DFS 的方式进行 GC 的，一个大对象会直接导致 GC 花费的时间增长（而且也有内存泄漏的风险）。&lt;/p&gt;
 &lt;p&gt;解决方法就是：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;使用 Redis 这样的外部缓存，实际上像 Redis 这样的内存型数据库非常适合这种场景；&lt;/li&gt;
  &lt;li&gt;限制本地缓存对象的大小，比如使用 FIFO、TTL 之类的机制来清理对象中的缓存。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;坑二：新生代空间不足，导致频繁 GC&lt;/h3&gt;
 &lt;p&gt;这个坑会比较隐蔽。&lt;/p&gt;
 &lt;p&gt;Node.js 默认给新生代分配的内存是 64MB（64位的机器，后同），但因为新生代 GC 使用的是 Scavenge 算法，所以实际能使用的内存只有一半，即 32MB。&lt;/p&gt;
 &lt;p&gt;当业务代码频繁地产生大量的小对象时，这个空间很容易就会被占满，从而触发 GC。虽然新生代的 GC 比老生代要快得多，但频繁的 GC 依然会很大地影响性能。极端的情况下，GC 甚至可以占用全部计算时间的 30% 左右。&lt;/p&gt;
 &lt;p&gt;解决方法就是，在启动 Node.js 时，修改新生代的内存上限，减少 GC 的次数：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;node --max-semi-space-size=128 app.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;当然有人肯定会问，新生代的内存是不是越大越好呢？&lt;/p&gt;
 &lt;p&gt;随着内存的增大，GC 的次数减少，但每次 GC 所需要的时间也会增加，所以并不是越大越好，具体数值需要对业务进行压测 profile 才能确定分配多少新生代内存最好。&lt;/p&gt;
 &lt;p&gt;但一般根据经验而言，  &lt;strong&gt;分配 64MB 或者 128MB 是比较合理的&lt;/strong&gt;。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;6、正确地使用 Stream&lt;/h2&gt;
 &lt;p&gt;Stream 是 Node.js 最基本的概念之一，Node.js 内部的大部分与 IO 相关的模块，比如 http、net、fs、repl，都是建立在各种 Stream 之上的。&lt;/p&gt;
 &lt;p&gt;下面这个经典的例子应该大部分人都知道，对于大文件，我们不需要把它完全读入内存，而是使用 Stream 流式地把它发送出去：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);
const fs = require(&amp;apos;fs&amp;apos;);

// bad
http.createServer(function (req, res) {
    fs.readFile(__dirname + &amp;apos;/data.txt&amp;apos;, function (err, data) {
        res.end(data);
    });
});

// good
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + &amp;apos;/data.txt&amp;apos;);
    stream.pipe(res);
});&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在业务代码中合理地使用 Stream 能很大程度地提升性能，当然是但实际的业务中我们很可能会忽略这一点，比如采用 React 服务器端渲染的项目，我们就可以用   &lt;a href="https://reactjs.org/docs/react-dom-server.html#rendertonodestream" rel="nofollow noreferrer"&gt;   &lt;code&gt;renderToNodeStream&lt;/code&gt;&lt;/a&gt;：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const ReactDOMServer require(&amp;apos;react-dom/server&amp;apos;)
const http = require(&amp;apos;http&amp;apos;)
const fs = require(&amp;apos;fs&amp;apos;)
const app = require(&amp;apos;./app&amp;apos;)

// bad
const server = http.createServer((req, res) =&amp;gt; {
    const body = ReactDOMServer.renderToString(app)
    res.end(body)
});

// good
const server = http.createServer(function (req, res) {
    const stream = ReactDOMServer.renderToNodeStream(app)
    stream.pipe(res)
})

server.listen(8000)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;使用 pipeline 管理 stream&lt;/h3&gt;
 &lt;p&gt;在过去的 Node.js 中，处理 stream 是非常麻烦的，举个例子：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;source.pipe(a).pipe(b).pipe(c).pipe(dest)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;一旦其中 source、a、b、c、dest 中，有任何一个 stream 出错或者关闭，会导致整个管道停止，此时我们需要手工销毁所有的 stream，在代码层面这是非常麻烦的。&lt;/p&gt;
 &lt;p&gt;所以社区出现了   &lt;a href="https://github.com/mafintosh/pump" rel="nofollow noreferrer"&gt;pump&lt;/a&gt; 这样的库来自动控制 stream 的销毁。而 Node.js v10.0 加入了一个新的特性：  &lt;a href="https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_stream_pipeline_streams_callback" rel="nofollow noreferrer"&gt;stream.pipeline&lt;/a&gt;，可以替代 pump 帮助我们更好的管理 stream。&lt;/p&gt;
 &lt;p&gt;一个官方的例子：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const { pipeline } = require(&amp;apos;stream&amp;apos;);
const fs = require(&amp;apos;fs&amp;apos;);
const zlib = require(&amp;apos;zlib&amp;apos;);

pipeline(
    fs.createReadStream(&amp;apos;archive.tar&amp;apos;),
    zlib.createGzip(),
    fs.createWriteStream(&amp;apos;archive.tar.gz&amp;apos;),
    (err) =&amp;gt; {
        if (err) {
            console.error(&amp;apos;Pipeline failed&amp;apos;, err);
        } else {
            console.log(&amp;apos;Pipeline succeeded&amp;apos;);
        }
    }
);&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;实现自己的高性能 Stream&lt;/h3&gt;
 &lt;p&gt;在业务中你可能也会自己实现一个 Stream，可读、可写、或者双向流，可以参考文档：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://nodejs.org/docs/latest/api/stream.html#stream_implementing_a_readable_stream" rel="nofollow noreferrer"&gt;implementing Readable streams&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://nodejs.org/docs/latest/api/stream.html#stream_implementing_a_writable_stream" rel="nofollow noreferrer"&gt;implementing Writable streams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;Stream 虽然很神奇，但自己实现 Stream 也可能会存在隐藏的性能问题，比如：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            this.push(chunk);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;当我们调用   &lt;code&gt;new MyReadable().pipe(xxx)&lt;/code&gt; 时，会把   &lt;code&gt;getNextChunk()&lt;/code&gt; 所得到的 chunk 都 push 出去，直到读取结束。但如果此时管道的下一步处理速度较慢，就会导致数据堆积在内存中，导致内存占用变大，GC 速度降低。&lt;/p&gt;
 &lt;p&gt;而正确的做法应该是，根据   &lt;code&gt;this.push()&lt;/code&gt; 返回值选择正确的行为，当返回值为   &lt;code&gt;false&lt;/code&gt; 时，说明此时堆积的 chunk 已经满了，应该停止读入。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            if (!this.push(chunk)) {
                return false  
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这个问题在 Node.js 官方的一篇文章中有详细的介绍：  &lt;a href="https://nodejs.org/en/docs/guides/backpressuring-in-streams/" rel="nofollow noreferrer"&gt;Backpressuring in Streams&lt;/a&gt;&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;7、C++ 扩展一定比 JavaScript 快吗？&lt;/h2&gt;
 &lt;p&gt;Node.js 非常适合 IO 密集型的应用，而对于计算密集的业务，很多人都会想到用编写 C++ Addon 的方式来优化性能。但实际上 C++ 扩展并不是灵丹妙药，V8 的性能也没有想象的那么差。&lt;/p&gt;
 &lt;p&gt;比如，我在今年九月份的时候把 Node.js 的   &lt;code&gt;net.isIPv6()&lt;/code&gt; 从 C++ 迁移到了 JS 的实现，让大多数的测试用例都获得了 10%- 250% 不等的性能提升（  &lt;a href="https://github.com/nodejs/node/pull/22673" rel="nofollow noreferrer"&gt;具体PR可以看这里&lt;/a&gt;）。&lt;/p&gt;
 &lt;p&gt;JavaScript 在 V8 上跑得比 C++ 扩展还快，这种情况多半发生在与字符串、正则表达式相关的场景，因为 V8 内部使用的正则表达式引擎是   &lt;a href="https://github.com/ashinn/irregex" rel="nofollow noreferrer"&gt;irregexp&lt;/a&gt;，这个正则表达式引擎比    &lt;a href="https://www.boost.org/" rel="nofollow noreferrer"&gt;boost&lt;/a&gt; 中自带的引擎（  &lt;code&gt;boost::regex&lt;/code&gt;）要快得多。&lt;/p&gt;
 &lt;p&gt;还有一处值得注意的就是，Node.js 的 C++  扩展在进行类型转换的时候，可能会消耗非常多的性能，如果不注意 C++ 代码的细节，性能会很大地下降。&lt;/p&gt;
 &lt;p&gt;这里有一篇文章对比了相同算法下 C++ 和 JS 的性能（需翻墙）：  &lt;a href="https://medium.com/developers-writing/how-to-get-a-performance-boost-using-node-js-native-addons-fd3a24719c85" rel="nofollow noreferrer"&gt;How to get a performance boost using Node.js native addons&lt;/a&gt;。其中值得注意的结论就是，C++ 代码在对参数中的字符串进行转换后（  &lt;code&gt;String::Utf8Value&lt;/code&gt;转为  &lt;code&gt;std::string&lt;/code&gt;），性能甚至不如 JS 实现的一半。只有在使用 NAN 提供的类型封装后，才获得了比 JS 更高的性能。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300760?w=1904&amp;h=970" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;换句话说，C++ 是否比 JavaScript 更加高效需要具体问题具体分析，某些情况下，C++ 扩展不一定就会比原生 JavaScript 更高效。如果你对自己的 C++ 水平不是那么有信心，其实还是建议用 JavaScript 来实现，因为 V8 的性能比你想象的要好得多。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;8、使用 node-clinic 快速定位性能问题&lt;/h2&gt;
 &lt;p&gt;说了这么多，有没有什么可以开箱即用，五分钟见效的呢？当然有。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://github.com/nearform/node-clinic" rel="nofollow noreferrer"&gt;node-clinic&lt;/a&gt; 是   &lt;a href="https://www.nearform.com/" rel="nofollow noreferrer"&gt;NearForm&lt;/a&gt; 开源的一款 Node.js 性能诊断工具，可以非常快速地定位性能问题。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm i -g clinic
npm i -g autocannon&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;使用的时候，先开启服务进程：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic doctor -- node server.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后我们可以用任何压测工具跑一次压测，比如使用同一个作者的   &lt;a href="https://github.com/mcollina/autocannon" rel="nofollow noreferrer"&gt;autocannon&lt;/a&gt;（当然你也可以使用 ab、curl 这样的工具来进行压测。）：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;autocannon http://localhost:3000&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;压测完毕后，我们 ctrl + c 关闭 clinic 开启的进程，就会自动生成报告。比如下面就是我们一个中间件服务的性能报告：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300761?w=2780&amp;h=1188" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们可以从 CPU 的使用曲线看出，这个中间件服务的性能瓶颈不在自身内部的计算，而在于 I/O 速度太慢。clinic 也在上面告诉我们检测到了潜在的 I/O 问题。&lt;/p&gt;
 &lt;p&gt;下面我们使用   &lt;code&gt;clinic bubbleprof&lt;/code&gt; 来检测 I/O 问题：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic bubbleprof -- node server.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;再次进行压测后，我们得到了新的报告：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300762" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这个报告中，我们可以看到，  &lt;code&gt;http.Server&lt;/code&gt; 在整个程序运行期间，96% 的时间都处于 pending 状态，点开后，我们会发现调用栈中存在大量的 empty frame，也就是说，由于网络 I/O 的限制，CPU 存在大量的空转，这在中间件业务中非常常见，也为我们指明了优化方向不在服务内部，而在服务器的网关和依赖的服务相应速度上。&lt;/p&gt;
 &lt;p&gt;想知道如何读懂   &lt;code&gt;clinic bubbleprof&lt;/code&gt; 生成的报告，可以看这里：  &lt;a href="https://clinicjs.org/bubbleprof/walkthrough/" rel="nofollow noreferrer"&gt;https://clinicjs.org/bubblepr...&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;同样，clinic 也可以检测到服务内部的计算性能问题&lt;/strong&gt;，下面我们做一些“破坏”，让这个服务的性能瓶颈出现在 CPU 计算上。&lt;/p&gt;
 &lt;p&gt;我们在某个中间件中加入了空转一亿次这样非常消耗 CPU 的“破坏性”代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;function sleep() {
    let n = 0
    while (n++ &amp;lt; 10e7) {
        empty()
    }
}
function empty() { }

module.exports = (ctx, next) =&amp;gt; {
    sleep()
    // ......
    return next()
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后使用   &lt;code&gt;clinic doctor&lt;/code&gt;，重复上面的步骤，生成性能报告：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300763" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这就是一个非常典型的  &lt;strong&gt;同步计算阻塞了异步队列&lt;/strong&gt;的“病例”，即主线程上进行了大量的计算，导致 JavaScript 的异步回调没法及时触发，Event Loop 的延迟极高。&lt;/p&gt;
 &lt;p&gt;对于这样的应用，我们可以继续使用   &lt;code&gt;clinic flame&lt;/code&gt; 来确定到底是哪里出现了密集计算：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;clinic flame -- node app.js&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;压测后，我们得到了火焰图（这里把空转次数减少到了100万次，让火焰图看起来不至于那么极端）：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://segmentfault.com/img/remote/1460000017300764" title="img"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从这张图里，我们可以明显看到顶部的那个大白条，它代表了   &lt;code&gt;sleep&lt;/code&gt; 函数空转所消耗的 CPU 时间。根据这样的火焰图，我们可以非常轻松地看出 CPU 资源的消耗情况，从而定位代码中哪里有密集的计算，找到性能瓶颈。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;此文已由作者授权腾讯云+社区发布&lt;/strong&gt;&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端 程序员 javascript node.js</category>
      <guid isPermaLink="true">https://itindex.net/detail/59051-%E7%9F%A5%E9%81%93-node-js</guid>
      <pubDate>Fri, 07 Dec 2018 17:50:18 CST</pubDate>
    </item>
    <item>
      <title>从 0 到 1 再到 100, 搭建、编写、构建一个前端项目</title>
      <link>https://itindex.net/detail/59009-%E5%BB%BA%E4%B8%80-%E5%89%8D%E7%AB%AF-%E9%A1%B9%E7%9B%AE</link>
      <description>&lt;h1&gt;从 0 到 1 再到 100, 搭建、编写、构建一个前端项目&lt;/h1&gt;
 &lt;h2&gt;1. 选择现成的项目模板还是自己搭建项目骨架&lt;/h2&gt;
 &lt;p&gt;搭建一个前端项目的方式有两种：选择现成的项目模板、自己搭建项目骨架。&lt;/p&gt;
 &lt;p&gt;选择一个现成项目模板是搭建一个项目最快的方式，模板已经把基本的骨架都搭建好了，你只需要向里面填充具体的业务代码，就可以通过内置的工具与命令构建代码、部署到服务器等。&lt;/p&gt;
 &lt;p&gt;一般来说，一个现成的项目模板会预定义一定的目录结构、书写方式，在编写项目代码时需要遵循相应的规范；也会内置必要的工具，比如   &lt;a href="http://editorconfig.org" rel="nofollow noreferrer"&gt;.editorconfig&lt;/a&gt;、  &lt;a href="https://github.com/eslint/eslint" rel="nofollow noreferrer"&gt;eslint&lt;/a&gt;、  &lt;a href="https://github.com/stylelint/stylelint" rel="nofollow noreferrer"&gt;stylelint&lt;/a&gt;、  &lt;a href="https://github.com/prettier/prettier" rel="nofollow noreferrer"&gt;prettier&lt;/a&gt;、  &lt;a href="https://github.com/typicode/husky" rel="nofollow noreferrer"&gt;husky&lt;/a&gt;、  &lt;a href="https://github.com/okonet/lint-staged" rel="nofollow noreferrer"&gt;lint-staged&lt;/a&gt; 等；也会内置必要的命令（  &lt;code&gt;package.json | scripts&lt;/code&gt;），比如   &lt;code&gt;本地开发：npm run dev&lt;/code&gt;、  &lt;code&gt;本地预览：npm run start&lt;/code&gt;、  &lt;code&gt;构建：npm run build&lt;/code&gt;、  &lt;code&gt;部署：npm run deploy&lt;/code&gt; 等。&lt;/p&gt;
 &lt;p&gt;社区比较好的项目模板：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://github.com/react-boilerplate/react-boilerplate" rel="nofollow noreferrer"&gt;react-boilerplate&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/ant-design/ant-design-pro" rel="nofollow noreferrer"&gt;ant-design-pro&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/PanJiaChen/vue-element-admin" rel="nofollow noreferrer"&gt;vue-element-admin&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/kriasoft/react-starter-kit" rel="nofollow noreferrer"&gt;react-starter-kit&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/facebook/create-react-app" rel="nofollow noreferrer"&gt;create-react-app&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/senntyou/lila/tree/master/packages/create-lila-app" rel="nofollow noreferrer"&gt;create-lila-app&lt;/a&gt;（我自己用的，哈哈）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这些模板的使用又分为两种：使用   &lt;code&gt;git&lt;/code&gt; 直接克隆到本地、使用命令行创建。&lt;/p&gt;
 &lt;p&gt;（使用现有模板构建的项目，可以跳过第 2 ～ 7 步）&lt;/p&gt;
 &lt;h3&gt;1.1 使用   &lt;code&gt;git&lt;/code&gt; 直接克隆到本地&lt;/h3&gt;
 &lt;p&gt;这是一种真正意义上的模板，可以直接到模板项目的   &lt;code&gt;github&lt;/code&gt; 主页，就能看到整个骨架，比如   &lt;a href="https://github.com/react-boilerplate/react-boilerplate" rel="nofollow noreferrer"&gt;react-boilerplate&lt;/a&gt;、  &lt;a href="https://github.com/ant-design/ant-design-pro" rel="nofollow noreferrer"&gt;ant-design-pro&lt;/a&gt;、  &lt;a href="https://github.com/PanJiaChen/vue-element-admin" rel="nofollow noreferrer"&gt;vue-element-admin&lt;/a&gt;、  &lt;a href="https://github.com/kriasoft/react-starter-kit" rel="nofollow noreferrer"&gt;react-starter-kit&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;以   &lt;code&gt;react-boilerplate&lt;/code&gt; 为例：&lt;/p&gt;
 &lt;p&gt;克隆到本地：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git &amp;lt;你的项目名字&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;切换到目录下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;cd &amp;lt;你的项目名字&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;一般来说，接下来运行   &lt;code&gt;npm run install&lt;/code&gt; 安装项目的依赖后，就可以运行；有些模板可能有内置的初始化命令，比如   &lt;code&gt;react-boilerplate&lt;/code&gt;：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm run setup&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;启动应用：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm start&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这时，就可以在浏览器中预览应用了。&lt;/p&gt;
 &lt;h3&gt;1.2 使用命令行创建&lt;/h3&gt;
 &lt;p&gt;这种方式需要安装相应的命令，然后由命令来创建项目。&lt;/p&gt;
 &lt;p&gt;以   &lt;code&gt;create-react-app&lt;/code&gt; 为例：&lt;/p&gt;
 &lt;p&gt;安装命令：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm install -g create-react-app&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;创建项目：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;create-react-app my-app&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;运行应用：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;cd my-app
npm start&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;1.3 自己搭建项目骨架&lt;/h3&gt;
 &lt;p&gt;如果你需要定制化，可以选择自己搭建项目的骨架，但这需要开发者对构建工具如   &lt;code&gt;webpack&lt;/code&gt;、  &lt;code&gt;npm&lt;/code&gt;、  &lt;code&gt;node&lt;/code&gt; 及其生态等有相当的了解与应用，才能完美的把控整个项目。&lt;/p&gt;
 &lt;p&gt;下面将会一步一步的说明如何搭建一个定制化的项目骨架。&lt;/p&gt;
 &lt;h2&gt;2. 选择合适的规范来写代码&lt;/h2&gt;
 &lt;p&gt;  &lt;code&gt;js&lt;/code&gt; 模块化的发展大致有这样一个过程   &lt;code&gt;iife =&amp;gt; commonjs/amd =&amp;gt; es6&lt;/code&gt;，而在这几个规范中：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;iife&lt;/code&gt;:    &lt;code&gt;js&lt;/code&gt; 原生支持，但一般不会直接使用这种规范写代码&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;amd&lt;/code&gt;:    &lt;a href="https://github.com/requirejs/requirejs" rel="nofollow noreferrer"&gt;requirejs&lt;/a&gt; 定义的加载规范，但随着构建工具的出现，便一般不会用这种规范写代码&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;commonjs&lt;/code&gt;:    &lt;code&gt;node&lt;/code&gt; 的模块加载规范，一般会用这种规范写    &lt;code&gt;node&lt;/code&gt; 程序&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;es6&lt;/code&gt;:    &lt;code&gt;ECMAScript2015&lt;/code&gt; 定义的模块加载规范，需要转码后浏览器才能运行&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这里推荐使用   &lt;code&gt;es6&lt;/code&gt; 的模块化规范来写代码，然后用工具转换成   &lt;code&gt;es5&lt;/code&gt; 的代码，并且   &lt;code&gt;es6&lt;/code&gt; 的代码可以使用   &lt;a href="https://en.wikipedia.org/wiki/Tree_shaking" rel="nofollow noreferrer"&gt;Tree shaking&lt;/a&gt; 功能。&lt;/p&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://en.wikipedia.org/wiki/Immediately-invoked_function_expression" rel="nofollow noreferrer"&gt;IIFE(Immediately-invoked function expression)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://en.wikipedia.org/wiki/Tree_shaking" rel="nofollow noreferrer"&gt;Tree shaking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://webpack.js.org/guides/tree-shaking/" rel="nofollow noreferrer"&gt;webpack - tree-shaking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://blog.csdn.net/haodawang/article/details/77199980" rel="nofollow noreferrer"&gt;webpack 如何优雅的使用tree-shaking（摇树优化）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;3. 选择合适的构建工具&lt;/h2&gt;
 &lt;p&gt;对于前端项目来说，构建工具一般都选用   &lt;a href="https://github.com/webpack/webpack" rel="nofollow noreferrer"&gt;webpack&lt;/a&gt;，  &lt;code&gt;webpack&lt;/code&gt; 提供了强大的功能和配置化运行。如果你不喜欢复杂的配置，可以尝试   &lt;a href="https://github.com/parcel-bundler/parcel" rel="nofollow noreferrer"&gt;parcel&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016409719"&gt;webpack 之外的另一种选择：parcel&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;4. 确定是单页面应用（SPA）还是多页面应用&lt;/h2&gt;
 &lt;p&gt;因为单页面应用与多页面应用在构建的方式上有很大的不同，所以需要从项目一开始就确定，使用哪种模式来构建项目。&lt;/p&gt;
 &lt;h3&gt;4.1 多页面应用&lt;/h3&gt;
 &lt;p&gt;传统多页面是由后端控制一个   &lt;code&gt;url&lt;/code&gt; 对应一个   &lt;code&gt;html&lt;/code&gt; 文件，页面之间的跳转需要根据后端给出的   &lt;code&gt;url&lt;/code&gt; 跳转到新的   &lt;code&gt;html&lt;/code&gt; 上。比如：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;http://www.example.com/page1 -&amp;gt; path/to/page1.html
http://www.example.com/page2 -&amp;gt; path/to/page2.html
http://www.example.com/page3 -&amp;gt; path/to/page3.html&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这种方式的应用，项目里会有多个入口文件，搭建项目的时候就需要对这种多入口模式进行封装。另外，也可以选择一些封装的多入口构建工具，如   &lt;a href="https://github.com/senntyou/lila" rel="nofollow noreferrer"&gt;lila&lt;/a&gt;。&lt;/p&gt;
 &lt;h3&gt;4.2 单页面应用&lt;/h3&gt;
 &lt;p&gt;  &lt;a href="https://en.wikipedia.org/wiki/Single-page_application" rel="nofollow noreferrer"&gt;单页面应用（single page application）&lt;/a&gt;，就是只有一个页面的应用，页面的刷新和内部子页面的跳转完全由   &lt;code&gt;js&lt;/code&gt; 来控制。&lt;/p&gt;
 &lt;p&gt;一般单页面应用都有以下几个特点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;本地路由，由    &lt;code&gt;js&lt;/code&gt; 定义路由、根据路由渲染页面、控制页面的跳转&lt;/li&gt;
  &lt;li&gt;所有文件只会加载一次，最大限度重用文件，并且极大提升加载速度&lt;/li&gt;
  &lt;li&gt;按需加载，只有真正使用到页面的时候，才加载相应的文件&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这种方式的应用，项目里只有一个入口文件，便无需封装。&lt;/p&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015297908"&gt;单页面应用（SPA）、按需加载&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;5. 选择合适的前端框架与 UI 库&lt;/h2&gt;
 &lt;p&gt;一般在搭建项目的时候就需要定下前端框架与 UI 库，因为如果后期想更换前端框架和 UI 库，代价是很大的。&lt;/p&gt;
 &lt;p&gt;比较现代化的前端框架：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://github.com/facebook/react" rel="nofollow noreferrer"&gt;react&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/vuejs/vue" rel="nofollow noreferrer"&gt;vue&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/angular/angular" rel="nofollow noreferrer"&gt;angular&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;一些不错的组合：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/jquery/jquery" rel="nofollow noreferrer"&gt;jquery&lt;/a&gt; +    &lt;a href="https://github.com/twbs/bootstrap" rel="nofollow noreferrer"&gt;bootstrap&lt;/a&gt;：比较经典的&lt;/li&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/facebook/react" rel="nofollow noreferrer"&gt;react&lt;/a&gt; +    &lt;a href="https://github.com/ant-design/ant-design" rel="nofollow noreferrer"&gt;ant-design&lt;/a&gt;、   &lt;a href="https://github.com/mui-org/material-ui" rel="nofollow noreferrer"&gt;material-ui&lt;/a&gt;、   &lt;a href="https://github.com/Semantic-Org/Semantic-UI" rel="nofollow noreferrer"&gt;Semantic-UI&lt;/a&gt;：   &lt;code&gt;react&lt;/code&gt; 的组合&lt;/li&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/vuejs/vue" rel="nofollow noreferrer"&gt;vue&lt;/a&gt; +    &lt;a href="https://github.com/ElemeFE/element" rel="nofollow noreferrer"&gt;element&lt;/a&gt;、   &lt;a href="https://github.com/iview/iview" rel="nofollow noreferrer"&gt;iview&lt;/a&gt;、   &lt;a href="https://github.com/airyland/vux" rel="nofollow noreferrer"&gt;vux&lt;/a&gt;、   &lt;a href="https://github.com/ElemeFE/mint-ui" rel="nofollow noreferrer"&gt;mint-ui&lt;/a&gt;：   &lt;code&gt;vue&lt;/code&gt; 的组合&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016334838"&gt;前端最受欢迎的 UI 框架&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;6. 定好目录结构&lt;/h2&gt;
 &lt;p&gt;一个好的目录结构对一个好的项目而言是非常必要的。&lt;/p&gt;
 &lt;p&gt;一个好的目录结构应当具有以下的一些特点：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;解耦：代码尽量去耦合，这样代码逻辑清晰，也容易扩展&lt;/li&gt;
  &lt;li&gt;分块：按照功能对代码进行分块、分组，并能快捷的添加分块、分组&lt;/li&gt;
  &lt;li&gt;编辑器友好：需要更新功能时，可以很快的定位到相关文件，并且这些文件应该是很靠近的，而不至于到处找文件&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;比较推荐的目录结构：&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;多页面应用&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间（与这个页面相关的文件都放在这个目录下）
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录（如果基于 react, vue 等组件化框架）
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间（内部结构参考 page1）
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;单页面应用&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录（如果基于 react, vue 等组件化框架）
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间（内部结构参考 page1）
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ... &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015816534"&gt;目录结构优化&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;7. 搭建一个好的脚手架&lt;/h2&gt;
 &lt;p&gt;搭建一个好的脚手架，能够更好的编写代码、构建项目等。&lt;/p&gt;
 &lt;p&gt;可以查看   &lt;a href="https://segmentfault.com/a/1190000016481132"&gt;搭建自己的前端脚手架&lt;/a&gt; 了解一些基本的脚手架文件与工具。&lt;/p&gt;
 &lt;p&gt;比如：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;.editorconfig&lt;/code&gt;: 用这个文件来统一不同编辑器的一些配置，比如    &lt;code&gt;tab&lt;/code&gt; 转 2 个空格、自动插入空尾行、去掉行尾的空格等，   &lt;a href="http://editorconfig.org" rel="nofollow noreferrer"&gt;http://editorconfig.org&lt;/a&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/eslint/eslint" rel="nofollow noreferrer"&gt;eslint&lt;/a&gt;、   &lt;a href="https://github.com/stylelint/stylelint" rel="nofollow noreferrer"&gt;stylelint&lt;/a&gt;、   &lt;a href="https://github.com/prettier/prettier" rel="nofollow noreferrer"&gt;prettier&lt;/a&gt;: 规范化代码风格、优化代码格式等&lt;/li&gt;
  &lt;li&gt;
   &lt;a href="https://github.com/typicode/husky" rel="nofollow noreferrer"&gt;husky&lt;/a&gt;、   &lt;a href="https://github.com/okonet/lint-staged" rel="nofollow noreferrer"&gt;lint-staged&lt;/a&gt;: 在    &lt;code&gt;git&lt;/code&gt; 提交之前对代码进行审查，否则不予提交&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:    &lt;a href="https://docs.gitlab.com/ee/ci/" rel="nofollow noreferrer"&gt;gitlab ci&lt;/a&gt; 持续集成服务&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016481132"&gt;搭建自己的前端脚手架&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015858432"&gt;怎样提升代码质量&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015403363"&gt;CSS 模块化&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016422897"&gt;css 的弱化与 js 的强化&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015297352"&gt;本地化接口模拟、前后端并行开发&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;=================================================&lt;/p&gt;
 &lt;p&gt;到这里为止，一个基本的项目骨架就算搭建好了。&lt;/p&gt;
 &lt;h2&gt;8. 使用版本控制系统管理源代码（git）&lt;/h2&gt;
 &lt;p&gt;项目搭建好后，需要一个版本控制系统来管理源代码。&lt;/p&gt;
 &lt;p&gt;比较常用的版本管理工具有   &lt;a href="https://git-scm.com/" rel="nofollow noreferrer"&gt;git&lt;/a&gt;、  &lt;a href="http://subversion.apache.org/" rel="nofollow noreferrer"&gt;svn&lt;/a&gt;，现在一般都用   &lt;code&gt;git&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;一般开源的项目可以托管到   &lt;a href="http://github.com" rel="nofollow noreferrer"&gt;http://github.com&lt;/a&gt;，私人的项目可以托管到   &lt;a href="https://gitee.com" rel="nofollow noreferrer"&gt;https://gitee.com&lt;/a&gt;、  &lt;a href="https://coding.net/" rel="nofollow noreferrer"&gt;https://coding.net/&lt;/a&gt;，而企业的项目则需要自建版本控制系统了。&lt;/p&gt;
 &lt;p&gt;自建版本控制系统主要有   &lt;a href="https://gitlab.com/" rel="nofollow noreferrer"&gt;gitlab&lt;/a&gt;、  &lt;a href="https://github.com/gogs/gogs" rel="nofollow noreferrer"&gt;gogs&lt;/a&gt;、  &lt;a href="https://github.com/go-gitea/gitea" rel="nofollow noreferrer"&gt;gitea&lt;/a&gt;：  &lt;code&gt;gitlab&lt;/code&gt; 是由商业驱动的，比较稳定，社区版是免费的，一般建议选用这个；  &lt;code&gt;gogs, gitea&lt;/code&gt; 是开源的项目，还不太稳定，期待进一步的更新。&lt;/p&gt;
 &lt;p&gt;所以，  &lt;code&gt;git&lt;/code&gt; +   &lt;code&gt;gitlab&lt;/code&gt; 是不错的配合。&lt;/p&gt;
 &lt;h2&gt;9. 编写代码&lt;/h2&gt;
 &lt;p&gt;编写代码时，  &lt;code&gt;js&lt;/code&gt; 选用   &lt;code&gt;es6&lt;/code&gt; 的模块化规范来写（如果喜欢用   &lt;a href="https://github.com/Microsoft/TypeScript" rel="nofollow noreferrer"&gt;TypeScript&lt;/a&gt;，需要加上   &lt;a href="https://github.com/TypeStrong/ts-loader" rel="nofollow noreferrer"&gt;ts-loader&lt;/a&gt;），样式可以用   &lt;a href="http://lesscss.org/" rel="nofollow noreferrer"&gt;less&lt;/a&gt;、  &lt;a href="http://sass-lang.com/" rel="nofollow noreferrer"&gt;scss&lt;/a&gt;、  &lt;code&gt;css&lt;/code&gt; 来写。&lt;/p&gt;
 &lt;p&gt;写   &lt;code&gt;js&lt;/code&gt; 模块文件时，注释可以使用   &lt;a href="http://usejsdoc.org/" rel="nofollow noreferrer"&gt;jsdoc&lt;/a&gt; 的规范来写，如果配置相应的工具，可以将这些注释导出接口文档。&lt;/p&gt;
 &lt;p&gt;因为脚手架里有   &lt;a href="https://github.com/typicode/husky" rel="nofollow noreferrer"&gt;husky&lt;/a&gt;、  &lt;a href="https://github.com/okonet/lint-staged" rel="nofollow noreferrer"&gt;lint-staged&lt;/a&gt; 的配合，所以每次提交的代码都会进行代码审查与格式优化，如果不符合规范，则需要把不规范的代码进行修改，然后才能提交到代码仓库中。&lt;/p&gt;
 &lt;p&gt;比如   &lt;code&gt;console.log(haha.hehe);&lt;/code&gt; 这段代码就会遇到错误，不予提交：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://segmentfault.com/img/bVbj9Q6?w=907&amp;h=555" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这个功能定义在   &lt;code&gt;package.json&lt;/code&gt; 中：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;{
  &amp;quot;devDependencies&amp;quot;: {             工具依赖
    &amp;quot;babel-eslint&amp;quot;: &amp;quot;^8.2.6&amp;quot;,
    &amp;quot;eslint&amp;quot;: &amp;quot;^4.19.1&amp;quot;,
    &amp;quot;husky&amp;quot;: &amp;quot;^0.14.3&amp;quot;,
    &amp;quot;lint-staged&amp;quot;: &amp;quot;^7.2.0&amp;quot;,
    &amp;quot;prettier&amp;quot;: &amp;quot;^1.14.0&amp;quot;,
    &amp;quot;stylelint&amp;quot;: &amp;quot;^9.3.0&amp;quot;,
    &amp;quot;eslint-config-airbnb&amp;quot;: &amp;quot;^17.0.0&amp;quot;,
    &amp;quot;eslint-config-prettier&amp;quot;: &amp;quot;^2.9.0&amp;quot;,
    &amp;quot;eslint-plugin-babel&amp;quot;: &amp;quot;^5.1.0&amp;quot;,
    &amp;quot;eslint-plugin-import&amp;quot;: &amp;quot;^2.13.0&amp;quot;,
    &amp;quot;eslint-plugin-jsx-a11y&amp;quot;: &amp;quot;^6.1.0&amp;quot;,
    &amp;quot;eslint-plugin-prettier&amp;quot;: &amp;quot;^2.6.2&amp;quot;,
    &amp;quot;eslint-plugin-react&amp;quot;: &amp;quot;^7.10.0&amp;quot;,
    &amp;quot;stylelint-config-prettier&amp;quot;: &amp;quot;^3.3.0&amp;quot;,
    &amp;quot;stylelint-config-standard&amp;quot;: &amp;quot;^18.2.0&amp;quot;
  },
  &amp;quot;scripts&amp;quot;: {                     可以添加更多命令
    &amp;quot;precommit&amp;quot;: &amp;quot;npm run lint-staged&amp;quot;,
    &amp;quot;prettier&amp;quot;: &amp;quot;prettier --write \&amp;quot;./**/*.{js,jsx,css,less,sass,scss,md,json}\&amp;quot;&amp;quot;,
    &amp;quot;eslint&amp;quot;: &amp;quot;eslint .&amp;quot;,
    &amp;quot;eslint:fix&amp;quot;: &amp;quot;eslint . --fix&amp;quot;,
    &amp;quot;stylelint&amp;quot;: &amp;quot;stylelint \&amp;quot;./**/*.{css,less,sass,scss}\&amp;quot;&amp;quot;,
    &amp;quot;stylelint:fix&amp;quot;: &amp;quot;stylelint \&amp;quot;./**/*.{css,less,sass,scss}\&amp;quot; --fix&amp;quot;,
    &amp;quot;lint-staged&amp;quot;: &amp;quot;lint-staged&amp;quot;
  },
  &amp;quot;lint-staged&amp;quot;: {                 对提交的代码进行检查与矫正
    &amp;quot;**/*.{js,jsx}&amp;quot;: [
      &amp;quot;eslint --fix&amp;quot;,
      &amp;quot;prettier --write&amp;quot;,
      &amp;quot;git add&amp;quot;
    ],
    &amp;quot;**/*.{css,less,sass,scss}&amp;quot;: [
      &amp;quot;stylelint --fix&amp;quot;,
      &amp;quot;prettier --write&amp;quot;,
      &amp;quot;git add&amp;quot;
    ],
    &amp;quot;**/*.{md,json}&amp;quot;: [
      &amp;quot;prettier --write&amp;quot;,
      &amp;quot;git add&amp;quot;
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;如果你想禁用这个功能，可以把    &lt;code&gt;scripts&lt;/code&gt; 中    &lt;code&gt;&amp;quot;precommit&amp;quot;&lt;/code&gt; 改成    &lt;code&gt;&amp;quot;//precommit&amp;quot;&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;如果你想自定    &lt;code&gt;eslint&lt;/code&gt; 检查代码的规范，可以修改    &lt;code&gt;.eslintrc, .eslintrc.js&lt;/code&gt; 等配置文件&lt;/li&gt;
  &lt;li&gt;如果你想自定    &lt;code&gt;stylelint&lt;/code&gt; 检查代码的规范，可以修改    &lt;code&gt;.stylelintrc, .stylelintrc.js&lt;/code&gt; 等配置文件&lt;/li&gt;
  &lt;li&gt;如果你想忽略某些文件不进行代码检查，可以修改    &lt;code&gt;.eslintignore, .stylelintignore&lt;/code&gt; 配置文件&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016481132"&gt;搭建自己的前端脚手架&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;10. 组件化&lt;/h2&gt;
 &lt;p&gt;当项目拥有了一定量的代码之后，就会发现，有些代码是很多页面共用的，于是把这些代码提取出来，封装成一个组件，供各个地方使用。&lt;/p&gt;
 &lt;p&gt;当拥有多个项目的时候，有些组件需要跨项目使用，一种方式是复制代码到其他项目中，但这种方式会导致组件代码很难维护，所以，一般是用另一种方式：组件化。&lt;/p&gt;
 &lt;p&gt;组件化就是将组件独立成一个项目，然后在其他项目中安装这个组件，才能使用。&lt;/p&gt;
 &lt;p&gt;一般组件化会配合私有 npm 仓库一起用。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在   &lt;code&gt;project1&lt;/code&gt; 中安装   &lt;code&gt;component1， component2&lt;/code&gt; 组件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;# package.json
{
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;component1&amp;quot;: &amp;quot;^0.0.1&amp;quot;,
    &amp;quot;component2&amp;quot;: &amp;quot;^0.0.1&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;import compoennt1 from &amp;apos;compoennt1&amp;apos;;
import compoennt2 from &amp;apos;compoennt2&amp;apos;;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果想要了解怎样写好一个组件（  &lt;code&gt;npm package&lt;/code&gt;），可以参考   &lt;a href="https://segmentfault.com/a/1190000017064659"&gt;从 1 到完美，写一个 js 库、node 库、前端组件库&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015297823"&gt;组件化&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000015297864"&gt;私有 npm 仓库&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000017064659"&gt;从 1 到完美，写一个 js 库、node 库、前端组件库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;11. 测试&lt;/h2&gt;
 &lt;p&gt;测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷，这在项目更新、重构等的过程中尤其重要，因为每当更改一些代码后，你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试，运行一遍测试用例，就知道更改的代码有没有问题、会不会产生影响。&lt;/p&gt;
 &lt;p&gt;一般前端测试分以下几种：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;单元测试：模块单元、函数单元、组件单元等的单元块的测试&lt;/li&gt;
  &lt;li&gt;集成测试：接口依赖（ajax）、I/O 依赖、环境依赖（localStorage、IndexedDB）等的上下文的集成测试&lt;/li&gt;
  &lt;li&gt;样式测试：对样式的测试&lt;/li&gt;
  &lt;li&gt;E2E 测试：端到端测试，也就是在实际生产环境测试整个应用&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;一般会用到下面的一些工具：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://github.com/facebook/jest" rel="nofollow noreferrer"&gt;jest&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/airbnb/enzyme" rel="nofollow noreferrer"&gt;enzyme&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/cypress-io/cypress" rel="nofollow noreferrer"&gt;cypress&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/SeleniumHQ/selenium" rel="nofollow noreferrer"&gt;selenium&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://github.com/GoogleChrome/puppeteer" rel="nofollow noreferrer"&gt;puppeteer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;另外，可以参考   &lt;a href="https://www.jianshu.com/p/1b99af371e66" rel="nofollow noreferrer"&gt;聊聊前端开发的测试&lt;/a&gt;。&lt;/p&gt;
 &lt;h2&gt;12. 构建&lt;/h2&gt;
 &lt;p&gt;一般单页面应用的构建会有   &lt;code&gt;npm run build&lt;/code&gt; 的命令来构建项目，然后会输出一个   &lt;code&gt;html&lt;/code&gt; 文件，一些   &lt;code&gt;js/css/images ...&lt;/code&gt; 文件，然后把这些文件部署到服务器就可以了。&lt;/p&gt;
 &lt;p&gt;多页面应用的构建要复杂一些，因为是多入口的，所以一般会封装构建工具，然后通过参数传入多个入口：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm run build -- page1 page2 dir1/* dir2/all --env test/prod&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;code&gt;page1, page2&lt;/code&gt; 确定构建哪些页面；   &lt;code&gt;dir1/*, dir2/all&lt;/code&gt; 某个目录下所有的页面；   &lt;code&gt;all, *&lt;/code&gt; 整个项目所有的页面&lt;/li&gt;
  &lt;li&gt;有时候可能还会针对不同的服务器环境（比如测试机、正式机）做出不同的构建，可以在后面加参数&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;--&lt;/code&gt; 用来分割    &lt;code&gt;npm&lt;/code&gt; 本身的参数与脚本参数，参考    &lt;a href="https://docs.npmjs.com/cli/run-script.html" rel="nofollow noreferrer"&gt;npm - run-script&lt;/a&gt; 了解详情&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;多页面应用会导出多个   &lt;code&gt;html&lt;/code&gt; 文件，需要注意这些导出的   &lt;code&gt;html&lt;/code&gt; 不要相冲突了。&lt;/p&gt;
 &lt;p&gt;当然，也可以用一些已经封装好的工具，如   &lt;a href="https://github.com/senntyou/lila" rel="nofollow noreferrer"&gt;lila&lt;/a&gt;。&lt;/p&gt;
 &lt;h2&gt;13. 部署&lt;/h2&gt;
 &lt;p&gt;在构建好项目之后，就可以部署到服务器了。&lt;/p&gt;
 &lt;p&gt;传统的方式，可以用   &lt;code&gt;ftp, sftp&lt;/code&gt; 等工具，手动传到服务器，但这种方式比较笨拙，不够自动化。&lt;/p&gt;
 &lt;p&gt;自动化的，可以用一些工具部署到服务器，如   &lt;a href="https://github.com/gulpjs/gulp" rel="nofollow noreferrer"&gt;gulp&lt;/a&gt;、  &lt;a href="https://github.com/teambition/gulp-ssh" rel="nofollow noreferrer"&gt;gulp-ssh&lt;/a&gt;，当然也可以用一些封装的工具，如   &lt;a href="https://github.com/senntyou/md-sync" rel="nofollow noreferrer"&gt;md-sync&lt;/a&gt;、  &lt;a href="https://github.com/senntyou/lila" rel="nofollow noreferrer"&gt;lila&lt;/a&gt; 等&lt;/p&gt;
 &lt;p&gt;以   &lt;code&gt;md-sync&lt;/code&gt; 为例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;npm install md-sync --save-dev&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;code&gt;md-sync.config.js&lt;/code&gt; 配置文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;module.exports = [
  {
    src: &amp;apos;./build/**/*&amp;apos;,
    remotePath: &amp;apos;remotePath&amp;apos;,
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: &amp;apos;host&amp;apos;,
        username: &amp;apos;username&amp;apos;,
        password: &amp;apos;password&amp;apos;
      }
    },
  },
  {
    src: &amp;apos;./build/**/*.html&amp;apos;,
    remotePath: &amp;apos;remotePath2&amp;apos;,
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: &amp;apos;host&amp;apos;,
        username: &amp;apos;username&amp;apos;,
        password: &amp;apos;password&amp;apos;
      }
    },
  },
];&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在   &lt;code&gt;package.json&lt;/code&gt; 的   &lt;code&gt;scripts&lt;/code&gt; 配置好命令：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;quot;scripts&amp;quot;: {
  &amp;quot;deploy&amp;quot;: &amp;quot;md-sync&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;npm run deploy&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;另外，一般大型项目会使用持续集成 +   &lt;code&gt;shell&lt;/code&gt; 命令（如   &lt;code&gt;rsync&lt;/code&gt;）部署。&lt;/p&gt;
 &lt;h2&gt;14. 持续集成测试、构建、部署&lt;/h2&gt;
 &lt;p&gt;一般大型工程的的构建与测试都会花很长的时间，在本地做这些事情的话就不太实际，这就需要做持续集成测试、构建、部署了。&lt;/p&gt;
 &lt;p&gt;持续集成工具用的比较多的：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://jenkins.io/" rel="nofollow noreferrer"&gt;jenkins&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://docs.gitlab.com/ee/ci/" rel="nofollow noreferrer"&gt;gitlab ci&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;code&gt;jenkins&lt;/code&gt; 是通用型的工具，可以与   &lt;a href="https://github.com" rel="nofollow noreferrer"&gt;github&lt;/a&gt;、  &lt;a href="https://bitbucket.org/" rel="nofollow noreferrer"&gt;bitbucket&lt;/a&gt;、  &lt;a href="https://about.gitlab.com/" rel="nofollow noreferrer"&gt;gitlab&lt;/a&gt; 等代码托管服务配合使用，优点是功能强大、插件多、社区活跃，但缺点是配置复杂、使用难度较高。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;gitlab ci&lt;/code&gt; 是   &lt;a href="https://about.gitlab.com/" rel="nofollow noreferrer"&gt;gitlab&lt;/a&gt; 内部自带的持续集成功能，优点是使用简单、配置简单，但缺点是不及   &lt;code&gt;jenkins&lt;/code&gt; 功能强大、绑定   &lt;code&gt;gitlab&lt;/code&gt; 才能使用。&lt;/p&gt;
 &lt;p&gt;以   &lt;code&gt;gitlab&lt;/code&gt; 为例（任务定义在   &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 中）：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;stages:
  - install
  - test
  - build
  - deploy

# 安装依赖
install:
  stage: install
  only:
    - dev
    - master
  script:
    - npm install

# 运行测试用例
test:
  stage: test
  only:
    - dev
    - master
  script:
    - npm run test

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;以上配置表示只要在   &lt;code&gt;dev&lt;/code&gt; 或   &lt;code&gt;master&lt;/code&gt; 分支有代码推送，就会进行持续集成，依次运行：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;npm run test&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;npm run clean&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;npm run deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;最终完成部署。如果中间某个命令失败了，将停止接下的命令的运行，并将错误报告给你。&lt;/p&gt;
 &lt;p&gt;这些操作都在远程机器上完成。&lt;/p&gt;
 &lt;p&gt;=================================================&lt;/p&gt;
 &lt;p&gt;到这里为止，基本上完成了一个项目的搭建、编写、构建。&lt;/p&gt;
 &lt;h2&gt;15. 清理服务器上过期文件&lt;/h2&gt;
 &lt;p&gt;现在前端的项目基本上都会用   &lt;code&gt;webpack&lt;/code&gt; 打包代码，并且文件名（  &lt;code&gt;html&lt;/code&gt; 文件除外）都是   &lt;code&gt;hash&lt;/code&gt; 化的，如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署，可以使用   &lt;a href="https://github.com/senntyou/sclean" rel="nofollow noreferrer"&gt;sclean&lt;/a&gt; 来清除过期文件。&lt;/p&gt;
 &lt;h2&gt;16. 收集前端错误反馈&lt;/h2&gt;
 &lt;p&gt;当用户在用线上的程序时，怎么知道有没有出 bug；如果出 bug 了，报的是什么错；如果是 js 报错，怎么知道是那一行运行出了错？&lt;/p&gt;
 &lt;p&gt;所以，在程序运行时捕捉   &lt;code&gt;js&lt;/code&gt; 脚本错误，并上报到服务器，是非常有必要的。&lt;/p&gt;
 &lt;p&gt;这里就要用到   &lt;code&gt;window.onerror&lt;/code&gt; 了：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) =&amp;gt; {
  const data = {
    title: document.getElementsByTagName(&amp;apos;title&amp;apos;)[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj &amp;amp;&amp;amp; errorObj.message) || &amp;apos;&amp;apos;,
    stack: (errorObj &amp;amp;&amp;amp; errorObj.stack) || &amp;apos;&amp;apos;,
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post(&amp;apos;url&amp;apos;, data); // 上报到服务器
};&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;线上的   &lt;code&gt;js&lt;/code&gt; 脚本都是压缩过的，需要用   &lt;code&gt;sourcemap&lt;/code&gt; 文件与   &lt;a href="https://github.com/mozilla/source-map" rel="nofollow noreferrer"&gt;source-map&lt;/a&gt; 查看原始的报错堆栈信息，可以参考   &lt;a href="https://segmentfault.com/a/1190000016987829"&gt;细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息&lt;/a&gt; 了解详细信息。&lt;/p&gt;
 &lt;p&gt;参考：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016852780"&gt;前端如何高效的与后端协作开发&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000016987829"&gt;细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;后续&lt;/h2&gt;
 &lt;p&gt;更多博客，查看   &lt;a href="https://github.com/senntyou/blogs" rel="nofollow noreferrer"&gt;https://github.com/senntyou/blogs&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;作者：  &lt;a href="https://github.com/senntyou" rel="nofollow noreferrer"&gt;深予之 (@senntyou)&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;版权声明：自由转载-非商用-非衍生-保持署名（  &lt;a href="https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh" rel="nofollow noreferrer"&gt;创意共享3.0许可证&lt;/a&gt;）&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>node.js html css javascript 前端</category>
      <guid isPermaLink="true">https://itindex.net/detail/59009-%E5%BB%BA%E4%B8%80-%E5%89%8D%E7%AB%AF-%E9%A1%B9%E7%9B%AE</guid>
      <pubDate>Tue, 27 Nov 2018 14:27:55 CST</pubDate>
    </item>
    <item>
      <title>前端监控实践——FMP的智能获取算法</title>
      <link>https://itindex.net/detail/58992-%E5%89%8D%E7%AB%AF-%E7%9B%91%E6%8E%A7-%E5%AE%9E%E8%B7%B5</link>
      <description>&lt;p&gt;今天来给大家介绍下前端监控中一个特定指标的获取算法，有人会问，为啥就单单讲一个指标？这是因为，目前大部分的指标，比如白屏时间，dom加载时间等等，都能通过现代浏览器提供的各种api去进行较为精确的获取，而今天讲的这个指标，以往获取他的方式只能是通过逻辑埋点去获取它的值，因此在做一些前端监控时，需要根据业务需要去改变页面对这个值的埋点方式，会比较繁琐，恰巧最近刚刚好在做一些前端监控相关的项目，遇到这个问题时就在想，能不能通过一种无须埋点的方式，将这个值给获取到？倒腾了一段时间，终于把算法弄出来了，今天就来给大家介绍下————FMP(first meaning paint) 指标的智能获取算法&lt;/p&gt;
 &lt;h3&gt;什么是FMP&lt;/h3&gt;
 &lt;p&gt;解答这个问题之前，我们先来了解下现代前端监控性能的主要指标统计方法，在2013年之后，标准组织推出了   &lt;code&gt;performance timing api&lt;/code&gt; ，如下图&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092755?w=1473&amp;h=879" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这个api统计了浏览器从网址开始导航到   &lt;code&gt;window.onload&lt;/code&gt;事件触发的时间点，比如请求开始的时间点——  &lt;code&gt;requestStart&lt;/code&gt;,响应结束的时间点——  &lt;code&gt;responseEnd&lt;/code&gt;,通过这些时间点我们可以计算出一些对页面加载质量有指导意见的时长，比如以下几个:&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;TTFB : ResponseStart - RequestStart (首包时间，关注网络链路耗时)&lt;/li&gt;
  &lt;li&gt;FPT : ResponseEnd - FetchStart （首次渲染时间 / 白屏时间）&lt;/li&gt;
  &lt;li&gt;TTI : DomInteractive - FetchStart （首次可交付时间）&lt;/li&gt;
  &lt;li&gt;Ready : DomContentLoadEventEnd - FetchStart （加载完成时间）&lt;/li&gt;
  &lt;li&gt;Load : LoadEventStart - FetchStart （页面完全加载时间）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;通过这些指标我们可以得到很多有用的web端网页加载信息，建立对网页性能概况&lt;/p&gt;
 &lt;p&gt;以上的指标可以对网页进行数值化的衡量，但是其实这种衡量只能体现一个视角的性能观点，比如TTFB很快，就能代表用户能够很快的看到页面的内容嘛？这个不一定是成立的，因此人们有开始从用户的视角去分析网页加载的性能情况，将用户看待加载过程，分成了以下几个阶段：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;页面是否正在正常加载 （happening）&lt;/li&gt;
  &lt;li&gt;页面加载的内容是否已经足够（useful）&lt;/li&gt;
  &lt;li&gt;页面是否已经可以操作了 （usable）&lt;/li&gt;
  &lt;li&gt;页面是否可以交互，动画是否顺畅（delightful）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;而我们今天讨论的  &lt;code&gt;FMP（first meaningful paint）&lt;/code&gt;，其实就是回答   &lt;code&gt;is it useful&lt;/code&gt;,加载的内容是否已经足够，其实这是一个很难被定义的概念。每个网页都有自己的特点，只有开发者和产品能够比较确定哪个元素加载的时间点属于  &lt;code&gt;FMP&lt;/code&gt;,今天我们就来讨论一下，如何比较智能的去找出页面那个主要的元素，确定页面的  &lt;code&gt;FMP&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;成为FMP元素的条件&lt;/h3&gt;
 &lt;p&gt;首先我们可以看看下面的图：  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092756?w=1400&amp;h=596" title=""&gt;&lt;/img&gt;  &lt;br /&gt;我们可以发现在页面中比较  &lt;code&gt;useful&lt;/code&gt;的内容，都是含有信息量比较丰富的，比如图片，视频，动画，另外就是占可视面积较大的，页面中还存在两种形态的内容可以被视为是  &lt;code&gt;useful&lt;/code&gt;的，一种是单一的块状元素，另外一种是由多个元素组合而成的大元素，比如视频元素，banner图，这种属于单一的块状元素，而像图片列表，多图像的组合，这种属于元素组合  &lt;br /&gt;总结一下成为FMP元素的条件：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;体积占比比较大&lt;/li&gt;
  &lt;li&gt;屏幕内可见占比大&lt;/li&gt;
  &lt;li&gt;资源加载元素占比更高(img, svg , video , object , embed, canvas)&lt;/li&gt;
  &lt;li&gt;主要元素可能是多个组成的&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;算法如何设计&lt;/h3&gt;
 &lt;p&gt;前面介绍了  &lt;code&gt;FMP&lt;/code&gt;的概念还有成为  &lt;code&gt;FMP&lt;/code&gt;的条件，接下来我们来看看如何设计  &lt;code&gt;FMP&lt;/code&gt;获取的算法，按照上面的介绍，我们知道算法分为以下两个部分:&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;获取   &lt;code&gt;FMP元素&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;计算   &lt;code&gt;FMP元素&lt;/code&gt;的加载时间&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;如果有了解过浏览器加载原理的同学都知道，浏览器在在获取到html页面之后会逐步的对html文档进行解析，遇到javascript会停止html文档的解析工作，执行javascript，执行完继续解析html，直到整个页面解析完成为止。页面除了html文档中的元素加载，可能在执行javascript的时候，会产生动态的元素片段加载，一般来说，首屏元素会在这期间加载。因此我们只需要监控元素的加载和加载的时间点，然后再进行计算。&lt;/p&gt;
 &lt;p&gt;具体的算法流程如下图&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092757" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;相关的代码链接我已经放在最后面了，下面我会逐步的讲解整个算法流程&lt;/p&gt;
 &lt;p&gt;我把整个流程分为两个下面两个部分：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;监听元素加载，主要是为了确定普通元素加载的时间点&lt;/li&gt;
  &lt;li&gt;确定   &lt;code&gt;FMP元素&lt;/code&gt;，计算出最终的   &lt;code&gt;FMP&lt;/code&gt;值&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;下面我们按照步骤来分析&lt;/p&gt;
 &lt;h4&gt;初始化监听&lt;/h4&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092758?w=998&amp;h=1168" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;可以看到首先我们先执行了   &lt;code&gt;firstSnapshot&lt;/code&gt;方法，用于记录在代码执行之前加载的元素的时间点&lt;/li&gt;
  &lt;li&gt;接下来初始化   &lt;code&gt;MutationObserver&lt;/code&gt;，开始监听   &lt;code&gt;document&lt;/code&gt;的加载情况，在发生回调的时候，记录下当前到   &lt;code&gt;performance.timing.fetchStart&lt;/code&gt;的时间间隔,然后对body的元素进行深度遍历，进行打点，记录是在哪一次回调的时候记录的，如下图&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092759" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;监听的最后我们会将在   &lt;code&gt;window.onload&lt;/code&gt;的时候去触发检查是否停止监听的条件，如下图&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092760?w=1160&amp;h=462" title=""&gt;&lt;/img&gt;  &lt;br /&gt;如果监听的时间超过  &lt;code&gt;LIMIT&lt;/code&gt;，或者发生回调的时间间隔已经超过1s中，我们认为页面已经稳定，停止dom元素加载的监听，开始进入计算过程&lt;/p&gt;
 &lt;h4&gt;完成监听，进行元素得分计算&lt;/h4&gt;
 &lt;ul&gt;  &lt;li&gt;首先前面我们说了，我们的元素对于页面的贡献是不同的，资源加载的元素会对用户视觉感官的影响比较大，比如图片，带背景的元素，视频等等，因此我设计了一套权重系统，如下：&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092761" title=""&gt;&lt;/img&gt;  &lt;br /&gt;可以看到  &lt;code&gt;svg&lt;/code&gt;,  &lt;code&gt;img&lt;/code&gt;的权重为2，  &lt;code&gt;canvas&lt;/code&gt;,  &lt;code&gt;object&lt;/code&gt;,  &lt;code&gt;embed&lt;/code&gt;,  &lt;code&gt;video&lt;/code&gt;的权重为4，其他的元素为1，  &lt;br /&gt;也就是说，如果一个图片面积为1/2首屏面积，其实他的影响力会和普通元素占满首屏的影响力一样&lt;/p&gt;
 &lt;ul&gt;  &lt;li&gt;接着我们回到代码，我们首先会对整个页面进行深度优先遍历搜索，然后对每一个元素进行进行分数计算，如下图&lt;/li&gt;&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092762?w=1236&amp;h=1674" title=""&gt;&lt;/img&gt;  &lt;br /&gt;可以看到我们通过  &lt;code&gt;element.getBoundingClientRect&lt;/code&gt;获取了元素的位置和大小，然后通过计算  &lt;code&gt;&amp;quot;width * height * weight * 元素在viewport的面积占比&amp;quot;&lt;/code&gt;的乘积，确定元素的最终得分，然后将改元素的子元素得分之和与其得分进行比较，去较大值，记录得分元素集&lt;/p&gt;
 &lt;h4&gt;通过计算确定  &lt;code&gt;FMP&lt;/code&gt;元素，计算最终  &lt;code&gt;FMP&lt;/code&gt;时间&lt;/h4&gt;
 &lt;p&gt;通过上面的步骤我们获取到了一个集合，这个集合是&amp;quot;可视区域内得分最高的元素的集合&amp;quot;,我们会对这个集合的得分取均值，然后过滤出在平均分之上的元素集合，然后进行时间计算&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000017092763" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;可以看到分为两种情况去处理:&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;code&gt;weight&lt;/code&gt;为1的普通元素，那么我们会通过元素上面的标记，去查询之前保存的时间集合，得到这个元素的加载时间点&lt;/li&gt;
  &lt;li&gt;
   &lt;code&gt;weight&lt;/code&gt;不为1的元素，那么其实就存在资源加载情况，元素的加载时间其实是资源加载的时间，我们通过   &lt;code&gt;performance.getEntries&lt;/code&gt;去获取对应资源的加载时间，获取元素的加载速度&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;最后去所有元素最大的加载时间值，作为页面加载的  &lt;code&gt;FMP&lt;/code&gt;时间&lt;/p&gt;
 &lt;h3&gt;最后&lt;/h3&gt;
 &lt;p&gt;以上就是整个算法的比较具体的流程，可能有人会说，这个东西算出来的就是准确的么？这个算法其实是按照特征分析，特定的规则总结出来的算法， 总体来说还是会比较准确，当然web页面的布局如果比较奇特，可能是会存在一些偏差的情况。也希望大家能够一起来丰富这个东西，为  &lt;code&gt;FMP&lt;/code&gt;这个计算方法提出自己的建议  &lt;br /&gt;附上代码   &lt;a href="https://github.com/qbright/fmp-timing" rel="nofollow noreferrer"&gt;链接&lt;/a&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>前端性能 监控 javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/58992-%E5%89%8D%E7%AB%AF-%E7%9B%91%E6%8E%A7-%E5%AE%9E%E8%B7%B5</guid>
      <pubDate>Wed, 21 Nov 2018 18:01:50 CST</pubDate>
    </item>
    <item>
      <title>浏览器输入url到发起http请求所经历的过程</title>
      <link>https://itindex.net/detail/58974-%E6%B5%8F%E8%A7%88%E5%99%A8-url-http</link>
      <description>&lt;h2&gt;用户输入url&lt;/h2&gt;
 &lt;p&gt;当用户输入url，操作系统会将输入事件传递到浏览器中，在这过程中，浏览器可能会做一些预处理，比如 Chrome 会根据历史统计来预估所输入字符对应的网站，例如输入goog，根据之前的历史发现 90% 的概率会访问「www.google.com 」，因此就会在输入回车前就马上开始建立 TCP 链接甚至渲染了。&lt;/p&gt;
 &lt;p&gt;接着是输入url之后，点击回车，这时浏览器会对 URL 进行检查，首先判断协议，如果是 http 就按照 Web 来处理，另外还会对这个 URL 进行安全检查&lt;/p&gt;
 &lt;p&gt;安全检查完成之后，在浏览器内核中会先查看缓存，然后设置 UA 等 HTTP 信息，接着调用不同平台下网络请求的方法。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意：&lt;/strong&gt;  &lt;br /&gt;浏览器和浏览器内核是不同的概念，浏览器指的是 Chrome、Firefox，而浏览器内核则是 Blink、Gecko，浏览器内核只负责渲染，GUI 及网络连接等跨平台工作则是浏览器实现的&lt;/p&gt;
 &lt;h2&gt;http网络请求&lt;/h2&gt;
 &lt;p&gt;通过 DNS 查询 IP；  &lt;br /&gt;通过 Socket 发送数据&lt;/p&gt;
 &lt;h2&gt;dns查询ip&lt;/h2&gt;
 &lt;p&gt;DNS，英文是Domain Name System，中文叫域名系统，是Internet的一项服务，他将域名和IP地址相互映射的一个分布式数据库&lt;/p&gt;
 &lt;p&gt;假设用户在浏览器中输入的是www.google.com，大概过程：&lt;/p&gt;
 &lt;p&gt;如果输入的是域名，则需要进行dns查询，将域名解析成ip；&lt;/p&gt;
 &lt;p&gt;进行DNS查询的主机或软件叫做DNS解析器，用户使用的工作站或电脑都属于解析器。域名解析就是利用DNS解析器得到对应IP过程，解析器会向域名服务器进行查询处理。&lt;/p&gt;
 &lt;p&gt;主要过程如下：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;从浏览器缓存中查找域名www.google.com的IP地址&lt;/li&gt;
  &lt;li&gt;在浏览器缓存中没找到，就在操作系统缓存中查找，这一步中也会查找本机的hosts看看有没有对应的域名映射（当然已经缓存在系统DNS缓存中了）&lt;/li&gt;
  &lt;li&gt;在系统中也没有的话，就到你的路由器来查找，因为路由器一般也会有自己的DNS缓存&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;如果以上都没有找到，则继续往下向dns域名服务器查询&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;用户电脑的解析器向LDNS（也就是Local   DNS，互联网服务提供商ISP），发起域名解析请求，查询www.google.com的IP地址，这是一个递归查找过程&lt;/li&gt;
  &lt;li&gt;在缓存没有命中的情况下，LDNS向根域名服务器.查询www.google.com的IP地址，LDNS的查询过程是一个迭代查询的过程&lt;/li&gt;
  &lt;li&gt;根告诉LDNS，我不知道www.google.com对应的IP，但是我知道你可以问com域的授权服务器，这个域归他管&lt;/li&gt;
  &lt;li&gt;LDNS向com的授权服务器问www.google.com对应的IP地址&lt;/li&gt;
  &lt;li&gt;com告诉LDNS，我不知道www.google.com对应的IP，但是我知道你可以问google.com域的授权服务器，这个域归他管&lt;/li&gt;
  &lt;li&gt;LDNS向google.com的授权服务器问www.google.com对应的IP地址&lt;/li&gt;
  &lt;li&gt;google.com查询自己的ZONE文件（也称区域文件记录），找到了www.google.com对应的IP地址，返回给LDNS&lt;/li&gt;
  &lt;li&gt;LDNS本地缓存一份记录，把结果返回给用户电脑的解析器&lt;/li&gt;
  &lt;li&gt;在这之后，用户电脑的解析器拿到结果后，缓存在自己操作系统DNS缓存中，同时返回给浏览器，浏览器依旧会缓存一段时间。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;注意&lt;/strong&gt;，  &lt;br /&gt;域名查询时有可能是经过了CDN调度器的（如果有cdn存储功能的话）&lt;/p&gt;
 &lt;p&gt;而且，需要知道dns解析是很耗时的，因此如果解析域名过多，会让首屏加载变得过慢，可以考虑dns-prefetch优化&lt;/p&gt;
 &lt;h2&gt;tcp/ip请求&lt;/h2&gt;
 &lt;p&gt;有了 IP 地址，就可以通过 Socket API 来发送数据了，这时可以选择 TCP 或 UDP 协议。&lt;/p&gt;
 &lt;p&gt;http本质是tcp协议。&lt;/p&gt;
 &lt;p&gt;TCP是一种面向有连接的传输层协议。他可以保证两端（发送端和接收端）通信主机之间的通信可达。他能够处理在传输过程中丢包、传输顺序乱掉等异常情况；此外他还能有效利用宽带，缓解网络拥堵。&lt;/p&gt;
 &lt;p&gt;建立TCP连接一开始都要经过三次握手：&lt;/p&gt;
 &lt;p&gt;第一次握手，请求建立连接，发送端发送连接请求报文  &lt;br /&gt;第二次握手，接收端收到发送端发过来的报文，可知发送端现在要建立联机。然后接收端会向发送端发送一个报文&lt;/p&gt;
 &lt;p&gt;第三次握手，发送端收到了发送过来的报文，需要检查一下返回的内容是否是正确的；若正确的话，发送端再次发送确认包&lt;/p&gt;
 &lt;p&gt;在TCP连接建立完成之后就可以发送HTTP请求了。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意&lt;/strong&gt;  &lt;br /&gt;浏览器对同一个域名有连接数限制，大部分是 6，http1.0中往往一个资源下载就需要对应一个tcp/ip请求，而像 HTTP 2.0 协议尽管只使用一个 TCP 连接来传输数据，但性能反而更好，而且还能实现请求优先级。&lt;/p&gt;
 &lt;p&gt;参考文章：  &lt;br /&gt;  &lt;a href="http://fex.baidu.com/blog/2014/05/what-happen/" rel="nofollow noreferrer"&gt;http://fex.baidu.com/blog/201...&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://blog.csdn.net/dojiangv/article/details/51794535" rel="nofollow noreferrer"&gt;https://blog.csdn.net/dojiang...&lt;/a&gt;  &lt;br /&gt;  &lt;a href="https://segmentfault.com/a/1190000013662126#articleHeader17"&gt;https://segmentfault.com/a/11...&lt;/a&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>html css javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/58974-%E6%B5%8F%E8%A7%88%E5%99%A8-url-http</guid>
      <pubDate>Sun, 18 Nov 2018 16:33:45 CST</pubDate>
    </item>
    <item>
      <title>程序员练级攻略（2018)  与我的专栏</title>
      <link>https://itindex.net/detail/58422-%E7%A8%8B%E5%BA%8F%E5%91%98-%E6%94%BB%E7%95%A5-%E4%B8%93%E6%A0%8F</link>
      <description>&lt;p&gt;  &lt;img alt="" height="262" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#19987;&amp;#26639;-300x262.jpg" width="300"&gt;&lt;/img&gt;写极客时间8个月了，我的专栏现在有一定的积累了，今天想自己推荐一下。因为最新的系列《程序员练级攻略（2018）版》正在连载中，而且文章积累量到了我也有比较足的自信向大家推荐我的这个专栏了。推荐就从最新的这一系统的文章开始。&lt;/p&gt;
 &lt;p&gt;2011年，我在   &lt;a href="https://coolshell.cn/"&gt;CoolShell&lt;/a&gt; 上发表了 《  &lt;a href="https://coolshell.cn/articles/4990.html"&gt;程序员技术练级攻略&lt;/a&gt;》一文，得到了很多人的好评（转载的不算，在我的网站上都有近1000W的访问量了）。并且陆续收到了一些人的反馈，说跟着这篇文章找到了不错的工作。几年过去，也收到了好些邮件和私信，希望我把这篇文章更新一下，因为他们觉得有点落伍了。是的，  &lt;strong&gt;老实说，抛开这几年技术的更新迭代不说，那篇文章写得也不算特别系统，同时标准也有点低，当时是给一个想要入门的朋友写的，所以，非常有必要从头更新一下《程序员练级攻略》这一主题&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;目前，我在我极客时间的专栏上更新《程序员练级攻略（2018版）》。升级版的《程序员练级攻略》会比Coolshell上的内容更多，也更专业。这篇文章有【入门篇】、【修养篇】、【专业基础篇】、【软件设计篇】、【高手成长篇】五大篇章，它们会帮助你从零开始，一步步地，系统地，从陌生到熟悉，到理解掌握，从编码到设计再到架构，从码农到程序员再到工程师再到架构师的一步一步进阶，完成从普通到精通到卓越的完美转身……&lt;/p&gt;
 &lt;p&gt;这篇文章是我写得最累也是最痛苦的文章，原因如下：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;    &lt;strong&gt;学习路径的梳理&lt;/strong&gt;。这是一份计算编程相关知识地图，也是一份成长和学习路径。所以有太多的推敲了，知识的路径，体，地图……这让我费了很多工夫，感觉像在编写一本教材一样，即不能太高大上，也不能误人子弟。&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;新旧知识的取舍。&lt;/strong&gt;另外，因为我的成长经历中很多技术都成了过去时，所以对于新时代的程序员应该学习新的技术，然后，很多基础技术在今天依然管用，所以，在这点上，哪些要那些不要，也花了我很多的工夫。&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;文章书籍的推荐&lt;/strong&gt;。为了推荐最好的学习资料和资源，老实说，我几乎翻遍了整个互联网，进行了大量的阅读和比较。这个过程让我也受益非浅。一开始，这篇文章的大小居然在500K左右，太多的信息就是没有信息，所以在信息的筛选上我花费了很多的工夫，删掉了60%的内容。但是，依然很宠大。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;总之，你一定会被这篇文章的内容所吓到的，是的，我就是故意这样做的，因为，这本来就没有什么捷径，也不可能速成，很多知识都是硬骨头，你只能一口一口的啃，我故意这样做就是为了让你不要有“速成”的幻想，也可以轻而一举的吓退那些不想用功不想努力的人&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;但是，我们也要知道《易经》有云：“  &lt;strong&gt;取法其上，得乎其中，取法其中，得乎其下，取法其下，法不得也&lt;/strong&gt;”。所以，我这里会给你立个比较高标准，你要努力达到，相信我，就算是达不到，也会比你一开始期望的要高很多……&lt;/p&gt;
 &lt;p&gt;下面是这份练级攻略的目录，目前只在极客时间上发布，你需要付费阅读（在本文最后有相关的二维码）。&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="1937" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#31243;&amp;#24207;&amp;#21592;&amp;#32451;&amp;#32423;&amp;#25915;&amp;#30053;.png" width="290"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;那么，除程序员练级攻略外，我还写了哪些内容？下面是迄今为止我所有的文章的目录。你可以在下面看一下相关的目录。这也算是我开收费专栏来8个月给大家的一份答卷吧。我也没有想到，我居然写了这么多的文章，而且对很多人都很有用。&lt;/p&gt;
 &lt;p&gt;首先是个人成长和经验之谈的东西，在这里的文章还没有完全更新完，未来要更新什么我也不清楚，但是可以呈现出来的内容和方向如下所示，供你参考。对于个人成长中的内容，都是我多年来的心得和体会，从读者的反馈来看是非常不错的，你一定要要阅读的。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="1024" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#20010;&amp;#20154;&amp;#25104;&amp;#38271;&amp;#21644;&amp;#32463;&amp;#39564;&amp;#20043;&amp;#35848;-319x1024.png" width="319"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;分布式系统架构，我一共出了两个系列，一个是分布式系统架构的本质，另一个是设计模式。前者偏概念，后者偏技术。这里旨在让你看到整个分布式系统设计的一个非常系统的蓝图，但是因为在手机端上，不可能写得非常细，所以，会缺失一些细节，这些细节我是故意缺失的，主要是有几方面的原因，&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;一方面，这是为了阅读的效果，手机上的文章不过长，所以，不能有太多的细节。&lt;/li&gt;
  &lt;li&gt;另一方面，也是是想留给大家自行学习，而不是一定要我把饭喂到你的嘴里，你才能吃得着。   &lt;strong&gt;学习不只是为要答案，而是学方法&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;最后是我的私心，因为我也在创业，所以，技术细节上东西正是我在做的产品，所以，如果你想了解得更细，你需要和我有更商业合作。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="" height="689" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#20998;&amp;#24067;&amp;#24335;&amp;#26550;&amp;#26500;&amp;#30340;&amp;#26412;&amp;#36136;.png" width="321"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="1065" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#20998;&amp;#24067;&amp;#24335;&amp;#26550;&amp;#26500;&amp;#35774;&amp;#35745;&amp;#27169;&amp;#24335;-&amp;#24377;&amp;#21147;&amp;#31687;.png" width="331"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="669" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#20998;&amp;#24067;&amp;#24335;&amp;#26550;&amp;#26500;&amp;#35774;&amp;#35745;&amp;#27169;&amp;#24335;-&amp;#31649;&amp;#29702;&amp;#31687;.png" width="353"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="592" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#20998;&amp;#24067;&amp;#24335;&amp;#26550;&amp;#26500;&amp;#35774;&amp;#35745;&amp;#27169;&amp;#24335;-&amp;#24615;&amp;#33021;&amp;#31687;.png" width="328"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;区块链的技术专栏本来不在我的写作计划中的，但是因为来问我这方面的技术人太多了，所以，就被问了一系列的文章，这里的文章除了一些技术上的科普，同样有有很多我的观点，你不但可以学到技术，还可以了解一些金融知识和相关的逻辑，我个人觉得这篇文章是让你有独立思考的文章。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="771" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#21306;&amp;#22359;&amp;#38142;&amp;#25216;&amp;#26415;.png" width="304"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我的专栏还在继续，接下来还有一个系列的文章——《从技术到管理》，欢迎关注，也欢迎扫码订阅。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;最后友情提示一下：在手机上学习并不是最好的学习方式，也不要在我的专栏上进行学习，把我的专栏当成一个你的助手，当成一个向导，当成一个跳板，真正的学习还是要在线下，专心的，系统地、有讨论地、不断实践地学习，这点希望大家切记！&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" height="580" src="https://coolshell.cn/wp-content/uploads/2018/05/&amp;#19987;&amp;#26639;.jpg" width="665"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（全文完）&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p align="center"&gt;  &lt;img src="https://coolshell.cn//wp-content/uploads/2009/04/qrcode_for_gh_dd9d8c843f20_860-300x300.jpg"&gt;&lt;/img&gt;  &lt;br /&gt;关注CoolShell微信公众账号可以在手机端搜索文章&lt;/p&gt;
 &lt;div&gt;
  &lt;p align="center"&gt;   &lt;strong&gt;（转载本站文章请注明作者和出处     &lt;a href="https://coolshell.cn/"&gt;酷 壳 – CoolShell&lt;/a&gt; ，请勿用于任何商业用途）&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;
 &lt;div&gt;——===   &lt;strong&gt;访问    &lt;a href="http://coolshell.cn/404/" target="_blank"&gt;酷壳404页面&lt;/a&gt; 寻找遗失儿童。&lt;/strong&gt; ===——&lt;/div&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;h3&gt;相关文章&lt;/h3&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/17459.html"&gt;      &lt;img alt="&amp;#20851;&amp;#20110;&amp;#39640;&amp;#21487;&amp;#29992;&amp;#30340;&amp;#31995;&amp;#32479;" height="150" src="https://coolshell.cn/wp-content/uploads/2016/08/HighAvailability-BK-150x150.png" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/17459.html"&gt;关于高可用的系统&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/4990.html"&gt;      &lt;img alt="&amp;#31243;&amp;#24207;&amp;#21592;&amp;#25216;&amp;#26415;&amp;#32451;&amp;#32423;&amp;#25915;&amp;#30053;" height="150" src="https://coolshell.cn/wp-content/uploads/2011/07/programmer-150x150.png" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/4990.html"&gt;程序员技术练级攻略&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/17680.html"&gt;      &lt;img alt="&amp;#20174;Gitlab&amp;#35823;&amp;#21024;&amp;#38500;&amp;#25968;&amp;#25454;&amp;#24211;&amp;#24819;&amp;#21040;&amp;#30340;" height="150" src="https://coolshell.cn/wp-content/uploads/2017/02/gitlab-600-150x150.jpg" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/17680.html"&gt;从Gitlab误删除数据库想到的&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/8088.html"&gt;      &lt;img alt="&amp;#23545;&amp;#25216;&amp;#26415;&amp;#30340;&amp;#24577;&amp;#24230;" height="150" src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/1.jpg" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/8088.html"&gt;对技术的态度&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/4102.html"&gt;      &lt;img alt="&amp;#22914;&amp;#20309;&amp;#23398;&amp;#22909;C&amp;#35821;&amp;#35328;" height="150" src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/17.jpg" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/4102.html"&gt;如何学好C语言&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;     &lt;a href="https://coolshell.cn/articles/12052.html"&gt;      &lt;img alt="Leetcode &amp;#32534;&amp;#31243;&amp;#35757;&amp;#32451;" height="150" src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/5.jpg" width="150"&gt;&lt;/img&gt;&lt;/a&gt;     &lt;a href="https://coolshell.cn/articles/12052.html"&gt;Leetcode 编程训练&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>技术读物 杂项资源 C++ Java Javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/58422-%E7%A8%8B%E5%BA%8F%E5%91%98-%E6%94%BB%E7%95%A5-%E4%B8%93%E6%A0%8F</guid>
      <pubDate>Tue, 29 May 2018 12:38:23 CST</pubDate>
    </item>
    <item>
      <title>python使用深度神经网络实现识别暹罗与英短</title>
      <link>https://itindex.net/detail/58050-python-%E6%B7%B1%E5%BA%A6-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C</link>
      <description>&lt;p&gt;先来上两张图看看那种猫是暹罗？那种猫是英短？  &lt;br /&gt;
第一张暹罗  &lt;br /&gt;
  &lt;img alt="" height="1000" src="http://www.cobub.com/wp-content/uploads/2018/02/image001.jpg" width="667"&gt;&lt;/img&gt;  &lt;br /&gt;
第二张英短  &lt;br /&gt;
  &lt;img alt="" height="288" src="http://www.cobub.com/wp-content/uploads/2018/02/image003.jpg" width="433"&gt;&lt;/img&gt;  &lt;br /&gt;
你以后是不是可以识别了暹罗和英短了？大概能，好像又不能。这是因为素材太少了，我们看这两张图能分别提取出来短特征太少了。那如果我们暹罗短放100张图，英短放100张图给大家参考，再给一张暹罗或者英短短照片是不是就能识别出来是那种猫了，即使不能完全认出来，是不是也有90%可能是可以猜猜对。那么如果提供500张暹罗500张英短短图片呢，是不是猜对的概率可以更高？  &lt;br /&gt;
我们是怎么识别暹罗和英短的呢？当然是先归纳两种猫的特征如面部颜色分布、眼睛的颜色等等，当再有一张要识别短图片时，我们就看看面部颜色分布、眼睛颜色是不是可暹罗的特征一致。  &lt;br /&gt;
同样把识别暹罗和英短的方法教给计算机后，是不是计算机也可以识别这两种猫？  &lt;br /&gt;
那么计算机是怎么识别图像的呢？先来看一下计算机是怎么存储图像的。  &lt;br /&gt;
  &lt;img alt="" height="290" src="http://www.cobub.com/wp-content/uploads/2018/02/image005.gif" width="291"&gt;&lt;/img&gt;  &lt;br /&gt;
图像在计算机里是一堆按顺序排列的数字，1到255，这是一个只有黑白色的图，但是颜色千变万化离不开三原色——红绿蓝。  &lt;br /&gt;
  &lt;img alt="" height="252" src="http://www.cobub.com/wp-content/uploads/2018/02/image006.jpg" width="305"&gt;&lt;/img&gt;  &lt;img alt="" height="210" src="http://www.cobub.com/wp-content/uploads/2018/02/image008.jpg" width="600"&gt;&lt;/img&gt;  &lt;br /&gt;
这样，一张图片在计算机里就是一个长方体！depth为3的长方体。每一层都是1到255的数字。  &lt;br /&gt;
让计算机识别图片，就要先让计算机了解它要识别短图片有那些特征。提取图片中的特征就是识别图片要做的主要工作。  &lt;br /&gt;
下面就该主角出场了，卷及神经网络(Convolutional Neural Network, CNN).  &lt;br /&gt;
最简单的卷积神经网络就长下面的样子。  &lt;br /&gt;
  &lt;img alt="" height="819" src="http://www.cobub.com/wp-content/uploads/2018/02/image009.png" width="2344"&gt;&lt;/img&gt;  &lt;br /&gt;
分为输入、卷积层、池化层(采样层)、全连接和输出。每一层都将最重要的识别信息进行压缩，并传导至下一层。  &lt;br /&gt;
卷积层：帮助提取特征，越深(层数多)的卷积神经网络会提取越具体的特征，越浅的网络提取越浅显的特征。  &lt;br /&gt;
池化层：减少图片的分辨率，减少特征映射。  &lt;br /&gt;
全连接：扁平化图片特征，将图片当成数组，并将像素值当作预测图像中数值的特征。  &lt;br /&gt;
•卷积层  &lt;br /&gt;
卷积层从图片中提取特征，图片在计算机中就上按我们上面说的格式存储的（长方体），先取一层提取特征，怎么提取？使用卷积核（权值）。做如下短操作：  &lt;br /&gt;
  &lt;img alt="" height="168" src="http://www.cobub.com/wp-content/uploads/2018/02/image011.gif" width="487"&gt;&lt;/img&gt;  &lt;br /&gt;
观察左右两个矩阵，矩阵大小从6×6 变成了 4×4，但数字的大小分布好像还是一致的。看下真实图片：  &lt;br /&gt;
  &lt;img alt="" height="249" src="http://www.cobub.com/wp-content/uploads/2018/02/image012.jpg" width="640"&gt;&lt;/img&gt;  &lt;br /&gt;
图片好像变模糊了，但这两个图片大小没变是怎么回事呢？其实是用了如下的方式：same padding  &lt;br /&gt;
  &lt;img alt="" height="189" src="http://www.cobub.com/wp-content/uploads/2018/02/image014.gif" width="419"&gt;&lt;/img&gt;  &lt;br /&gt;
在6×6的矩阵周围加了一圈0，再做卷积的时候得到的还是一个6×6的矩阵，为什么加一圈0这个和卷积核大小、步长和边界有关。自己算吧。  &lt;br /&gt;
上面是在一个6×6的矩阵上使用3X3的矩阵做的演示。在真实的图片上做卷积是什么样的呢？如下图：  &lt;br /&gt;
  &lt;img alt="" height="220" src="http://www.cobub.com/wp-content/uploads/2018/02/image015.jpg" width="640"&gt;&lt;/img&gt;  &lt;br /&gt;
对一个32x32x3的图使用10个5x5x3的filter做卷积得到一个28x28x10的激活图(激活图是卷积层的输出).  &lt;br /&gt;
•池化层  &lt;br /&gt;
减少图片的分辨率，减少特征映射。怎么减少的呢？  &lt;br /&gt;
池化在每一个纵深维度上独自完成，因此图像的纵深保持不变。池化层的最常见形式是最大池化。  &lt;br /&gt;
可以看到图像明显的变小了。如图：  &lt;br /&gt;
  &lt;img alt="" height="110" src="http://www.cobub.com/wp-content/uploads/2018/02/image017.jpg" width="259"&gt;&lt;/img&gt;  &lt;br /&gt;
在激活图的每一层的二维矩阵上按2×2提取最大值得到新的图。真实效果如下：  &lt;br /&gt;
  &lt;img alt="" height="246" src="http://www.cobub.com/wp-content/uploads/2018/02/image019.jpg" width="640"&gt;&lt;/img&gt;  &lt;br /&gt;
随着卷积层和池化层的增加，对应滤波器检测的特征就更加复杂。随着累积，就可以检测越来越复杂的特征。这里还有一个卷积核优化的问题，多次训练优化卷积核。  &lt;br /&gt;
下面使用apple的卷积神经网络框架TuriCreate实现区分暹罗和英短。(先说一下我是在win10下装的熬夜把电脑重装了不下3次，系统要有wls,不要用企业版，mac系统和ubuntu系统下安装turicreae比较方便)  &lt;br /&gt;
首先准备训练用图片暹罗50张，英短50长。测试用图片10张。  &lt;br /&gt;
上代码：（开发工具anaconda，python 2.7）  &lt;br /&gt;
  &lt;img alt="" height="525" src="http://www.cobub.com/wp-content/uploads/2018/02/&amp;#20195;&amp;#30721;1.png" width="573"&gt;&lt;/img&gt;  &lt;img alt="" height="193" src="http://www.cobub.com/wp-content/uploads/2018/02/&amp;#20195;&amp;#30721;4.png" width="571"&gt;&lt;/img&gt;  &lt;br /&gt;
数据放到了h盘image目录下，我是在win10下装的ubuntu,所以h盘挂在mnt/下。  &lt;br /&gt;
  &lt;img alt="" height="164" src="http://www.cobub.com/wp-content/uploads/2018/02/image021.png" width="614"&gt;&lt;/img&gt;  &lt;br /&gt;
test的文件：(x指暹罗，y指英短，这样命名是为了代码里给测试图片区分猫咪类型)  &lt;br /&gt;
  &lt;img alt="" height="264" src="http://www.cobub.com/wp-content/uploads/2018/02/image023.png" width="946"&gt;&lt;/img&gt;  &lt;br /&gt;
test_data[‘label’] = test_data[‘path’].apply(lambda path: ‘xianluo’ if ‘x’ in path else ‘yingduan’)  &lt;br /&gt;
第一次结果如下：  &lt;br /&gt;
  &lt;img alt="" height="526" src="http://www.cobub.com/wp-content/uploads/2018/02/image024.png" width="811"&gt;&lt;/img&gt;  &lt;br /&gt;
训练精度0.955 验证精度才0.75 正确率才0.5。好吧，看来是学习得太少，得上三年高考五年模拟版，将暹罗和英短的图片都增加到100张。在看结果。  &lt;br /&gt;
  &lt;img alt="" height="484" src="http://www.cobub.com/wp-content/uploads/2018/02/image026.png" width="901"&gt;&lt;/img&gt;  &lt;br /&gt;
这次训练精度就达到0.987了，验证精度1.0，正确率1.0 牛逼了。  &lt;br /&gt;
看下turicreate识别的结果：  &lt;br /&gt;
  &lt;img alt="" height="240" src="http://www.cobub.com/wp-content/uploads/2018/02/image028.png" width="975"&gt;&lt;/img&gt;  &lt;br /&gt;
我们实际图片上猫是：(红色为真实的猫的类型-在代码里根据图片名称标记的，绿色为识别出来的猫的类型)  &lt;br /&gt;
  &lt;img alt="" height="296" src="http://www.cobub.com/wp-content/uploads/2018/02/image029.png" width="1034"&gt;&lt;/img&gt;  &lt;br /&gt;
可以看到两者是一致的。牛逼了训练数据才两百张图片，就可以达到这种效果。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://www.cobub.com/python-uses-deep-neural-networks-to-identify-siamese-and-british-short/" rel="nofollow"&gt;python使用深度神经网络实现识别暹罗与英短&lt;/a&gt;，首发于  &lt;a href="http://www.cobub.com" rel="nofollow"&gt;Cobub&lt;/a&gt;。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>博客 未分类 APP统计 J-Query JavaScript APP 数据统计 Python</category>
      <guid isPermaLink="true">https://itindex.net/detail/58050-python-%E6%B7%B1%E5%BA%A6-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C</guid>
      <pubDate>Mon, 05 Feb 2018 11:25:59 CST</pubDate>
    </item>
    <item>
      <title>深入理解JS引擎的执行机制</title>
      <link>https://itindex.net/detail/57907-%E7%90%86%E8%A7%A3-js-%E5%BC%95%E6%93%8E</link>
      <description>&lt;h1&gt;深入理解JS引擎的执行机制&lt;/h1&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/blogs#1"&gt;1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? &lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/blogs#2"&gt;2.JS中的event loop(1)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/blogs#3"&gt;3.JS中的event loop(2)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/blogs#4"&gt;4.说说setTimeout&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;首先,请牢记2点:&lt;/h2&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;(1) JS是单线程语言&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;(2) JS用过Event Loop是JS的执行机制。想深入了解JS的执行,就等于深入了解JS里的event loop&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;  &lt;a&gt;1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢?&lt;/a&gt;&lt;/h3&gt;
 &lt;p&gt;技术的出现,都跟现实世界里的应用场景密切相关的。&lt;/p&gt;
 &lt;p&gt;同样的,我们就结合现实场景,来回答这三个问题&lt;/p&gt;
 &lt;blockquote&gt;(1) JS为什么是单线程的?&lt;/blockquote&gt;
 &lt;p&gt;JS最初被设计用在浏览器中,那么想象一下,如果浏览器中的JS是多线程的。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;场景描述:

那么现在有2个进程,process1 process2,由于是多进程的JS,所以他们对同一个dom,同时进行操作

process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这样想,JS为什么被设计成单线程应该就容易理解了吧。&lt;/p&gt;
 &lt;blockquote&gt;(2) JS为什么需要异步?&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;场景描述:

如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。
对于用户而言,阻塞就意味着&amp;quot;卡死&amp;quot;,这样就导致了很差的用户体验
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;所以,JS中存在异步执行。&lt;/p&gt;
 &lt;blockquote&gt;(3) JS单线程又是如何实现异步的呢?&lt;/blockquote&gt;
 &lt;p&gt;既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢? &lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;  &lt;a&gt;2.JS中的event loop(1)&lt;/a&gt;&lt;/h3&gt;
 &lt;p&gt;例1,观察它的执行顺序&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    console.log(1)
    
    setTimeout(function(){
        console.log(2)
    },0)

    console.log(3)
    &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;运行结果是: 1 3 2&lt;/p&gt;
 &lt;p&gt;也就是说,setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足一定条件后,才去执行的,这类代码,我们叫异步代码。&lt;/p&gt;
 &lt;p&gt;所以,这里我们首先知道了JS里的一种分类方式,就是将任务分为: 同步任务和异步任务&lt;/p&gt;
 &lt;p&gt;图片描述&lt;/p&gt;
 &lt;p&gt;按照这种分类方式:JS的执行机制是&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table&lt;/li&gt;
  &lt;li&gt;异步任务在event table中注册函数,当满足触发条件后,被推入event queue&lt;/li&gt;
  &lt;li&gt;同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;以上三步循环执行,这就是event loop&lt;/p&gt;
 &lt;p&gt;所以上面的例子,你是否可以描述它的执行顺序了呢?&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;console.log(1) 是同步任务,放入主线程里
setTimeout() 是异步任务,被放入event table, 0秒之后被推入event queue里
console.log(3 是同步任务,放到主线程里

当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;  &lt;a&gt;3.JS中的event loop(2)&lt;/a&gt;&lt;/h3&gt;
 &lt;p&gt;所以,上面关于event loop就是我对JS执行机制的理解,直到我遇到了下面这段代码&lt;/p&gt;
 &lt;p&gt;例2:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt; setTimeout(function(){
     console.log(&amp;apos;定时器开始啦&amp;apos;)
 });
 
 new Promise(function(resolve){
     console.log(&amp;apos;马上执行for循环啦&amp;apos;);
     for(var i = 0; i &amp;lt; 10000; i++){
         i == 99 &amp;amp;&amp;amp; resolve();
     }
 }).then(function(){
     console.log(&amp;apos;执行then函数啦&amp;apos;)
 });
 
 console.log(&amp;apos;代码执行结束&amp;apos;);
    &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;尝试按照,上文我们刚学到的JS执行机制去分析&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;setTimeout 是异步任务,被放到event table

new Promise 是同步任务,被放到主进程里,直接执行打印 console.log(&amp;apos;马上执行for循环啦&amp;apos;)

.then里的函数是 异步任务,被放到event table

 console.log(&amp;apos;代码执行结束&amp;apos;)是同步代码,被放到主进程里,直接执行
 &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;所以,结果是 【马上执行for循环啦 --- 代码执行结束 --- 定时器开始啦 --- 执行then函数啦】吗?&lt;/p&gt;
 &lt;p&gt;亲自执行后,结果居然不是这样,而是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】&lt;/p&gt;
 &lt;p&gt;那么,难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。&lt;/p&gt;
 &lt;p&gt;而准确的划分方式是:&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;macro-task(宏任务)：包括整体代码script，setTimeout，setInterval&lt;/li&gt;
  &lt;li&gt;micro-task(微任务)：Promise，process.nextTick&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="clipboard.png" src="https://segmentfault.com/img/bV1TKz?w=879&amp;h=723" title="clipboard.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;按照这种分类方式:JS的执行机制是&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里&lt;/li&gt;
  &lt;li&gt;当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。&lt;/p&gt;
 &lt;p&gt;尝试按照刚学的执行机制,去分析例2:&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里

遇到 new Promise直接执行,打印&amp;quot;马上执行for循环啦&amp;quot;

遇到then方法,是微任务,将其放到微任务的【队列里】

打印 &amp;quot;代码执行结束&amp;quot;

本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印&amp;quot;执行then函数啦&amp;quot;

到此,本轮的event loop 全部完成。


下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印&amp;quot;定时器开始啦&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;所以最后的执行顺序是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】&lt;/p&gt;
 &lt;h3&gt;  &lt;a&gt;4. 谈谈setTimeout&lt;/a&gt;&lt;/h3&gt;
 &lt;p&gt;这段setTimeout代码什么意思? 我们一般说: 3秒后,会执行setTimeout里的那个函数&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt; setTimeout(function(){
    console.log(&amp;apos;执行了&amp;apos;)
 },3000)    &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;但是这种说并不严谨,准确的解释是: 3秒后,setTimeout里的函数被会推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;   &lt;em&gt;所以只有满足 (1)3秒后 (2)主线程空闲,同时满足时,才会3秒后执行该函数&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;如果主线程执行内容很多,执行时间超过3秒,比如执行了10秒,那么这个函数只能10秒后执行了&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>javascript</category>
      <guid isPermaLink="true">https://itindex.net/detail/57907-%E7%90%86%E8%A7%A3-js-%E5%BC%95%E6%93%8E</guid>
      <pubDate>Thu, 11 Jan 2018 21:18:39 CST</pubDate>
    </item>
    <item>
      <title>现代浏览器性能优化-CSS篇</title>
      <link>https://itindex.net/detail/57837-%E7%8E%B0%E4%BB%A3-%E6%B5%8F%E8%A7%88%E5%99%A8-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</link>
      <description>&lt;p&gt;我来填坑了，CSS篇终于写出来了，如果你没看过前面的JS篇，可以  &lt;a href="https://github.com/GeoffZhu/geoffzhu.github.io/issues/2"&gt;在这里观看&lt;/a&gt;。&lt;/p&gt;
 &lt;blockquote&gt;众所周知，CSS的加载会阻塞浏览器渲染或是引起浏览器重绘，目前业界普遍推荐把CSS放到  &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;中，防止在CSS还没加载完，DOM就已经绘制出来了，造成CSS加载完成后的重绘。那在现代浏览器中我们有没有办法提高首屏渲染速度那？&lt;/blockquote&gt;
 &lt;p&gt;你是不是经常在第一次打开某个网站的时候看到这种情况，本来的页面是这样的  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000012643588?w=1574&amp;h=698" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;实际上刚加载出来的是这样的  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000012643589" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;字体文件没加载出来，或者加载的太慢了&lt;/p&gt;
 &lt;h2&gt;理解CSS解析过程&lt;/h2&gt;
 &lt;p&gt;以下面这段HTML为例，解释一遍CSS加载解析的过程。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;!-- headStyle.css中存在字体文件webfont.woff2 --&amp;gt;
  &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; type=&amp;quot;text/css&amp;quot; href=&amp;quot;/headStyle.css&amp;quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;p&amp;gt;Text&amp;lt;/p&amp;gt;
  &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; type=&amp;quot;text/css&amp;quot; href=&amp;quot;/bodyEndStyle.css&amp;quot;&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;浏览器自上而下读取HTML文档，当发现headStyle.css的时候，停止Parser HTML，开始下载headStyle.css，解析headStyle.css的过程中发现字体文件webfont.woff2，开始下载webfont.woff2，并继续解析css生成CSSStyleSheet。解析完毕后，继续Parser HTML，当发现p标签时，会将p标签结合当前的CSSStyleSheet展示出来，此时用户屏幕中已经有p标签的内容了。当浏览器发现bodyEndStyle.css时，就会下载headStyle.css，解析CSS，然后更新CSSStyleSheet，这时会引起一次重绘。当字体下载完毕的时候也会引起一次重绘。&lt;/p&gt;
 &lt;p&gt;这个过程中，有两个非常严重的问题。一、如果headStyle.css文件很大，浏览器需要解析很多行CSS后才能还有个字体文件需要下载，其实此时已经很晚了，字体下载时间稍长一点，就会出现我前面截图提到的问题。二、bodyEndStyle.css中如果存在p标签对应的样式，那p标签的样式会在bodyEndStyle.css解析完成后，改变一次样式，很影响体验。&lt;/p&gt;
 &lt;p&gt;如何解决这些问题那？其中也会用到一些JS篇中提到的点，如果没看过，建议先看看。&lt;/p&gt;
 &lt;h2&gt;优化核心依旧是减少下载时间&lt;/h2&gt;
 &lt;p&gt;JS篇中的预先解析DNS（dns-prefetch）依旧适用，提前解析CSS文件所在域名的DNS。&lt;/p&gt;
 &lt;h4&gt;Preload&lt;/h4&gt;
 &lt;p&gt;因为CSS已经在head中，我们不需要为css加preload属性了，但是css中用到的字体文件，一定要在所有css之前proload上。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;link rel=&amp;quot;preload&amp;quot; href=&amp;quot;/webfont.woff2&amp;quot; as=&amp;quot;font&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;首页CSS内联，非必要CSS异步加载&lt;/h4&gt;
 &lt;p&gt;首页用到的CSS内联写在  &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;中，其余CSS均采用异步加载，可以采用这种自己实现的加载CSS的方法，在合适的需要时加载需要的css&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;function LoadStyle(url) {
  try {
    document.createStyleSheet(url)
  } catch(e) {
    var cssLink = document.createElement(&amp;apos;link&amp;apos;);
    cssLink.rel = &amp;apos;stylesheet&amp;apos;;
    cssLink.type = &amp;apos;text/css&amp;apos;;
    cssLink.href = url;
    var head = document.getElementsByTagName(&amp;apos;head&amp;apos;)[0];
    head.appendChild(cssLink)
  }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果你使用webpack，那就更轻松了，使用import函数，大致如下&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 在a.js模块中直接引入css
import &amp;apos;style.css&amp;apos;&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;// 在需要a.js模块的地方
improt(&amp;apos;path-of-a.js&amp;apos;).then(module =&amp;gt; {})&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;webpack打包后，其实是把style.css打包进了a.js，在异步加载a.js的时候，会将style.css中的代码插入  &lt;code&gt;haed&lt;/code&gt;标签中。&lt;/p&gt;
 &lt;h2&gt;终极完美结构&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
  &amp;lt;title&amp;gt;Faster&amp;lt;/title&amp;gt;
  &amp;lt;link rel=&amp;quot;dns-prefetch&amp;quot; href=&amp;quot;//cdn.cn/&amp;quot;&amp;gt;

  &amp;lt;link rel=&amp;quot;preload&amp;quot; href=&amp;quot;//cdn.cn/webfont.woff2&amp;quot; as=&amp;quot;font&amp;quot;&amp;gt;
  &amp;lt;link rel=&amp;quot;preload&amp;quot; href=&amp;quot;//cdn.cn/Page1-A.js&amp;quot; as=&amp;quot;script&amp;quot;&amp;gt;
  &amp;lt;link rel=&amp;quot;preload&amp;quot; href=&amp;quot;//cdn.cn/Page1-B.js&amp;quot; as=&amp;quot;script&amp;quot;&amp;gt;
  
  &amp;lt;link rel=&amp;quot;prefetch&amp;quot; href=&amp;quot;//cdn.cn/Page2.js&amp;quot;&amp;gt;
  &amp;lt;link rel=&amp;quot;prefetch&amp;quot; href=&amp;quot;//cdn.cn/Page3.js&amp;quot;&amp;gt;
  &amp;lt;link rel=&amp;quot;prefetch&amp;quot; href=&amp;quot;//cdn.cn/Page4.js&amp;quot;&amp;gt;

  &amp;lt;style type=&amp;quot;text/css&amp;quot;&amp;gt;
    /* 首页用到的CSS内联 */
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;script type=&amp;quot;text/javascript&amp;quot; src=&amp;quot;//cdn.cn/Page1-A.js&amp;quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&amp;quot;text/javascript&amp;quot; src=&amp;quot;//cdn.cn/Page1-B.js&amp;quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在  &lt;a href="https://segmentfault.com/blogs#"&gt;JS篇&lt;/a&gt;)中，我已经解释过这套结构中JS的执行顺序了，本篇只是加入了CSS和字体。至此，我心中终极完美的页面HTML结构就是这样了。&lt;/p&gt;
 &lt;p&gt;如果你对异步加载CSS的方案感兴趣，欢迎留言与我讨论！&lt;/p&gt;
 &lt;h2&gt;扩展阅读&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#Webkit_CSS_parser"&gt;浏览器的工作原理&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.jianshu.com/p/24ffa6d45087"&gt;关于Preload, 你应该知道些什么？&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://juejin.im/post/58e8acf10ce46300585a7a42"&gt;Preload，Prefetch 和它们在 Chrome 之中的优先级&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>性能优化 javascript css html</category>
      <guid isPermaLink="true">https://itindex.net/detail/57837-%E7%8E%B0%E4%BB%A3-%E6%B5%8F%E8%A7%88%E5%99%A8-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96</guid>
      <pubDate>Fri, 29 Dec 2017 19:00:56 CST</pubDate>
    </item>
    <item>
      <title>前后端完全分离之API设计</title>
      <link>https://itindex.net/detail/57406-%E5%90%8E%E7%AB%AF-%E5%AE%8C%E5%85%A8-%E5%88%86%E7%A6%BB</link>
      <description>&lt;h2&gt;背景&lt;/h2&gt; &lt;p&gt;API就是开发者使用的界面。我的目标不仅是能用，而且好用, 跨平台(PC, Android, IOS, etc…)使用; 本文将详细介绍API的设计及异常处理, 并将异常信息进行封装友好地反馈给前端.&lt;/p&gt;
 &lt;p&gt;上篇文章  &lt;a href="http://www.arccode.net/2015/04/08/the-complete-separation-of-front-and-backend.html" rel="external" target="_blank"&gt;前后端完全分离初探&lt;/a&gt;只是讲了些宽泛的概念, 接下来的文章将直接上干货, 干货的源码会挂在  &lt;code&gt;github&lt;/code&gt;上.&lt;/p&gt;
 &lt;p&gt;前后端完全分离后, 前端和后端如何交互? &lt;/p&gt;
 &lt;p&gt;答: 通过双方协商好的API.&lt;/p&gt;
 &lt;p&gt;接下来我分享我自己设计的API接口, 欢迎各位朋友指教.&lt;/p&gt;
 &lt;h2&gt;API设计理念&lt;/h2&gt; &lt;ol&gt;
  &lt;li&gt;将涉及的实体抽象成资源, 即按   &lt;code&gt;id&lt;/code&gt;访问资源, 在   &lt;code&gt;url&lt;/code&gt;上做文章, 以后再也不用为   &lt;code&gt;url&lt;/code&gt;起名字而苦恼了.&lt;/li&gt;
  &lt;li&gt;使用   &lt;code&gt;HTTP&lt;/code&gt;动词对资源进行   &lt;code&gt;CRUD&lt;/code&gt;(增删改查); get-&amp;gt;查, post-&amp;gt;增, put-&amp;gt;改, delete-&amp;gt;删.&lt;/li&gt;
  &lt;li&gt;URL命名规则, 对于资源无法使用一个单数名词表示的情况, 我使用中横线(   &lt;code&gt;-&lt;/code&gt;)连接.   &lt;ul&gt;
    &lt;li&gt;资源采用名词命名, e.g: 产品 -&amp;gt; product&lt;/li&gt;
    &lt;li&gt;新增资源, e.g: 新增产品, url -&amp;gt; /product ,  verb -&amp;gt; POST&lt;/li&gt;
    &lt;li&gt;修改资源, e.g: 修改产品, url -&amp;gt; /products/{id} ,  verb -&amp;gt; PUT&lt;/li&gt;
    &lt;li&gt;资源详情, e.g: 指定产品详情, url -&amp;gt; /products/{id} ,  verb -&amp;gt; GET&lt;/li&gt;
    &lt;li&gt;删除资源, e.g: 删除产品, url -&amp;gt; /products/{id} ,  verb -&amp;gt; DELETE&lt;/li&gt;
    &lt;li&gt;资源列表, e.g: 产品列表, url -&amp;gt; /products ,  verb -&amp;gt; GET&lt;/li&gt;
    &lt;li&gt;资源关联关系, e.g: 收藏产品, url -&amp;gt; /products/{id}/star ,  verb -&amp;gt; PUT&lt;/li&gt;
    &lt;li&gt;资源关联关系, e.g: 删除收藏产品, url -&amp;gt; /products/{id}/star ,  verb -&amp;gt; DELETE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;目前我API的设计只涉及这两点, 至于第三点  &lt;code&gt;HATEOAS&lt;/code&gt;(Hypermedia As The Engine Of Application State)那就由读者自己去选择了.&lt;/p&gt;
 &lt;a&gt;&lt;/a&gt;
 &lt;h2&gt;项目地址&lt;/h2&gt; &lt;p&gt;本文中只涉及了设计的理念, 具体的实现请下载源码  &lt;a href="https://github.com/arccode/rest-api" rel="external" target="_blank"&gt;https://github.com/arccode/rest-api&lt;/a&gt;, 项目内写了比较详细的注释.&lt;/p&gt;
 &lt;h2&gt;项目实战&lt;/h2&gt; &lt;p&gt;实战将从业务场景出发, 详细介绍如何使用HTTP verb对资源进行操作(  &lt;code&gt;状态转移&lt;/code&gt;), 使用JSON返回结果(  &lt;code&gt;资源表述&lt;/code&gt;), 并定义JSON的基础结构.&lt;/p&gt;
 &lt;h3&gt;JSON结构&lt;/h3&gt; &lt;p&gt;requestParams: &lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;responseBody: &lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;  &lt;code&gt;meta&lt;/code&gt;中封装操作成功或失败的消息,   &lt;code&gt;data&lt;/code&gt;中封装返回的具体数据.&lt;/p&gt;
 &lt;p&gt;当新建商品或更新产品时, 相关属性封装在JSON中, 通过POST或PUT发送,&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;  &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;当用户对商品进行操作后, 将得到响应结果,&lt;/p&gt;
 &lt;p&gt;GET, POST, PUT操作成功, 返回如下结果&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 201,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;创建成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {     &lt;br /&gt;    &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;    &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;    &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;DELETE操作成功, 返回如下结果&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 204,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;删除成功&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h3&gt;业务场景一&lt;/h3&gt; &lt;p&gt;电商网站的管理员对商品进行新增,编辑,删除,浏览的操作; 暂时不考虑认证授权, 只关注对商品的操作.&lt;/p&gt;
 &lt;p&gt;为了以后便于做分布式, 所有资源id(表主键)均采用uuid.&lt;/p&gt;
 &lt;h4&gt;新增商品&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/product&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;POST&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, requestParams: &lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;  &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;4, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 201,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;创建成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {     &lt;br /&gt;    &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;    &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;    &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;编辑商品&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products/{id}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;PUT&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, requestParams: &lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;name&amp;quot;: &amp;quot;iPhone 6&amp;quot;,     &lt;br /&gt;  &amp;quot;description&amp;quot;: &amp;quot;此次苹果发布会发布了iPhone 6与iPhone 6 Plus，搭载iOS 8，尺寸分别是4.7和5.5英寸。外观设计不再棱角分明，表层玻璃边有一个弧度向下延伸，与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄，续航能力更强。&amp;quot;     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;4, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 200,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;修改成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {     &lt;br /&gt;    &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;    &amp;quot;name&amp;quot;: &amp;quot;iPhone 6&amp;quot;,     &lt;br /&gt;    &amp;quot;description&amp;quot;: &amp;quot;此次苹果发布会发布了iPhone 6与iPhone 6 Plus，搭载iOS 8，尺寸分别是4.7和5.5英寸。外观设计不再棱角分明，表层玻璃边有一个弧度向下延伸，与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄，续航能力更强。&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;删除商品&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products/{id}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;DELETE&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 204,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;删除成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {}     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;获取商品详情&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products/{id}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;GET&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, responseBody&lt;/p&gt;
 &lt;p&gt;删除前&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 200,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;查询成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: {     &lt;br /&gt;    &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;    &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;    &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;删除后&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 404,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;指定产品不存在&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;获取商品列表(未分页)&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;GET&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;12     &lt;br /&gt;13     &lt;br /&gt;14     &lt;br /&gt;15     &lt;br /&gt;16     &lt;br /&gt;17     &lt;br /&gt;18     &lt;br /&gt;19     &lt;br /&gt;20     &lt;br /&gt;21     &lt;br /&gt;22     &lt;br /&gt;23     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 200,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;获取全部商品成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: [     &lt;br /&gt;    {     &lt;br /&gt;      &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;      &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;      &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;    },     &lt;br /&gt;    {     &lt;br /&gt;      &amp;quot;id&amp;quot;: &amp;quot;9db1992a-c342-4ff0-a2a4-aeb3dbfd93f6&amp;quot;,     &lt;br /&gt;      &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;      &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;    },     &lt;br /&gt;    {     &lt;br /&gt;      &amp;quot;id&amp;quot;: &amp;quot;4481619b-45c5-4729-9539-f93bb01f10d8&amp;quot;,     &lt;br /&gt;      &amp;quot;name&amp;quot;: &amp;quot;Apple Watch SPORT&amp;quot;,     &lt;br /&gt;      &amp;quot;description&amp;quot;: &amp;quot;Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属，强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带，共有 5 款缤纷色彩。&amp;quot;     &lt;br /&gt;    }     &lt;br /&gt;  ]     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h3&gt;业务场景二&lt;/h3&gt; &lt;p&gt;  &lt;code&gt;业务场景一&lt;/code&gt;中只涉及了单个资源的操作, 但实际场景中还有些关联操作; 如用户去电商网站浏览商品, 并收藏了一些商品, 之后又取消收藏了部分商品.&lt;/p&gt;
 &lt;p&gt;暂时不考虑用户认证授权, 以后加了  &lt;code&gt;token&lt;/code&gt;后, 用户信息可以从中获取.&lt;/p&gt;
 &lt;h4&gt;收藏商品&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products/{id}/star&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;PUT&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;8     &lt;br /&gt;9     &lt;br /&gt;10     &lt;br /&gt;11     &lt;br /&gt;12     &lt;br /&gt;13     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 200,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: [     &lt;br /&gt;    {     &lt;br /&gt;      &amp;quot;id&amp;quot;: &amp;quot;5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9&amp;quot;,     &lt;br /&gt;      &amp;quot;name&amp;quot;: &amp;quot;iPhone 6&amp;quot;,     &lt;br /&gt;      &amp;quot;description&amp;quot;: &amp;quot;此次苹果发布会发布了iPhone 6与iPhone 6 Plus，搭载iOS 8，尺寸分别是4.7和5.5英寸。外观设计不再棱角分明，表层玻璃边有一个弧度向下延伸，与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄，续航能力更强。&amp;quot;     &lt;br /&gt;    }     &lt;br /&gt;  ]     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;取消收藏商品&lt;/h4&gt; &lt;p&gt;1, url:   &lt;code&gt;/api/products/{id}/star&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;2, method:   &lt;code&gt;DELETE&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;3, responseBody&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;7     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 200,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;删除收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功&amp;quot;     &lt;br /&gt;  },     &lt;br /&gt;  &amp;quot;data&amp;quot;: []     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h2&gt;自定义异常和异常处理&lt;/h2&gt; &lt;p&gt;所有自定义异常继承RuntimeException, 在业务层抛出, 统一在Controller层进行处理.&lt;/p&gt;
 &lt;p&gt;异常分为全局异常和局部异常, 例如http method unsupported(405), unauthorized(401), accessDenied(403), not found(404)等属于全局异常; 针对对独立业务的一些异常属于局部异常, 例如产品编辑出错; &lt;/p&gt;
 &lt;p&gt;异常在Controller中进行处理, 并封装成json返回给前端, 封装后的数据如下, 相关实现见  &lt;a href="https://github.com/arccode/rest-api" rel="external" target="_blank"&gt;源码&lt;/a&gt;;&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 404,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;指定产品不存在&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;1     &lt;br /&gt;2     &lt;br /&gt;3     &lt;br /&gt;4     &lt;br /&gt;5     &lt;br /&gt;6     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;   &lt;td&gt;    &lt;pre&gt;{     &lt;br /&gt;  &amp;quot;meta&amp;quot;: {     &lt;br /&gt;    &amp;quot;code&amp;quot;: 405,     &lt;br /&gt;    &amp;quot;message&amp;quot;: &amp;quot;Request method &amp;apos;POST&amp;apos; not supported&amp;quot;     &lt;br /&gt;  }     &lt;br /&gt;}     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h2&gt;项目运行截图部分&lt;/h2&gt; &lt;p&gt;  &lt;img alt="" src="http://arccode.net/images/arc/arc_add.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://arccode.net/images/arc/arc_list.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://arccode.net/images/arc/arc_exception.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;本系列文章&lt;/h2&gt; &lt;ul&gt;
  &lt;li&gt;   &lt;a href="http://www.arccode.net/2015/04/08/the-complete-separation-of-front-and-backend.html" rel="external" target="_blank"&gt;前后端完全分离初探&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.arccode.net/2015/04/18/API-design-for-front-and-backend-completely-separate.html" rel="external" target="_blank"&gt;前后端完全分离之API设计&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;前后端完全分离之安全认证与授权-上&lt;/li&gt;
  &lt;li&gt;前后端完全分离之安全认证与授权-下&lt;/li&gt;
  &lt;li&gt;前后端完全分离之前端模块化开发&lt;/li&gt;
  &lt;li&gt;前后端完全分离之前端路由系统&lt;/li&gt;
  &lt;li&gt;前后端完全分离之后端面向服务的模块化开发&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Java Javascript Rest 架构</category>
      <guid isPermaLink="true">https://itindex.net/detail/57406-%E5%90%8E%E7%AB%AF-%E5%AE%8C%E5%85%A8-%E5%88%86%E7%A6%BB</guid>
      <pubDate>Sat, 18 Apr 2015 17:46:48 CST</pubDate>
    </item>
  </channel>
</rss>

