<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0">
  <channel>
    <title>IT瘾网页推荐</title>
    <link>https://itindex.net/categories/网页</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/categories/网页</link>
    </image>
    <item>
      <title>从输入URL到页面展示过程：深入解析网络请求与渲染</title>
      <link>https://itindex.net/detail/62838-url-%E9%A1%B5%E9%9D%A2-%E5%B1%95%E7%A4%BA</link>
      <description>&lt;h1&gt;推荐阅读&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://cloud.tencent.com/developer/article/2304343"&gt;项目实战:AI文本 OCR识别最佳实践&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://gamma.app/signup?r=sqgovd3iif5btr4"&gt;AI Gamma一键生成PPT工具直达链接&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://cloud.tencent.com/developer/article/2272077?areaSource=&amp;traceId="&gt;玩转cloud Studio 在线编码神器&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://cloud.tencent.com/developer/article/2291530?shareByChannel=link"&gt;玩转 GPU AI绘画、AI讲话、翻译,GPU点亮AI想象空间&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;资源分享&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://zkk-1300025204.cos.ap-nanjing.myqcloud.com/%E5%8F%B2%E4%B8%8A%E6%9C%80%E5%85%A8StableDiffusion%E8%B5%84%E6%96%99%E5%8C%85.csv"&gt;史上最全文档AI绘画stablediffusion资料分享&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://yv4kfv1n3j.feishu.cn/docx/MRyxdaqz8ow5RjxyL1ucrvOYnnH"&gt;AI绘画关于SD,MJ,GPT,SDXL百科全书&lt;/a&gt;&lt;/h1&gt;
 &lt;h1&gt;  &lt;a href="https://blog.csdn.net/weixin_42373241/article/details/132341577?spm=1001.2014.3001.5501"&gt;AI绘画 stable diffusion Midjourney 官方GPT文档 AIGC百科全书资料收集&lt;/a&gt;&lt;/h1&gt;
 &lt;pre&gt;  &lt;code&gt;「java、python面试题」来自UC网盘app分享，打开手机app，额外获得1T空间
https://drive.uc.cn/s/2aeb6c2dcedd4
AIGC资料包
https://drive.uc.cn/s/6077fc42116d4
https://pan.xunlei.com/s/VN_qC7kwpKFgKLto4KgP4Do_A1?pwd=7kbv#
&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;引言&lt;/h2&gt;
 &lt;p&gt;在当今互联网时代，我们每天都会通过浏览器访问各种网页。但是，你是否曾经思考过在我们输入一个URL后，浏览器是如何加载并显示页面的呢？这背后涉及到一系列复杂的技术和过程。本文将带领大家深入了解从输入URL到页面展示的过程，并给出相应的代码示例，让我们一起探索这个神奇而又复杂的世界。&lt;/p&gt;
 &lt;h2&gt;1. 网络请求的发起&lt;/h2&gt;
 &lt;p&gt;通过浏览器输入URL后，浏览器会根据协议类型（如HTTP或HTTPS）向服务器发起请求。这个过程可以通过下面的代码示例来体现：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const url = &amp;quot;https://example.com&amp;quot;;
fetch(url)
  .then(response =&amp;gt; response.text())
  .then(data =&amp;gt; {
    console.log(data);
  })
  .catch(error =&amp;gt; {
    console.error(error);
  });
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在代码中，我们使用了JavaScript的fetch API来发起网络请求，并将服务器响应转换为文本输出到控制台。&lt;/p&gt;
 &lt;h2&gt;2. DNS解析&lt;/h2&gt;
 &lt;p&gt;在发送网络请求前，浏览器首先需要将URL中的域名解析成对应的IP地址。这个过程称为DNS解析。下面是一个简化版的DNS解析示例代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const dns = require(&amp;apos;dns&amp;apos;);

const domain = &amp;quot;example.com&amp;quot;;
dns.resolve(domain, &amp;apos;A&amp;apos;, (err, addresses) =&amp;gt; {
  if (err) {
    console.error(err);
    return;
  }
  console.log(addresses);
});
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;上述代码中，我们使用Node.js的  &lt;code&gt;dns&lt;/code&gt;模块来进行DNS解析，并输出解析得到的IP地址。&lt;/p&gt;
 &lt;h2&gt;3. 建立TCP连接&lt;/h2&gt;
 &lt;p&gt;经过DNS解析后，浏览器会尝试与服务器建立TCP连接。这个过程涉及到三次握手，确保数据能够安全可靠地传输。以下是一个简化的TCP连接代码示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const net = require(&amp;apos;net&amp;apos;);

const serverIP = &amp;apos;192.168.0.1&amp;apos;;
const port = 80;

const client = new net.Socket();
client.connect(port, serverIP, () =&amp;gt; {
  console.log(&amp;apos;TCP connection established&amp;apos;);
});
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在上述代码中，我们使用Node.js的  &lt;code&gt;net&lt;/code&gt;模块创建了一个TCP socket，并通过  &lt;code&gt;connect&lt;/code&gt;方法与服务器建立连接。&lt;/p&gt;
 &lt;h2&gt;4. 发送HTTP请求&lt;/h2&gt;
 &lt;p&gt;TCP连接建立后，浏览器会构建HTTP请求并发送给服务器。以下是一个简化的HTTP请求发送代码示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

const options = {
  hostname: &amp;apos;example.com&amp;apos;,
  port: 80,
  path: &amp;apos;/&amp;apos;,
  method: &amp;apos;GET&amp;apos;
};

const req = http.request(options, res =&amp;gt; {
  console.log(`HTTP response status code: ${res.statusCode}`);
});

req.end();
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在上述代码中，我们使用Node.js的  &lt;code&gt;http&lt;/code&gt;模块创建了一个HTTP请求，并通过  &lt;code&gt;request&lt;/code&gt;方法发送给服务器。&lt;/p&gt;
 &lt;h2&gt;5. 服务器处理请求&lt;/h2&gt;
 &lt;p&gt;服务器收到浏览器发送的HTTP请求后，会根据请求的内容进行相应的处理。这个过程通常包括路由解析、数据查询等操作。下面是一个简化的服务器处理请求的代码示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const http = require(&amp;apos;http&amp;apos;);

const server = http.createServer((req, res) =&amp;gt; {

  if (req.url === &amp;apos;/&amp;apos;) {

res.writeHead(200, { &amp;apos;Content-Type&amp;apos;: &amp;apos;text/plain&amp;apos; });

res.end(&amp;apos;Hello, World!&amp;apos;);

  } else if (req.url === &amp;apos;/about&amp;apos;) {

res.writeHead
(200, { &amp;apos;Content-Type&amp;apos;: &amp;apos;text/html&amp;apos; });
    res.end(&amp;apos;&amp;lt;h1&amp;gt;About Page&amp;lt;/h1&amp;gt;&amp;apos;);
  } else {
    res.writeHead(404, { &amp;apos;Content-Type&amp;apos;: &amp;apos;text/plain&amp;apos; });
    res.end(&amp;apos;Page not found&amp;apos;);
  }
});

server.listen(80, () =&amp;gt; {
  console.log(&amp;apos;Server running at http://localhost:80/&amp;apos;);
});
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在上述代码中，我们使用Node.js的  &lt;code&gt;http&lt;/code&gt;模块创建了一个简单的HTTP服务器。根据请求的URL路径，服务器会返回不同的响应内容。&lt;/p&gt;
 &lt;h2&gt;6. 接收响应数据&lt;/h2&gt;
 &lt;p&gt;当服务器处理完请求并生成响应后，浏览器会接收到响应数据。这个过程在浏览器内部进行，我们无法直接访问其代码。浏览器会将响应数据存储在缓存中，并准备进行后续的解析和渲染。&lt;/p&gt;
 &lt;h2&gt;7. 解析HTML&lt;/h2&gt;
 &lt;p&gt;浏览器接收到响应数据后，会对HTML进行解析，构建出一棵DOM树。这个过程包括识别HTML标签、属性、文本等，并将其转换为可以操作的数据结构。以下是一个简化的HTML解析代码示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const parser = new DOMParser();
const htmlString = &amp;apos;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Hello, World!&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;apos;;
const doc = parser.parseFromString(htmlString, &amp;apos;text/html&amp;apos;);

console.log(doc.title); // Output: &amp;quot;Hello, World!&amp;quot;
console.log(doc.body.innerHTML); // Output: &amp;quot;&amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在上述代码中，我们使用JavaScript的DOMParser来解析HTML字符串，并通过操作解析后的DOM树来获取需要的信息。&lt;/p&gt;
 &lt;h2&gt;8. 构建DOM树&lt;/h2&gt;
 &lt;p&gt;浏览器在解析HTML后，会根据标签之间的层次关系构建一棵DOM树。每个HTML元素都会被转换为DOM节点，并按照其在HTML中的嵌套关系形成父子节点的层次结构。以下是一个简化的DOM树构建示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;const htmlString = &amp;apos;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Hello, World!&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;apos;;
const doc = new DOMParser().parseFromString(htmlString, &amp;apos;text/html&amp;apos;);

console.log(doc.documentElement); // Output: HTML元素节点
console.log(doc.documentElement.childNodes.length); // Output: 2，包含&amp;lt;head&amp;gt;和&amp;lt;body&amp;gt;
console.log(doc.documentElement.childNodes[1].childNodes[0]); // Output: &amp;lt;h1&amp;gt;Welcome&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在上述代码中，我们使用DOMParser来解析HTML字符串，并通过访问  &lt;code&gt;documentElement&lt;/code&gt;和  &lt;code&gt;childNodes&lt;/code&gt;属性来获取DOM树的节点信息。&lt;/p&gt;
 &lt;h2&gt;9. 渲染页面&lt;/h2&gt;
 &lt;p&gt;经过DOM树的构建后，浏览器会根据DOM树的结构和样式信息对页面进行渲染。这个过程包括布局计算、绘制元素、加载外部资源等操作，最终将页面显示给用户。由于浏览器的渲染过程非常复杂，我们无法直接操作其渲染引擎。但是，我们可以通过调试工具来观察页面的渲染情况。&lt;/p&gt;
 &lt;h2&gt;10. 用户交互与动态效果&lt;/h2&gt;
 &lt;p&gt;在页面渲染完成后，用户可以与页面进行交互，并享受丰富的动态效果。这包括点击链接、提交表单、触发事件等操作。JavaScript在此起到了重要的作用，它可以监听用户的操作并相应地更新页面内容或执行相应的逻辑。&lt;/p&gt;
 &lt;h2&gt;11. 性能优化&lt;/h2&gt;
 &lt;p&gt;为了提供更好的用户体验，我们需要关注性能优化。这包括减少网络请求次数、压缩资源文件、使用缓存等策略。同时，优化JavaScript和CSS的编写方式也可以提升页面的加载速度和响应性能。&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 />
      <guid isPermaLink="true">https://itindex.net/detail/62838-url-%E9%A1%B5%E9%9D%A2-%E5%B1%95%E7%A4%BA</guid>
      <pubDate>Thu, 31 Aug 2023 09:33:35 CST</pubDate>
    </item>
    <item>
      <title>使用 OpenAi Api 在本地搭建一个 ChatGPT 网页版</title>
      <link>https://itindex.net/detail/62680-openai-api-%E5%BB%BA%E4%B8%80</link>
      <description>&lt;div&gt;  &lt;p&gt;   &lt;img alt="" src="https://img.learn2.cn/img/202303151246721.jpg"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;h2&gt;前言&lt;/h2&gt;
  &lt;p&gt;之前 ChatGPT 在网络上大火，由于很多开发者使劲薅羊毛，OpenAi 也撑不住了，对于新注册账号只有5美刀的免费额度了，网页版也各种限制。OpenAi 使用了 CloudFlare 的防火墙，而我使用的 IP 是美西的机房，没有任何意外地被屏蔽了。&lt;/p&gt;  &lt;p&gt;   &lt;img alt="" src="https://img.learn2.cn/img/202303141734571.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;为了方便我继续使用，我在移动端设备使用 baye 大佬开发的    &lt;a href="https://apps.apple.com/us/app/opencat/id6445999201" target="_blank"&gt;OpenCat&lt;/a&gt; 完美解决网页版无法使用的问题。&lt;/p&gt;  &lt;p&gt;桌面端上呢，我找了半天，最终找到这个项目：   &lt;a href="https://github.com/slippersheepig/chatgpt-web" target="_blank"&gt;chatgpt-web&lt;/a&gt;，简单部署安装了一下，完全符合我现阶段的使用需求，绕开 CloudFlare 的防火墙限制，使用OpenAi Api 直接获取问题答案。&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;本文将会教你如何通过api在本地部署一个独属于你的 ChatGPT。&lt;/p&gt;  &lt;br /&gt;  &lt;h2&gt;部署流程&lt;/h2&gt;
  &lt;p&gt;首先要确认的一点是你的局域网环境是否可以访问 OpenAi 的网站，如果无法访问的话，请先扶墙再回来看这篇文章进行下一步的部署操作。&lt;/p&gt;  &lt;br /&gt;  &lt;h3&gt;获取API key&lt;/h3&gt;
  &lt;p&gt;可以参考这篇文章《   &lt;a href="https://tstrs.me/result/Njqd5YQBU87SstoFYZVT" target="_blank"&gt;OpenAI 的 ChatGPT 超简易注册攻略！&lt;/a&gt;》注册，登录后在下面这个链接内即可创建的你api key：&lt;/p&gt;  &lt;p&gt;   &lt;a href="https://platform.openai.com/account/api-keys" target="_blank"&gt;https://platform.openai.com/account/api-keys&lt;/a&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;页面应该如下图所示，创建好了复制保存下来。因为它只会显示一次。&lt;/p&gt;  &lt;p&gt;   &lt;img alt="" src="https://img.learn2.cn/img/202303141743106.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;h3&gt;docker部署&lt;/h3&gt;
  &lt;p&gt;进入 Linux 系统的命令行界面，首先要确保 docker 可用，如果没有安装过可以使用以下代码安装。在不同的系统中安装方法可能不一样，以下为 Ubuntu 下的安装方法。&lt;/p&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;sudo apt-get install docker-compose     &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;br /&gt;  &lt;p&gt;接下来需要创建    &lt;code&gt;.env&lt;/code&gt; ，将你的 key 填写到引号内，然后保存。&lt;/p&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;OPENAI_API_KEY=&amp;quot;前面你获取到的 OpenAI API KEY&amp;quot;     &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;br /&gt;  &lt;p&gt;再创建    &lt;code&gt;docker-compose.yml&lt;/code&gt; 配置文件，保存即可。&lt;/p&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;version: &amp;quot;3.3&amp;quot;     &lt;br /&gt;services:     &lt;br /&gt;  chatgpt:     &lt;br /&gt;    image: sheepgreen/chatgpt-web #如果是arm架构，需要改成chatgpt-web:arm     &lt;br /&gt;    container_name: webchat     &lt;br /&gt;    volumes:     &lt;br /&gt;- ./.env:/chatgpt-web/.env     &lt;br /&gt;    ports:     &lt;br /&gt;- &amp;quot;8888:8088&amp;quot; #8088为容器内部端口，不可更改；8888为外部映射端口，可自行更改     &lt;br /&gt;    restart: always     &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;br /&gt;  &lt;p&gt;最后输入命令    &lt;code&gt;docker-compose up -d&lt;/code&gt; 即可，等待命令行跑完即可启动成功。&lt;/p&gt;  &lt;br /&gt;  &lt;h3&gt;测试&lt;/h3&gt;
  &lt;p&gt;打开   &lt;a href="http://ip:port/chat" target="_blank"&gt;http://ip:port/chat&lt;/a&gt; 即可访问我们刚刚创建的ChatGpt-web了。&lt;/p&gt;  &lt;p&gt;   &lt;img alt="" src="https://img.learn2.cn/img/202303141756801.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;我们可以测试一下它：&lt;/p&gt;  &lt;p&gt;   &lt;img alt="" src="https://img.learn2.cn/img/202303151235431.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;br /&gt;  &lt;h2&gt;后记&lt;/h2&gt;
  &lt;p&gt;在使用 OpenAi Api 搭建 ChatGPT 网页版的过程中，我深深感受到了技术对于我们生活的影响和改变。ChatGPT 不仅仅是一款智能对话系统，更是一种开发人工智能技术的思路和方法。&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;这个项目也让我深入地了解到了人工智能技术的存在和发展。从机器学习、自然语言处理到深度学习、神经网络等多个领域，人工智能技术正在不断地革新和进步。通过这次实践，我更加深信，未来的世界一定会离不开人工智能技术的支持和应用。&lt;/p&gt;  &lt;br /&gt;  &lt;p&gt;最后，我觉得，在不远的将来，低端程序员就会像现在建筑行业的搬运工一样会越来越少，只不过一个是被机械装置取代，一个是被人工智能取代。而会驱使人工智能，就像是会开挖掘机一样，至少不会被时代的浪潮给拍死在岸上。  &lt;/p&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 />
      <guid isPermaLink="true">https://itindex.net/detail/62680-openai-api-%E5%BB%BA%E4%B8%80</guid>
      <pubDate>Wed, 15 Mar 2023 20:41:00 CST</pubDate>
    </item>
    <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>网页音视频通话大火，WebRTC 正式成为 W3C 和 IETF 标准</title>
      <link>https://itindex.net/detail/61216-%E7%BD%91%E9%A1%B5-%E8%A7%86%E9%A2%91-%E5%A4%A7%E7%81%AB</link>
      <description>&lt;p&gt;IT之家2月14日消息 外媒 Neowin 报道，万维网联盟 (W3C)和互联网工程任务组 (IETF)宣布，网络实时通信 (WebRTC)现在是一个正式的网络标准，可以在网络上任何地方进行音频和视频通信。虽然 WebRTC 才刚刚成为一个标准，但它已经被流行的网络浏览器支持多年。&lt;/p&gt; &lt;p&gt;  &lt;img src="https://img.ithome.com/newsuploadfiles/2021/2/20210214_232828_935.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;IT之家获悉，在技术层面上，WebRTC 是一个框架，允许开发人员将点对点的音频和视频聊天添加到他们的项目中。如果你曾经使用过 Facebook Messenger、Discord 或其他众多服务来进行视频或音频通话，那么你已经使用过 WebRTC。&lt;/p&gt; &lt;p&gt;IETF 主席 Alissa Cooper 表示，&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;“IP 语音和视频彻底改变了人们在世界各地的通信方式。将这些技术整合到网络平台中，极大地扩大了它们的影响力。得益于 IETF 和 W3C 之间的密切合作，使这些技术标准化。WebRTC 使数十亿人在 COVID-19 疫情期间能够相互联系和接触，无论设备或地理环境如何。”&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;通过对 WebRTC 技术进行标准化，任何想要在未来实现该功能的软件项目都将有一套明确的指导方针，他们可以遵循这些指导方针，以确保软件的 WebRTC 实现可以正确完成，并满足所有要求。&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 />
      <guid isPermaLink="true">https://itindex.net/detail/61216-%E7%BD%91%E9%A1%B5-%E8%A7%86%E9%A2%91-%E5%A4%A7%E7%81%AB</guid>
      <pubDate>Sun, 14 Feb 2021 23:29:44 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>写了个磁力搜索的网页 － 收录最近热门分享的资源</title>
      <link>https://itindex.net/detail/59599-%E7%A3%81%E5%8A%9B-%E6%90%9C%E7%B4%A2-%E7%BD%91%E9%A1%B5</link>
      <description>&lt;p&gt;好吧，又很疯狂地做了一个东西：  &lt;a href="http://bt.shousibaocai.com/" target="_blank"&gt;http://bt.shousibaocai.com/&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://xiaoxia.org/upfiles/2013/05/btsearch.png"&gt;   &lt;img alt="btsearch" height="491" src="http://xiaoxia.org/upfiles/2013/05/btsearch-700x491.png" width="700"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;平常我们高端用户都会用到BT工具来分享一些好玩的资源，例如ubuntu 13.04的ISO安装盘，一些好听的音乐等。这个时候我们会进入一个叫做P2P的网络，大家都在这个网络里互相传递数据，这种分布式的数据传输解决了HTTP、FTP等单一服务器的带宽压力。以往的BT工具（包括现在也有）在加入这个P2P网络的时候都需要借助一个叫Tracker的中心服务器，这个服务器是用来登记有哪些用户在请求哪些资源，然后让请求同一个资源的用户都集中在一起互相分享数据，形成的一个集群叫做Swarm。&lt;/p&gt;
 &lt;p&gt;这种工作方式有一个弊端就是一旦Tracker服务器出现故障或者线路遭到屏蔽，BT工具就无法正常工作了。所以聪明的人类后来发明了一种叫做DHT（Distributed Hash Table）的去中心化网络。每个加入这个DHT网络的人都要负责存储这个网络里的资源信息和其他成员的联系信息，相当于所有人一起构成了一个庞大的分布式存储数据库。在DHT里定位一个用户和定位一个资源的方法是一样的，他们都使用SHA－1产生的哈希值来作标识。&lt;/p&gt;
 &lt;p&gt;具体如何工作的呢？举个形象点的例子，把DHT网络比作一个朋友圈子，你想进入这个圈子必须要有一个人带领你进去，通常会有一些特定人负责介绍你进入这个圈子。当你被A带进这个朋友圈，此刻你就只认识A而已。但是你的目的是想找奥巴马总统，所以你会问A要奥巴马的联系方式，但是A没有奥巴马的联系方式，他会介绍一个美国朋友B给你认识。于是你去问B要奥巴马的联系方式，B其实也没有奥巴马的联系方式，但是B认识一个州长C。于是你又得到了C的联系方式，C把奥巴马的联系方式告诉你之后，你就可以写信或者致电给奥巴马了。&lt;/p&gt;
 &lt;p&gt;这种DHT网络听起来很不错，没有中心服务器，也不怕被DDOS，看看海盗湾如今还能挺立那么久就知道了。但是有没有安全隐患在里面呢？答案是肯定有的。有些不听话的用户可能会在DHT网络里捣乱，譬如说撒谎，明明自己不是奥巴马，却偏说自己是奥巴马，这样会误导其他人无法正常获取想要的资源。另外，用户在DHT网络里的隐私可能会被窃听，因为在DHT网络里跟其他用户交换资源的时候，难免会暴露自己的IP地址，所以别人就会知道你有什么资源，你在请求什么资源了。这也是目前DHT网络里一直存在的一个弱点。&lt;/p&gt;
 &lt;p&gt;正是利用这个弱点，我根据DHT协议用Python写了一段白菜程序，加入了这个DHT网络。在这个网络里，我会认识很多人，越多越好，并且观察这些人的举动，比如说A想要ubuntu的安装盘，那么我会把A的这个行为记下来，同时我会把ubuntu安装盘这个资源的信息也记下来，保存到数据库中，统计请求ubuntu这个资源的人有多少。如今，这个爬虫已经运行了两个昼夜，以每分钟记录3000多个资源信息的速度工作（单机器单线程，耗尽了CPU的一个核心）。到目前为止（48小时），共发出4亿条交友请求，收到1100万条来自朋友的资源请求。到底有多少资源目前另外一个程序还在分析中，从已分析的300万请求中，独立资源个数为20万个。保守估计这1100万条请求中涉及资源个数为50万以上。迟点有空的话，对这些资源进行分类和分析。&lt;/p&gt;
 &lt;p&gt;在DHT网络里发现的资源基本上都是最新的活跃资源，一些几年前的资源现在没有用户请求的话就不会被挖掘出来。想想海盗湾沉淀了那么久才500万资源，我跑了两天的程序就已经探测到50万资源，所以随着时间推移，会有更多的资源被探测出来，而且那些最新的能够被接近实时的速度发现到。本来通过增加一个节点进程可以加快探测速度，但是我没有这样做的原因是分析请求的哈希值的速度赶不上发现新请求的速度。有兴趣的朋友可以自己琢磨一下这方面的东西，国内研究DHT网络的人应该比较少。前段时间我打探过几家现在做P2P视频播放的公司里，没有人做这方面的研究。&lt;/p&gt;
 &lt;p&gt; &lt;/p&gt;
 &lt;p&gt;[DHT Protocol]   &lt;a href="http://www.bittorrent.org/beps/bep_0005.html" target="_blank"&gt;http://www.bittorrent.org/beps/bep_0005.html&lt;/a&gt;&lt;/p&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>Internet 我的分享</category>
      <guid isPermaLink="true">https://itindex.net/detail/59599-%E7%A3%81%E5%8A%9B-%E6%90%9C%E7%B4%A2-%E7%BD%91%E9%A1%B5</guid>
      <pubDate>Sat, 11 May 2013 05:16:21 CST</pubDate>
    </item>
    <item>
      <title>不需要昂贵的 Photoshop，这 7 款免费网页工具想帮你「轻量化」处理图片</title>
      <link>https://itindex.net/detail/59413-%E9%9C%80%E8%A6%81-photoshop-%E5%85%8D%E8%B4%B9</link>
      <description>&lt;div&gt;此前，少数派曾为你介绍过 8 款在线图片处理工具，让你仅需打开浏览器前往特定的网页就能进行图片处理，而无需安装动辄几百 MB 的复杂软件，以及付出相对的时间和上手成本，实现即开即用、用完即走。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;本文将会继续为你推荐另外 7 款实用并且免费的在线图片处理工具，让你在图片处理的「轻量化」方面更进一步。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;  &lt;strong&gt;关联阅读：&lt;/strong&gt;  &lt;a href="https://sspai.com/post/53091" target="_blank"&gt;《不装 App，用这 8 款免费的网页工具解决日常图片处理需求》&lt;/a&gt;&lt;/div&gt; &lt;h2&gt;黑白图片着色：ColouriseSG&lt;/h2&gt; &lt;p&gt;   &lt;a href="https://colourise.sg/" target="_blank"&gt;ColouriseSG&lt;/a&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;ColouriseSG 是近期新晋的热门在线图片处理工具。它是一款运用 AI 技术、  &lt;strong&gt;专门为黑白老照片重新着色的免费工具&lt;/strong&gt;，该项目由新加坡科技局（GovTech Singapore）的数据科学和人工智能部门所提供。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/0e7bab1759ebd92de80e9ed2962d891a.png"&gt;&lt;/img&gt;ColouriseSG &lt;p&gt;据官方介绍，相比于以往的着色工具，ColouriseSG 通过机器学习并储存了超过 50 万张在当地所拍摄的图像，以实现在数秒的时间内，准确识别出黑白照片的人像和自然风景，生成一张颜色更为真实的彩色照片。&lt;/p&gt; &lt;p&gt;在成功导入照片后，ColouriseSG 便会在数秒内自动着色，并在图片上提供一条对比的拖拽线，让你可在左右方向随意对比着色前后的差距：&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/b3bf84f2fdd7f3964f26b8c9962670b0.gif"&gt;&lt;/img&gt;对比着色效果 &lt;p&gt;在图片下方，ColouriseSG 还提供了「Download result」 与「Download result comparison」两种不同的下载方式，前者可让用户直接下载着色后的彩色图片，后者则是下载黑白与彩色的对比图片。&lt;/p&gt; &lt;p&gt;我们以来源于网络的 80 年代中国照片为例，着色前后对比图图如下：&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/dc20ed7e5ed333a844824f0464cb0580.jpg" width="600"&gt;&lt;/img&gt;左：着色前/ 右：着色后 &lt;p&gt;从上方图片的效果看，ColouriseSG 确实有着不错的着色效果。不过值得强调的时，官方声明其着色的图片并不能够准确还原黑白照片实际的颜色，仅是提供更为合理的着色而已。&lt;/p&gt; &lt;h2&gt;在线、免费的 PS：Photopea&lt;/h2&gt; &lt;div&gt;  &lt;a href="https://www.photopea.com" target="_blank"&gt;Photopea&lt;/a&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;PhotoShop 功能的强大，使它在任一领域都发挥着不可替代的重要作用，但对于部分人而言，常常对它有着这样的抱怨：&lt;/div&gt; &lt;p&gt;&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;不便宜的售价，使用频率低；&lt;/li&gt;&lt;/ul&gt; &lt;ul&gt;  &lt;li&gt;软件体积较大，安装较繁琐。&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;若你也有这样的抱怨，那么不妨试一试免费、免安装的在线 PhotoShop 网站：Photopea。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/1d666b5894190f112cfbc2fd7ac582b7.png"&gt;&lt;/img&gt;Photopea &lt;p&gt;进入 Photopea 网站后，我们可以发现，它除了默认深灰色调的 UI 界面与 PS 保持一致外，各功能图标和布局的设计、快捷功能键设定也十分相似，让熟悉 PS 的你也能快速上手。对于常用的 PS 功能，Photopea 自然也均有提供。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/7229db291419aa84e02248a86a86ca25.png"&gt;&lt;/img&gt;相似的功能布局 &lt;p&gt;此外，Photopea 还支持直接打开 Sketch 文件，并且在新建文件尺寸中，还贴心提供了更多的默认尺寸，比如说 Facebook、Instagram、Twitter、YouTube 等网站适宜尺寸。&lt;/p&gt; &lt;p&gt;值得注意的是，Photopea 免费版本会在页面右方显示广告，如果需要关闭广告，则需要付费进行解锁，并获得更多的历史记录保存次数（60 次，免费版为 30 次）。&lt;/p&gt; &lt;h2&gt;蒙太奇效果：EasyMoza&lt;/h2&gt; &lt;div&gt;EasyMoza  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;p&gt;蒙太奇效果即是指一张照片是由很多张小照片拼组而成，使得近看、远看能够呈现不同的视觉效果，极具创意而且富有艺术感。比如说：&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/01/65b501824553c16c6444c17576425433.jpg" width="600"&gt;&lt;/img&gt;乔布斯与苹果产品，图/ 网络 &lt;p&gt;相比于普通的图片，蒙太奇效果也更能体现照片与照片间的联系。对于咱们这些普通人而言，用在情侣照、个人及宠物成长照上再合适不过了。&lt;/p&gt; &lt;p&gt;而 EasyMoza 便是一款能帮助你制作蒙太奇效果的在线工具。我们仅需选择导入的图片后便可自动生成，相比于传统费时费力的 PS 手工拼凑要方便不少。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/ea50646781c79eca76d5bb905b54420e.gif" width="600"&gt;&lt;/img&gt;猫和少数派～ &lt;p&gt;EasyMoza 还提供了小图的图片样张，供你参考或者制作蒙太奇效果，你可以在网站的右上方切换语言为中文以更方便地使用。不过，easymoza 仅免费提供 960 x 720px 低分辨率效果，如需下载更高的分辨率则需付费下载。&lt;/p&gt; &lt;h2&gt;动图压缩：EZGif&lt;/h2&gt; &lt;div&gt;  &lt;a href="https://ezgif.com/" target="_blank"&gt;EZGif&lt;/a&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;p&gt;很多网站都对 GIF 图片的体积有着限制，但若是「不讲究」的缩小体积，图片的画质便会得到下降,所以如何有效控制和减小 GIF 体积就成了一门必修功课。&lt;/p&gt; &lt;p&gt;而如果你想在不借助软件的情况下快速缩小 GIF 体积，那么便不可错过全能型的 GIF 编辑网站：EZGif。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/5a83a9f4934c63945c59310ca241cce9.png"&gt;&lt;/img&gt; &lt;p&gt;EZGif&lt;/p&gt; &lt;p&gt;在压缩 GIF 方面，Ezgif 支持上传最大 35MB 的 GIF 图片或者直接复制粘贴 URL。图片载入后，便可在上方工具栏中选择对应的工具，对影响 GIF 体积的尺寸、颜色、帧率和时长进行调整。&lt;/p&gt; &lt;p&gt;而且调整后的效果会在页面下方预览，并显示文件的具体信息，包括大小、长宽、帧数。倘若你对结果仍然不满意，也可在预览界面中继续自由调整。除了压缩 GIF 外，Ezgif 还支持视频转 GIF、WebP、APNG 文件格式转换等等，功能非常全面。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/5aca518e7211ddfa3a54de8031ad7391.png"&gt;&lt;/img&gt;GIF 调整工具（网页经过浏览器翻译） &lt;h2&gt;在线版 Prisma：Ostagram&lt;/h2&gt; &lt;div&gt;  &lt;a href="https://www.ostagram.me" target="_blank"&gt;Ostagram&lt;/a&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;p&gt;很多朋友想必还记得曾经爆红网络、利用算法将普通照片转成艺术风格作品的   &lt;a href="https://sspai.com/app/Prisma" target="_blank"&gt;Prisma&lt;/a&gt;。&lt;/p&gt; &lt;p&gt;而 Ostagram 便是一款与 Prisma 功能类似的在线工具。不同的是，Ostagram 是一个基于 DeepDream 算法来生成绘画作品的项目，即学习绘画作品的风格，从而在不破坏原图构图的情况下，将其变成类似绘画作品风格的画作。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/f7f343d93173b720b7aff2307154f8f6.png"&gt;&lt;/img&gt; &lt;p&gt;Ostagram&lt;/p&gt; &lt;p&gt;Ostagram 使用起来也并不复杂，登录账号后便可上传图片，选择模拟的绘画作品后，即可将原图渲染输出为风格相似的画作，效果令人满意。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/d7f872b3588b06534a3e16731d91702a.png" width="600"&gt;&lt;/img&gt;渲染效果 &lt;p&gt;美中不足的是，ostagram 的免费版渲染图片分辨率受限，且渲染一张图片时长还需耗时几分钟，付费版则会好很多。&lt;/p&gt; &lt;h2&gt;动画图片制作：Loading.io&lt;/h2&gt; &lt;div&gt;  &lt;a href="https://Loading.io" target="_blank"&gt;Loading.io&lt;/a&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;p&gt;微信公众号图文、网页总少不了各式各样的 Loading（载入）图示，在自己未具备能力制作前，总是很容易对第三方所提供的样式感到审美疲劳。&lt;/p&gt; &lt;p&gt;如果你有这样的困惑，不妨试一试支持通过这款在线的网页工具来制作 Loading 效果图的网站：Loading.io。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/ca35b39d52feeed8e11c730cad540578.png"&gt;&lt;/img&gt;Loading.io（网页经过浏览器翻译） &lt;p&gt;网站中有着各式各样的 Loading 载入图示可供选择，选择喜欢的图示后，便可设定 Loading 图示的大小、动画速度、颜色、背景颜色等等，上手非常简单且也提供了高度的自定义选项。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/86313e37a01a39d3649742d57a875888.gif"&gt;&lt;/img&gt;动态展示 &lt;p&gt;此外，在动画设定完成后，支持导出为 GIF、CSS、PNG、SVG 多种格式，便于你在不同场景的使用。&lt;/p&gt; &lt;h2&gt;图片合成：Moose&lt;/h2&gt; &lt;div&gt;  &lt;a href="https://photos.icons8.com/creator" target="_blank"&gt;Moose&lt;/a&gt;&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;p&gt;在制作一张合成图片时，往往都会到免费的素材库中找寻，但在质量却良莠不齐，并不能在第一时间找到高质量的素材图片。由 icon8 推出的高质量图库「Moose」，便能在一定程度上解决你的问题。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/bbc8ad0af8c6a4030ee0e2cd2df8f149.png"&gt;&lt;/img&gt;Moose &lt;p&gt;Moose 主要将素材图片分拆成三部分：人物、背景、物件。作为使用者，你不仅可以直接将图片素材导出为高清 PNG，还可以直接在网站中选择组合各个部分的素材，拼成一张组合图片。&lt;/p&gt; &lt;p&gt;操作的方法非常简单，可直接拖拽素材，并支持大小、方向调节，让最终的组合图片更具合理的视觉效果。除此之外，Moose 还提供了添加文字的功能选项。值得注意的是，Moose 免费版仅支持导出为 PNG 格式，PSD 图层格式则需付费进行解锁。&lt;/p&gt; &lt;img alt="" src="https://cdn.sspai.com/2019/04/02/1636b248660e5a52d39656e25f3842d7.gif" width="600"&gt;&lt;/img&gt;选择素材拼图 &lt;p&gt;倘若你还有其它的在线图片处理工具推荐，也欢迎在评论里留言分享。  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;&amp;gt; 下载少数派   &lt;a href="https://sspai.com/page/client" target="_blank"&gt;客户端&lt;/a&gt;、关注   &lt;a href="http://sspai.com/s/KEPQ" target="_blank"&gt;少数派公众号&lt;/a&gt; ，了解更多有趣的应用   &lt;br /&gt;&amp;gt; 特惠、好用的硬件产品，尽在   &lt;a href="https://shop549593764.taobao.com/?spm=a230r.7195193.1997079397.2.2ddc7e0bPqKQHc"&gt;少数派 sspai 官方店铺&lt;/a&gt; &lt;/p&gt; &lt;p&gt;  &lt;br /&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 />
      <guid isPermaLink="true">https://itindex.net/detail/59413-%E9%9C%80%E8%A6%81-photoshop-%E5%85%8D%E8%B4%B9</guid>
      <pubDate>Tue, 02 Apr 2019 16:02:27 CST</pubDate>
    </item>
    <item>
      <title>如何使用Jenkins持续集成C#网站项目 - 暗夜孤灯的个人页面 - 开源中国</title>
      <link>https://itindex.net/detail/59394-jenkins-%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90-%E7%BD%91%E7%AB%99</link>
      <description>&lt;div&gt;    &lt;div&gt;      &lt;p&gt;    &lt;br /&gt;&lt;/p&gt;&lt;/div&gt;    &lt;p&gt;上两节分别讲了如何从vss迁移C#网站项目到gitlab和如何使用nuget管理C#网站项目，其实都是为了最后一节的内容做铺垫：持续集成C#网站项目,这里我们使用的持续集成工具是Jenkins&lt;/p&gt;    &lt;p&gt;软件环境：&lt;/p&gt;    &lt;p&gt;      &lt;a href="https://jenkins.io/" rel="nofollow"&gt;Jenkins ver. 2.73&lt;/a&gt;&lt;/p&gt;    &lt;p&gt;freesshd 1.3.1&lt;/p&gt;    &lt;p&gt;nuget 4.1.0.2450&lt;/p&gt;    &lt;p&gt;msbuild 15.0&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;1.持续集成思路&lt;/p&gt;    &lt;p&gt;      &lt;img height="309" src="https://static.oschina.net/uploads/space/2017/0928/200610_8XN8_815996.png" width="589"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;项目按这个思路初始化配置到Jenkins后，每次提交代码到gitlab，Jenkins自动触发完成部署。我用一台Windows服务器192.168.0.136用来部署Jenkins，另外一台Windows服务器234部署项目&lt;/p&gt;    &lt;p&gt;2.Jenkins的安装比较简单，这里不细讲，网上有很多教程，用到的插件包括      &lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin" rel="nofollow"&gt;GitLab Plugin&lt;/a&gt;，      &lt;a href="http://wiki.jenkins-ci.org/display/JENKINS/MSBuild+Plugin" rel="nofollow"&gt;MSBuild Plugin&lt;/a&gt;，      &lt;a href="http://wiki.jenkins-ci.org/display/JENKINS/Publish+Over+SSH+Plugin" rel="nofollow"&gt;Publish Over SSH&lt;/a&gt;等&lt;/p&gt;    &lt;p&gt;3.初始配置过程&lt;/p&gt;    &lt;p&gt;新建Jenkins项目，选择“构建一个自由风格的软件项目”，保存&lt;/p&gt;    &lt;p&gt;      &lt;img height="638" src="https://static.oschina.net/uploads/space/2017/0928/200624_U9eK_815996.png" width="1181"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;1）gitlab获取最新代码&lt;/p&gt;    &lt;p&gt;在“源码管理”区，选择git配置好项目地址，认证用户，分支等信息（需事先在Jenkins配置好gitlab，如gitlab地址，sshkey等认证信息）&lt;/p&gt;    &lt;p&gt;      &lt;img height="685" src="https://static.oschina.net/uploads/space/2017/0928/200643_nuFV_815996.png" width="1184"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;2）Nuget获取依赖包配置&lt;/p&gt;    &lt;p&gt;在“构建”区，选择“Execute Windows batch command”,&lt;/p&gt;    &lt;p&gt;      &lt;img height="197" src="https://static.oschina.net/uploads/space/2017/0928/200654_udOs_815996.png" width="1130"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;&amp;quot;D:\Program Files&amp;quot;\nuget\nuget.exe restore &amp;quot;Suntime.DigitalMarketing.Web.sln&amp;quot; -ConfigFile &amp;quot;C:\Users\Administrator\AppData\Roaming\NuGet\NuGet.config&amp;quot; -NoCache&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;去官网（      &lt;a href="https://www.nuget.org/downloads" rel="nofollow"&gt;https://www.nuget.org/downloads&lt;/a&gt;）下载nuget.exe并安装，把自定义的nuget server配置到NuGet.config中，以便能从自定义Nuget仓库中下载依赖包&lt;/p&gt;    &lt;p&gt;      &lt;img height="644" src="https://static.oschina.net/uploads/space/2017/0928/200709_EKGG_815996.png" width="1156"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;3）msbuild构建解决方案并发布&lt;/p&gt;    &lt;p&gt;继续在“构建”区，选择“Build a Visual Studio project or solution using MSBuild”&lt;/p&gt;    &lt;p&gt;      &lt;img height="422" src="https://static.oschina.net/uploads/space/2017/0928/200718_Deuh_815996.png" width="1139"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;其中的MSBuild Version需要先配好，在Jenkins&amp;gt;Global Tool Configuration设置MSBuild，如下图我配置了两个MSBuild4.0和MSBuild15.0（指向的是vs2017的MSBuild.exe）&lt;/p&gt;    &lt;p&gt;      &lt;img height="905" src="https://static.oschina.net/uploads/space/2017/0928/200723_IckQ_815996.png" width="1908"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;参数列表：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;/t:Rebuild

/toolsversion:15.0

/property:Configuration=Release;PublishProfile=jenkins-deploy;DeployOnBuild=true;TargetFrameworkVersion=v4.5&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;其中的PublishProfile=jenkins-deploy指的是发布用的pubxml文件，我的发布路径是D:\jenkins-deploy\digitalmarket_web，这个路径在后面的打包压缩时会用到&lt;/p&gt;    &lt;p&gt;      &lt;img height="1041" src="https://static.oschina.net/uploads/space/2017/0928/200731_LgDC_815996.png" width="1920"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;4）压缩发布文件&lt;/p&gt;    &lt;p&gt;继续在“构建”区，选择“Execute Windows batch command”&lt;/p&gt;    &lt;p&gt;      &lt;img height="296" src="https://static.oschina.net/uploads/space/2017/0928/200737_noRe_815996.png" width="1126"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;set projectName=digitalmarket_web

set targetFilePath=&amp;quot;D:\jenkins-deploy\&amp;quot;

set jenkins=&amp;quot;D:\Program Files\Jenkins\workspace\&amp;quot;%projectName%

set rarexe=&amp;quot;C:\Program Files\WinRAR\&amp;quot;

cd %targetFilePath%

%rarexe%Rar.exe a -r -x%projectName%\Web.config -ep1 %jenkins%\%projectName%.rar %projectName%&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;因本地环境和服务器Web.config配置可能不同，所以在压缩文件时需排除项目中的Web.config文件&lt;/p&gt;    &lt;p&gt;5）上传到部署服务器并解压文件，使用ssh上传文件后执行远程脚本解压文件，在234服务器上使用freesshd搭建ssh服务&lt;/p&gt;    &lt;p&gt;在两台服务器能正常通信的前提下，开通234服务器上的22和23端口&lt;/p&gt;    &lt;p&gt;      &lt;img height="931" src="https://static.oschina.net/uploads/space/2017/0928/200745_B9gf_815996.png" width="1729"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在freesshd中创建用户jenkins，把136服务器.ssh下的公钥复制到设置的Public key folder路径下，并重命名为jenkins，公钥名必须和用户名一致&lt;/p&gt;    &lt;p&gt;设置SFTP路径为：D:\jenkins，用于存放上传文件&lt;/p&gt;    &lt;p&gt;      &lt;img height="471" src="https://static.oschina.net/uploads/space/2017/0928/200817_nIgo_815996.png" width="505"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在“构建后操作”区，选择“Send build artifacts over SSH”&lt;/p&gt;    &lt;p&gt;      &lt;img height="704" src="https://static.oschina.net/uploads/space/2017/0928/200826_O1Zo_815996.png" width="1106"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;命令行：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;cmd.exe /c &amp;quot;d: &amp;amp;&amp;amp; cd jenkins &amp;amp;&amp;amp; unrar.bat digitalmarket_web&amp;quot;&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;其中unrar.bat为放在D:\jenkins中的解压脚本，脚本命令如下&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;set targetFilePath=D:\jenkins\

set rarexe=&amp;quot;C:\Program Files\WinRAR\&amp;quot;

cd &amp;quot;%targetFilePath%&amp;quot;

%rarexe%UnRAR x -o+ -y %1.rar&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;4.最后添加自动构建设置：gitlab上项目代码一旦更新就会触发构建&lt;/p&gt;    &lt;p&gt;      &lt;img height="822" src="https://static.oschina.net/uploads/space/2017/0928/200842_Ntf5_815996.png" width="1153"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;5.试验一下，成功后在部署服务器的iis中配好网站就行&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;git获取最新代码&lt;/p&gt;    &lt;p&gt;      &lt;img height="169" src="https://static.oschina.net/uploads/space/2017/0928/200848_6P53_815996.png" width="868"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;Nuget获取依赖包配置&lt;/p&gt;    &lt;p&gt;      &lt;img height="626" src="https://static.oschina.net/uploads/space/2017/0928/200857_YZJY_815996.png" width="1222"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt;msbuild生成解决方案&lt;/p&gt;    &lt;p&gt;      &lt;img height="573" src="https://static.oschina.net/uploads/space/2017/0928/200902_G7s0_815996.png" width="1425"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;发布打包&lt;/p&gt;    &lt;p&gt;      &lt;img height="586" src="https://static.oschina.net/uploads/space/2017/0928/200907_CqfT_815996.png" width="1293"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;压缩发布文件并上传&lt;/p&gt;    &lt;p&gt;      &lt;img height="332" src="https://static.oschina.net/uploads/space/2017/0928/200922_KC5A_815996.png" width="820"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;远程解压成功&lt;/p&gt;    &lt;p&gt;      &lt;img height="254" src="https://static.oschina.net/uploads/space/2017/0928/200928_Kwp8_815996.png" width="821"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;p&gt; &lt;/p&gt;    &lt;div&gt;      &lt;div&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 />
      <guid isPermaLink="true">https://itindex.net/detail/59394-jenkins-%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90-%E7%BD%91%E7%AB%99</guid>
      <pubDate>Sat, 23 Mar 2019 23:03:15 CST</pubDate>
    </item>
    <item>
      <title>如何把多个网页合并下载为 PDF</title>
      <link>https://itindex.net/detail/59227-%E7%BD%91%E9%A1%B5-%E5%90%88%E5%B9%B6-%E4%B8%8B%E8%BD%BD</link>
      <description>&lt;p&gt;来自微博的  &lt;a href="https://weibo.com/1684197391/HccTX2Qib"&gt;问题&lt;/a&gt;：有没有什么软件可以把多个网页合并下载为 PDF，比如我想把图上的域名下的所有网页合并下载为一个 PDF 文件。@小众软件 ​​​​&lt;/p&gt;



 &lt;div&gt;  &lt;img alt="" src="https://img3.appinn.com/images/201901/banner.jpg!o"&gt;&lt;/img&gt;&lt;/div&gt;



 &lt;p&gt;这是一个非常经典的问题，关乎两种文档格式的不同特性。因为 HTML 的离线不可用性，和 PDF 的任何情况下的完整性，所以将需要离线阅读的内容 PDF 化是一个非常不错的主意。&lt;/p&gt;



 &lt;p&gt;最终 @  &lt;strong&gt;剑心_h&lt;/strong&gt; 同学推荐了一款命令行工具：  &lt;a href="https://github.com/wkhtmltopdf/wkhtmltopdf"&gt;wkhtmltopdf&lt;/a&gt;（在 GitHub 开源），青小蛙研究了一下，似乎可行，简单易用。&lt;/p&gt;



 &lt;p&gt;wkhtmltopdf 全称   &lt;strong&gt;WK&amp;lt;html&amp;gt;TOpdf&lt;/strong&gt;，是一个专门用来转换网页到   &lt;a href="https://www.appinn.com/tag/pdf/"&gt;PDF&lt;/a&gt; 的工具，唯一的门槛就是没有图形化界面，需要使用命令行。&lt;/p&gt;



 &lt;p&gt;在使用之前需要安装，Windows 用户需要进入安装目录后，在地址栏输入 cmd 以打开当前目录的命令行，然后就可以使用了。其他系统用户相信会很熟悉命令行。&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;wkhtmltopdf https://www.appinn.com/ appinn.pdf&lt;/code&gt;&lt;/pre&gt;



 &lt;p&gt;上面就是最近的将网页转换为 pdf 的命令，而如果你想添加目录，或者禁用 js，还可以使用参数:&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;wkhtmltopdf toc https://www.appinn.com/ appinn.pdf #生成目录
wkhtmltopdf -n https://www.appinn.com/ appinn.pdf #禁用 js&lt;/code&gt;&lt;/pre&gt;



 &lt;p&gt;这样就能非常方便的将网页转换为 pdf 了，还有参数可以设置分辨率、页面边距、是否压缩、是否显示图片等等，更多可以参考  &lt;a href="https://www.jianshu.com/p/4d65857ffe5e"&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;



 &lt;p&gt;有了 PDF，合并就容易了，这里推荐   &lt;a href="https://www.appinn.com/smallpdf/"&gt;SmallPDF&lt;/a&gt; 在线工具，很方便就合并了。&lt;/p&gt;



 &lt;p&gt;如果，你有更好的将图片转换成 PDF 的工具，欢迎留言推荐。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt; &lt;h2&gt;相关阅读&lt;/h2&gt; &lt;ul&gt;  &lt;li&gt;   &lt;a href="https://www.appinn.com/pdfzilla-pdf-word/" rel="bookmark" title="Permanent Link: PDFZilla &amp;#38480;&amp;#26102;&amp;#20813;&amp;#36153;&amp;#65292;&amp;#19987;&amp;#19994; PDF &amp;#36716;&amp;#25442; Word &amp;#24037;&amp;#20855;"&gt;PDFZilla 限时免费，专业 PDF 转换 Word 工具&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://www.appinn.com/convert-simply-converting/" rel="bookmark" title="Permanent Link: Convert &amp;#8211; &amp;#31616;&amp;#21333;&amp;#24555;&amp;#36895;&amp;#30340;&amp;#21333;&amp;#20301;&amp;#36716;&amp;#25442;&amp;#24212;&amp;#29992;[iPhone/Android]"&gt;Convert – 简单快速的单位转换应用[iPhone/Android]&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://www.appinn.com/wendintong-pdf-suite/" rel="bookmark" title="Permanent Link: &amp;#22269;&amp;#20135;&amp;#27491;&amp;#29256; PDF &amp;#36719;&amp;#20214; &amp;#8211; &amp;#12300;&amp;#25991;&amp;#30005;&amp;#36890; PD F&amp;#22871;&amp;#35013;&amp;#29256; 4&amp;#12301;&amp;#35780;&amp;#27979;"&gt;国产正版 PDF 软件 – 「文电通 PD F套装版 4」评测&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://www.appinn.com/pdf-burger/" rel="bookmark" title="Permanent Link: PDF Burger &amp;#8211; &amp;#21508;&amp;#31181; PDF &amp;#22312;&amp;#32447;&amp;#36716;&amp;#25442;[Web]"&gt;PDF Burger – 各种 PDF 在线转换[Web]&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;   &lt;a href="https://www.appinn.com/online-pdf-tools/" rel="bookmark" title="Permanent Link: 12&amp;#27454;&amp;#22312;&amp;#32447; PDF &amp;#24037;&amp;#20855;"&gt;12款在线 PDF 工具&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;hr&gt;&lt;/hr&gt;
 &lt;a href="http://www.appinn.com/copyright/?utm_source=feeds&amp;utm_medium=copyright&amp;utm_campaign=feeds" title="&amp;#29256;&amp;#26435;&amp;#22768;&amp;#26126;"&gt;©&lt;/a&gt;2019 青小蛙 for  &lt;a href="http://www.appinn.com/?utm_source=feeds&amp;utm_medium=appinn&amp;utm_campaign=feeds" title="&amp;#26412;&amp;#25991;&amp;#26469;&amp;#33258;&amp;#23567;&amp;#20247;&amp;#36719;&amp;#20214;"&gt;小众软件&lt;/a&gt; |  &lt;a href="http://www.appinn.com/join-us/?utm_source=feeds&amp;utm_medium=joinus&amp;utm_campaign=feeds" title="&amp;#21152;&amp;#20837;&amp;#23567;&amp;#20247;&amp;#36719;&amp;#20214;"&gt;加入我们&lt;/a&gt; |  &lt;a href="https://meta.appinn.com/c/faxian/?utm_source=feeds&amp;utm_medium=contribute&amp;utm_campaign=feeds" target="_blank" title="&amp;#32473;&amp;#23567;&amp;#20247;&amp;#36719;&amp;#20214;&amp;#25237;&amp;#31295;"&gt;投稿&lt;/a&gt; |  &lt;a href="http://www.appinn.com/feeds-subscribe/?utm_source=feeds&amp;utm_medium=feedsubscribe&amp;utm_campaign=feeds" target="_blank" title="&amp;#21487;&amp;#20197;&amp;#20998;&amp;#31867;&amp;#35746;&amp;#38405;&amp;#23567;&amp;#20247;&amp;#65292;Windows/MAC/&amp;#28216;&amp;#25103;"&gt;订阅指南&lt;/a&gt; |  &lt;a href="http://appinn.wufoo.com/forms/eccae-aeeae/"&gt;反馈&lt;/a&gt; |  &lt;a href="http://hellohostnet.com/proxy.html"&gt;代理&lt;/a&gt;(优惠码 Appinn) &lt;br /&gt; 3659b075e72a5b7b1b87ea74aa7932ff  &lt;br /&gt;
 &lt;a href="https://www.appinn.com/how-to-convert-html-to-pdf/#comments" title="to the comments"&gt;点击这里留言、和原作者一起评论&lt;/a&gt; &lt;em&gt;&lt;/em&gt;收藏0&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>问答 PDF 网页 转换</category>
      <guid isPermaLink="true">https://itindex.net/detail/59227-%E7%BD%91%E9%A1%B5-%E5%90%88%E5%B9%B6-%E4%B8%8B%E8%BD%BD</guid>
      <pubDate>Thu, 17 Jan 2019 12:11:09 CST</pubDate>
    </item>
    <item>
      <title>把网页文章变成电子书装进 Kindle：Doocer</title>
      <link>https://itindex.net/detail/58664-%E7%BD%91%E9%A1%B5-%E6%96%87%E7%AB%A0-%E7%94%B5%E5%AD%90%E4%B9%A6</link>
      <description>&lt;p&gt;
	一直以来我都在研究如何将关注的网页或文章装进 Kindle 来进行严肃阅读，但是无奈久久没有实质进展。尽管    &lt;a href="https://sspai.com/post/44436"&gt;手动排版书籍制作电子书&lt;/a&gt; 的方法简单易行，但是对于篇幅短、更新频繁的网页文章来说，这种方法的效率低下。如果能有一款工具把网页文章批量抓取、生成电子书、直接推送到 Kindle，那真是再好不过了。  &lt;a href="https://doocer.com/my/home"&gt;Doocer&lt;/a&gt; 就是这样的一款实用工具。
&lt;/p&gt;
 &lt;p&gt;
	Doocer 是由   &lt;a href="https://twitter.com/lepture"&gt;@lepture&lt;/a&gt; 开发的在线服务，它可以允许用户提交网址、RSS 订阅源地址还有 Pocket 稍后读账号里的文章，随后挨个或批量制作成 ePub、MOBI 电子书。你可以直接在 Doocer 里阅读所有文章，或者将它们推送到 Kindle、Apple Books 里阅读。
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/850323dc1c72c7cb859f5a85ce916ca5.jpg"&gt;&lt;/img&gt;
 &lt;h2&gt;阅读体验确实不错&lt;/h2&gt;
 &lt;p&gt;
	Doocer 生成的电子书，排版优良可圈可点，该有的内容一样不少，不该有的内容一样不多。书籍不仅封面图文并茂，而且文章目录、网站来源、文章原作者等信息一应俱全。Doocer 生成的 MOBI 电子书支持 KF8 标准，因此支持   &lt;a href="https://sspai.com/post/44914"&gt;Kindle 原生更换自定义字体&lt;/a&gt; 功能。
&lt;/p&gt;
 &lt;p&gt;
	得益于网站文章通常有标准通用的排版规范，Doocer 生成的电子书文章里的大小标题、列表图例都与原网页文章高度一致。原文章中的超链接也全部保留，评论信息、广告等内容则全部摈弃。整本书籍的阅读体验非常友好。（当然如果原网页文章的排版毫无章法，那么生成的电子书也可能面目全非。）
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/56a026e92b9c0aa342f4143e8828736a.jpg"&gt;&lt;/img&gt;
 &lt;h2&gt;将网页文章制作成电子书籍&lt;/h2&gt;
 &lt;p&gt;
	在   &lt;a href="https://doocer.com/my/home"&gt;Doocer&lt;/a&gt; 完成注册登录之后，我们就可以开始将网页文章制作成电子书。首先我们点击「NEW BOOK」按钮来新建一本电子书，并输入电子书名。接下来在右上角选择「ADD」来添加文章网址或 RSS 订阅源地址。
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/7f7ef428edcc817414ebaaf2aea1343d.png"&gt;&lt;/img&gt;
 &lt;p&gt;
	以将少数派网页的文章为例，我们选择「FEED」，在输入框内粘贴 RSS 地址，随后点击「PARSE」，那么少数派近期文章的列表都会显示出来供我们添加。我们可以按需选择，也可以点击「SELECT ALL」选中全部文章。最后下拉到页面底部，选择「SAVE」，那么这些文章都会被添加进书籍里。
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/260a207ec2fdf0283b568b9ff2369895.png"&gt;&lt;/img&gt;
 &lt;p&gt;
	实际上，Doocer 网页与 RSS 工具极为相似，它是实现了从网站中批量抓取文章，集中展示的功能。
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/d451d3cbb068ed831c0b851f715acfe1.png"&gt;&lt;/img&gt;
 &lt;p&gt;
	要将这些文章转换成电子书并推送至 Kindle，我们还要进行一些简单的操作。
&lt;/p&gt;
 &lt;p&gt;
	首先根据 Doocer 个人设置页面中的提示，我们打开   &lt;a href="https://www.amazon.cn/hz/mycd/myx#/home/settings/payment"&gt;亚马逊 Kindle 的个人文档设置&lt;/a&gt;，把 Doocer 电子书的发送地址添加进个人文档接收地址中去。完成后，我们再把 Kindle 的个人文档接收地址填写在输入框中，点击保存。 
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/c8f1fbbee49d54557d31f15f6d517770.png"&gt;&lt;/img&gt;
 &lt;p&gt;
	最后，我们打开 Doocer 里的「少数派」这本书籍，在页面中找到 「Publish」，选择 Send to Kindle。10～30 分钟左右，Doocer 会完成书籍制作并将这本书推送到 Kindle 中去。
&lt;/p&gt;
 &lt;img alt="" src="https://cdn.sspai.com/2018/08/26/d374b13409b054410e97b7f9618ad126.png"&gt;&lt;/img&gt;
 &lt;h2&gt;还有一些问题需要注意&lt;/h2&gt;
 &lt;p&gt;
	Doocer 现处于 Beta 测试阶段，仍有一些 bug，特别是对于中文网站常会出现问题。好在 Doocer 官网就有开发者的对话通道，可以直接和他取得联系帮助解决。
&lt;/p&gt;
 &lt;p&gt;
	实现所有操作的自动化流程，是我认为 Doocer 最需要努力的方向。Doocer 可以像 RSS 工具那样抓取网页内更新的文章，但是将新增文章的抓取并生成电子书以及推送，还是需要手动进行。如果能够将整个流程都实现自动化，RSS - MOBI - Kindle 一气呵成，我相信它的实用性会更上一层楼。
&lt;/p&gt;
 &lt;p&gt;
	目前，  &lt;a href="https://doocer.com/my/home"&gt;Doocer&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 />
      <guid isPermaLink="true">https://itindex.net/detail/58664-%E7%BD%91%E9%A1%B5-%E6%96%87%E7%AB%A0-%E7%94%B5%E5%AD%90%E4%B9%A6</guid>
      <pubDate>Sun, 26 Aug 2018 16:07:06 CST</pubDate>
    </item>
    <item>
      <title>如何下载网页上的视频？ - 知乎</title>
      <link>https://itindex.net/detail/58532-%E4%B8%8B%E8%BD%BD-%E7%BD%91%E9%A1%B5-%E8%A7%86%E9%A2%91</link>
      <description>&lt;div&gt;    &lt;h2&gt;2017.1.13   &lt;br /&gt;&lt;/h2&gt;    &lt;h2&gt;pc端软件&lt;/h2&gt;    &lt;p&gt;适用场景：优酷，土豆1080p视频，腾讯视频等&lt;/p&gt;    &lt;p&gt;1.硕鼠。我比较常用的一类软件，能够进行      &lt;strong&gt;清晰度解析&lt;/strong&gt;下载，用过很长一段时间了，需要下载视频的就会找到它。今天发现硕鼠没有以前强大了...以前还能下载优酷视频专辑。&lt;/p&gt;    &lt;p&gt;类似的有      &lt;strong&gt;猎影&lt;/strong&gt;，以前没用过，今天试用了一下，很好用。能够下载优酷和土豆1080p的视频。很棒的软件。&lt;/p&gt;    &lt;img src="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='547'%20height='122'&gt;&lt;/svg&gt;" width="547"&gt;&lt;/img&gt;    &lt;p&gt;2.IDM下载器。平时代替浏览器下载器使用，有视频和音频嗅探功能，可以下载在线视频，一个好用的地方是：百度      &lt;strong&gt;贴吧的语音&lt;/strong&gt;，能够嗅探并且下载下来，而有些扩展插件是不能嗅探到的。&lt;/p&gt;    &lt;h2&gt;扩展插件&lt;/h2&gt;    &lt;p&gt;适用场景：秒拍，微博视频等&lt;/p&gt;    &lt;p&gt;我平时用chrome和360极速浏览器； 360极速上安装有      &lt;strong&gt;        &lt;a href="https://link.zhihu.com/?target=https%3A//ext.chrome.360.cn/webstore/detail/bbpblmaibbbppggogfpbmjkfacpbffen" rel="nofollow noreferrer" target="_blank"&gt;FVD Video downloader&lt;/a&gt;&lt;/strong&gt;；这款扩展的作用也是嗅探网页上的视频，嗅探到后有图标显示，直接点击即可下载，使用很方便。&lt;/p&gt;    &lt;h2&gt;youtube视频&lt;/h2&gt;    &lt;p&gt;对于youtube视频，我的方法是一个油猴脚本：Download YouTube Videos as MP4；在视频播放页面会出现下载按钮，可选清晰度至720p。下载地址：      &lt;strong&gt;        &lt;a href="https://link.zhihu.com/?target=https%3A//greasyfork.org/zh-CN/scripts/1317-download-youtube-videos-as-mp4" rel="nofollow noreferrer" target="_blank"&gt;Download YouTube Videos as MP4&lt;/a&gt;&lt;/strong&gt;（在chrome上先安tampermonkey脚本管理器）；&lt;/p&gt;    &lt;p&gt;如果需要下载youtube 1080p的视频，可以用      &lt;a href="https://link.zhihu.com/?target=https%3A//www.gihosoft.com/free-youtube-downloader.html" rel="nofollow noreferrer" target="_blank"&gt;Gihosoft TubeGet&lt;/a&gt;；&lt;/p&gt;    &lt;p&gt;      &lt;img src="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='578'%20height='375'&gt;&lt;/svg&gt;" width="578"&gt;&lt;/img&gt;      &lt;strong&gt;其他&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;VIP解析，支持爱奇艺，优酷，腾讯，搜狐，芒果等；地址：      &lt;a href="https://link.zhihu.com/?target=https%3A//pan.baidu.com/s/1qYLbfvI" rel="nofollow noreferrer" target="_blank"&gt;https://pan.baidu.com/s/1qYLbfvI&lt;/a&gt;密码: ikm6&lt;/p&gt;    &lt;p&gt;审查元素，在线解析视频的网站，google。&lt;/p&gt;    &lt;p&gt;网友推荐：      &lt;a href="https://link.zhihu.com/?target=http%3A//www.jianshu.com/p/a3f8df948395" rel="nofollow noreferrer" target="_blank"&gt;You-Get——基于Python3的媒体下载工具&lt;/a&gt;&lt;/p&gt;    &lt;br /&gt;    &lt;p&gt;原文在这里：      &lt;a href="https://zhuanlan.zhihu.com/p/24861140"&gt;https://zhuanlan.zhihu.com/p/24861140&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;作为使用过N种下载网页视频的实践者，郑重推荐几乎能下载网页全部视频的浏览器插件：   &lt;strong&gt;FVD Downloader&lt;/strong&gt;（基本上可以下载绝大部分常规和非常规视频网站的视频）。   &lt;br /&gt;   &lt;br /&gt;（PS:此插件不仅能够下载网页上的视频，   &lt;strong&gt;还可以支持下载音频网站的音乐&lt;/strong&gt;哦）   &lt;br /&gt;   &lt;br /&gt;只需要2步就可以轻松快速的把网页中的内容保存下来。   &lt;br /&gt;下面是具体的操作步骤：   &lt;br /&gt;   &lt;strong&gt;第一步：&lt;/strong&gt;   &lt;strong&gt;安装FVD Downloader插件（以360浏览器为例）&lt;/strong&gt;   &lt;br /&gt;   &lt;br /&gt;大家点击浏览器中的“拓展”   &lt;br /&gt;&lt;/p&gt;  &lt;img src="https://pic1.zhimg.com/80/70eaed8041041b2b196cc385db424eec_hd.png" width="667"&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;   &lt;br /&gt;然后在应用市场中找到FVD Downloader，点击“安装”（这里我已经安装好了）。   &lt;br /&gt;&lt;/p&gt;  &lt;img src="https://pic1.zhimg.com/80/c6c7973aca218535757314df6d11e624_hd.png" width="752"&gt;&lt;/img&gt;  &lt;p&gt;安装好后，你的浏览器搜索框附近会出现FVD Downloader的图标。   &lt;br /&gt;&lt;/p&gt;  &lt;img src="https://pic4.zhimg.com/80/2420936d16deb7253fd28bcbceb549f3_hd.png" width="519"&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;第二步：&lt;/strong&gt;   &lt;strong&gt;打开视频播放页面，点击插件，选择下载并保存&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;这里我们以“新浪视频”为例。（经测试：爱奇艺也可以，非常规视频网站也可以，具体请自行测试。）&lt;/p&gt;  &lt;p&gt;打开新浪视频，输入你想下载的视频名称，这里我输入：PPT，得到以下结果，我们就下载：PPT场景—5公里。   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;从网页中看到可以，该视频是不提供下载的，因为根本就找不到下载的按钮。&lt;/p&gt;  &lt;img src="https://pic2.zhimg.com/80/1615ba388406d3ecad51a55432f036fd_hd.png" width="940"&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;点开后，可以看到FVD Downloader图标是有颜色的，代表该页面有视频可以提供下载。假如该网站没有可提供的视频下载的话，该图标将会呈现灰色并且有一个×。&lt;/p&gt;  &lt;img src="https://pic1.zhimg.com/80/42a1238ac38e2a74010e80d8902f2e20_hd.png" width="663"&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;点击图标，可以看到以下界面   &lt;br /&gt;&lt;/p&gt;  &lt;img src="https://pic3.zhimg.com/80/0ee9a10c21164837dadeb7b24cb8cae6_hd.png" width="567"&gt;&lt;/img&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;最后一步点击下载。&lt;/p&gt;  &lt;p&gt;下载下来后用视频播放器播放，就可以离线观看该视频。&lt;/p&gt;  &lt;p&gt;如果你使用该插件后，可以看到有很多可以下载的视频内容，而且视频的格式为：ts，原因是该视频被分成了很多小段，只需要用命令行拼接一些就可以了。可以在windows的命令行中输入：copy/b （文件保存地址）\*.ts （文件保存地址）\new.ts 这样就可以&lt;/p&gt;  &lt;p&gt;注意：请勿用此方法下载收费或者有版权视频，毕竟原创者付出了大量的时间和精力制作视频，还望大家保护版权！&lt;/p&gt;  &lt;p&gt;——————分割线————————&lt;/p&gt;  &lt;p&gt;对于大家在评论区说的问题进行统一回答：&lt;/p&gt;  &lt;p&gt;（1）如果在插件下，一个网页内提示有多个视频文件，如何下载高清版本&lt;/p&gt;  &lt;p&gt;首先如果你想下载网页内高清版视频，首先你得把网页内视频的清晰度选择为高清的，因为插件是需要网页加载视频后才会识别是否有视频可以下载&lt;/p&gt;  &lt;p&gt;接着再次点击插件，就可以发现插件视频比原来多个视频了，选择大的文件就是高清版本的了&lt;/p&gt;  &lt;p&gt;（2）ts格式要用什么播放器看&lt;/p&gt;  &lt;p&gt;只要用常规的播放器就可以了，ts只是整个视频一小段，需要拼接起来才是一个完整的视频，具体拼接方法看我的之前的方法。播放器我建议用QQ影音，因为它没有广告而且体积小启动速度快&lt;/p&gt;  &lt;p&gt;——————分割线——————&lt;/p&gt;  &lt;p&gt;如果你对于这次讲解还有什么问题的话，可以随时在公众号【   &lt;strong&gt;    &lt;u&gt;印象演示&lt;/u&gt;&lt;/strong&gt;】发消息给我。&lt;/p&gt;  &lt;p&gt;或者加我的个人微信：rsmshuzhou&lt;/p&gt;  &lt;p&gt;   &lt;a href="https://link.zhihu.com/?target=http%3A//weixin.qq.com/r/kcAsNFfE6AqXrRNX95WD" rel="nofollow noreferrer" target="_blank"&gt;http://weixin.qq.com/r/kcAsNFfE6AqXrRNX95WD&lt;/a&gt; (二维码自动识别)&lt;/p&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 />
      <guid isPermaLink="true">https://itindex.net/detail/58532-%E4%B8%8B%E8%BD%BD-%E7%BD%91%E9%A1%B5-%E8%A7%86%E9%A2%91</guid>
      <pubDate>Thu, 19 Jul 2018 11:44:37 CST</pubDate>
    </item>
    <item>
      <title>代替那些「用完就删」的 App，试试这 9 个实用的网页小工具</title>
      <link>https://itindex.net/detail/58226-app-%E7%BD%91%E9%A1%B5-%E5%B0%8F%E5%B7%A5%E5%85%B7</link>
      <description>&lt;div&gt;  &lt;p&gt;很对人对于软件都有一种「收藏」的习惯，觉得这可能是一个工具就把它购买或下载放在那里，新鲜了两天就搁置在那儿，终究成了橱柜上的一个布满灰尘的摆设。其实很多时候我们可以换个思路，如果这个工具是在网页上，用的时候再打开，不用的时候就关闭，既不占用你电脑空间，也不受限于设备的系统，或许还能帮你省下不少的钱，今天少数派就为大家介绍一些实用而且有趣的网页工具。&lt;/p&gt;  &lt;h2&gt;   &lt;strong&gt;Internet Speed Test&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;一个良好的网速大概会有 80% 的概率提高我们的生产力和执行力，糟糕的网速大概会有 99% 的概率影响我们的情绪。所以经常会有一些朋友频繁的测量网速，有的选择下载一款软件进行测量，有的随便找一个网站就将就了。而   &lt;strong&gt;FAST&lt;/strong&gt;和    &lt;strong&gt;SPEEDTEST&lt;/strong&gt;是两家「优雅」的测速站点，只点击一下 ► 按钮再稍等片刻，你的网速测试结果就会呈现在你的面前，无污染、零广告、完全免费。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="https://fast.com" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;FAST&lt;/strong&gt;&lt;/a&gt;、   &lt;strong&gt;    &lt;a href="http://www.speedtest.net" rel="nofollow,noindex" target="_blank"&gt;SPEEDTEST&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;  &lt;img src="https://img2.tuicool.com/JFzAnmE.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;whiteboard&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;白板的用处有很多，打草稿，记录突如其来的灵感，或者玩玩你画我猜也是可以的，   &lt;strong&gt;whiteboard&lt;/strong&gt;就是这样的一个随开随用的白板网站。这个网站的构造非常简洁，工具只有画笔、文本框、橡皮擦，虽然工具简单，但几乎所有的操作都支持快捷键，无需你去工具中点来点去。如果你绘制的图像趋于一定的形状，例如圆形、矩形，它可以自动的转换为标准形状。绘制完成的白板，也可以通过右上角的「Share」分享给其他的人，而其余的人也可以进行编辑，而两边的画面也是同步的。除此之外，该网站也提供了 Slack 群组的插件，如果你们有用到 Slack，不妨添加这款插件试一试。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="https://witeboard.com" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;whiteboard&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img2.tuicool.com/YVfmyuN.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;在线 Markdown 编辑器&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;不知道前段时看了   &lt;a href="https://sspai.com/post/43866" rel="nofollow,noindex" target="_blank"&gt;《想学 Markdown？这篇文章帮你快速上手》&lt;/a&gt;之后有没有想尝试一下 Markdown 的冲动，对于还没选择到合适 Markdown 工具的时候，不如先用这款   &lt;strong&gt;在线 Markdown 编辑器&lt;/strong&gt;体验一下语法，在这里左侧输入的文本内容都会实时的显示右侧和下侧，分别是 HTML 格式预览以及实时效果预览。但是在网站编辑的内容并没有提供保存到本地的入口，如果你需要请及时复制粘贴。建议只是作为临时备用方案或是体验方案，如需深度体验还是选择专门的 Markdown 工具。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="http://tool.oschina.net/markdown" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;在线 Markdown 编辑器&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img0.tuicool.com/mUfABvm.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;草料二维码&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;二维码在生活中可以说是非常常见了，从付款码到共享单车，随处可见。其实对于我们来说，不仅可以扫码当然也可以制作二维码，例如我们想分享某个网址，或者把我们的 Wi-Fi 提供给别人（可以用这个 Workflow）。而   &lt;strong&gt;草料二维码&lt;/strong&gt;不仅给我们提供了制作二维码的途径，也为我们附上了一些「优雅」姿势。在这里你只需要输入的文本内容，在右侧选择二维码属性。可以选择直接使用模版图案快速美化，也可以使用旧版的美化器（功能齐全）来进行更细致的操作，例如选择背景前景图、颜色以及二维码状态。这样以来，不论你的二维码用在哪里，都再也不怕单调无味。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="https://cli.im/" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;草料二维码&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img1.tuicool.com/qU3QFfI.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;图片处理&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;图片在我们生活中的用处太多了，但时常我们也会因为图片的事情而苦恼。因为在不同的使用场景里，对图片的要求可能都不太一样，例如有的限制了大小，有的限制了格式。对于这些问题，我们其实不需要复杂了步骤，也不需要安装一些软件，只需要上传一下然后处理就可以了。对于图片的格式问题，你只需要在   &lt;strong&gt;Jinaconvert&lt;/strong&gt;上选择你需要的格式类型，再将图片文件上传至即可。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="https://jinaconvert.com/cn/" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;Jinaconvert&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img1.tuicool.com/BBRZZbn.png!web"&gt;&lt;/img&gt;  &lt;p&gt;你在上传图片时因为大小而受到了限制，可以在   &lt;strong&gt;TinyPNG&lt;/strong&gt;这个网站上进行压缩操作。该网站支持对    &lt;code&gt;.png&lt;/code&gt;和    &lt;code&gt;.jpg&lt;/code&gt;格式的图片进行压缩，只需要将图片拖拽至网站的窗口即可，一次性可以压缩 20 张，且质量非常不错。如果你非常喜欢它，也可以移步阅读    &lt;a href="https://sspai.com/post/41979" rel="nofollow,noindex" target="_blank"&gt;《TinyPNG 是我最喜欢的在线压图服务，现在有人给它做了 macOS 客户端》&lt;/a&gt;。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="https://tinypng.com" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;TinyPNG&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img2.tuicool.com/A3Av2aq.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;Ringer&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;手机的铃声是不是从来没有更换过，可能是因为制作铃声的方法有那么点复杂，一步步的操作加上各式软件，或许就让你放弃了这个念头。其实，你只需要把各类格式的音乐源文件上传到   &lt;strong&gt; Ringer &lt;/strong&gt;上，选择选上铃声的开头结尾。最后选择 M4R 或是 MP3 就可以了，可以说是免费又快捷了。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="http://ringer.org/" rel="nofollow,noindex" target="_blank"&gt;Ringer&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img2.tuicool.com/2MnMna2.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;Keyboard Layout Editor&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;   &lt;strong&gt;Keyboard Layout Editor&lt;/strong&gt;是一个键盘图纸设计的网站，它的作用可以说是非常的多。如果你刚上手一个软件，但是却无法记住它的全部快捷键，不如直接将快捷强的功能放置在图纸上，打印下来贴在桌边，帮助你记忆。如果你正常考虑一款机械键盘的配色，你可以在这上面选择相应的键位进行颜色的修改，最后看看整体的配色效果。它的自定义程度非常的高，在这里几乎可以编辑键位所有的参数，所以它究竟能起到什么样的作用，就看你如何利用它了。设计好的内容网站也提供了五种格式的文件方便用户下载，如果喜欢不妨加入到收藏夹里。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="http://www.keyboard-layout-editor.com/" rel="nofollow,noindex" target="_blank"&gt;Keyboard Layout Editor&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img0.tuicool.com/aUrMrqB.png!web"&gt;&lt;/img&gt;  &lt;h2&gt;   &lt;strong&gt;Gifntext&lt;/strong&gt;&lt;/h2&gt;  &lt;p&gt;还记得网上很火的「为所欲为」和「王境泽」吧，丰富的表情配上字幕，随随便便就是一个表情包。其实为 Gif 添加字幕也没有那么的困难，只需要将准备好的 Gif 文件上传到   &lt;strong&gt;Gifntext&lt;/strong&gt;上，然后就可以对播放速度以及画面尺寸进行调节。于此同时，也可以自由的在时间轴中加入想要的字幕、图片或是涂鸦。网站最大可以上传 100M 的 Gif 文件，修改完成后创建的动图自动的进行压缩。&lt;/p&gt;  &lt;p&gt;相关链接：   &lt;a href="http://www.gifntext.com" rel="nofollow,noindex" target="_blank"&gt;    &lt;strong&gt;Gifntext&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;  &lt;img src="https://img1.tuicool.com/uYzIvqM.png!web"&gt;&lt;/img&gt;  &lt;p&gt;如果你还有其他有意思的以及实用的网络在线工具，也欢迎在评论区留言推荐给大家！:tada:&lt;/p&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>tuicool</category>
      <guid isPermaLink="true">https://itindex.net/detail/58226-app-%E7%BD%91%E9%A1%B5-%E5%B0%8F%E5%B7%A5%E5%85%B7</guid>
      <pubDate>Sun, 08 Apr 2018 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>Pinterest 渐进式网页应用及性能问题 | 人人都是互联网创意G客</title>
      <link>https://itindex.net/detail/57884-pinterest-%E7%BD%91%E9%A1%B5%E5%BA%94%E7%94%A8-%E6%80%A7%E8%83%BD</link>
      <description>&lt;div&gt;    &lt;p&gt;Pinterest 新的移动端网页采用了        &lt;a href="https://developers.google.com/web/progressive-web-apps/"&gt;Progressive Web App&lt;/a&gt;。在这篇文章中，我们将介绍他们的一些工作，通过保持JavaScript包的精简以及采用 Service Workers 来应对网络波动。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01a69b69f933cab7fa.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;在你的手机上登录       &lt;a href="https://pinterest.com/"&gt;https://pinterest.com&lt;/a&gt; 体验他们新的移动端网站&lt;/p&gt;    &lt;h4&gt;为什么是渐进式网页应用 (PWA)? 和一些历史。      &lt;br /&gt;&lt;/h4&gt;    &lt;p&gt;Pinterest 决定使用开始使用 PWA 是因为他们专注于国际增长，他们把目光放到了移动端网页。      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;在分析了使用了移动端网页未登录用户的数据后，他们意识到自己古老，缓慢的网页体验仅仅转化了 1% 的用户去注册，登录或下载安装原生应用。这是大大提高转化率的机会，所以他们投资了 PWA。&lt;/p&gt;    &lt;p&gt;在一个季度中打造并且推出 PWA      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;在过去的三个月中，Pinterest 使用 React，Redux，webpack 重新打造了他们移动端网页。他们的移动端网页重写使核心业务指标的一些积极改进。&lt;/p&gt;    &lt;p&gt;与旧的移动Web体验相比，花费的时间是40%，用户产生的广告收入增加了 44% ，核心业务增加了 60% ： &lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01a9097688cc0a539e.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们移动端网页的重写同时也提高了性能。&lt;/p&gt;    &lt;h4&gt;在移动端3G网络下平均加载速度更快&lt;/h4&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 旧版的移动端网页体验像一块巨石—它包含了大量 CPU 计算的 JavaScript 包，      &lt;a href="https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e"&gt;拖延&lt;/a&gt;了 Pin 页面的加载和可交互的速度。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;在UI界面可响应之前，用户通常需要等待 23秒 ：&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01847db450269f2afe.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 旧版的移动端网站需要花23秒才可以交互，浏览器需要接收大约 2.5 MB 的 JavaScript (主包大约1.5MB，懒加载大约1MB) 并且花数秒钟去解析和编译在主线程可响应之前。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们新版移动端网页体验猛增。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们不仅分散和削减了数百KB的JavaScript，将其核心包的大小从650KB降低到150KB，而且还改进了关键性能指标。       &lt;a href="https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint"&gt;首次有意义的渲染&lt;/a&gt;从4.2s下降到1.8s，      &lt;a href="https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive"&gt;可交互时间&lt;/a&gt;从23s减少到5.6s。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01acd8f9dc8f792af9.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;这是Android设备在使用缓慢的3G网络连接。 重复访问，情况更好。&lt;/p&gt;    &lt;p&gt;由于Service Worker对主要JavaScript，CSS和静态UI资源进行缓存，这样能够将重复访问的互动时间缩短至3.9秒：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t010cd3fcea7bda7a15.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;虽然 Pinterest 提供 iOS 和 Android 应用程序，但是它们能够提供在网页上相同的核心主页消息流订阅体验，只需要一小部分的前期下载成本 - 压缩后只需约150KB。 这与 Android 的 9.6MB 和 iOS 的 56MB 提供相同的体验恰恰相反：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01f87673c19c1dddb6.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;然而，这里要指出的是，这不是苹果设备之间的比较。PWA根据需要加载新路由的代码，附加代码的成本在应用程序的生命周期内分摊。随后的导航仍然不会像下载应用程序那样花费太多的流量。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01ffdcb87387794156.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 渐进式网页应用在移动端的 Firefox, Edge 和 Safari 上。&lt;/p&gt;    &lt;h4&gt;基于路由的 JavaScript 打包组合&lt;/h4&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;只需加载用户需要的代码，就可以快速获取网页来加载和获取交互。 这减少了网络传输和JavaScript解析/编译时间。 非关键资源可以根据需要延迟加载。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest开始打破他们的几兆字节的JavaScript包，将其分成三个不同类别的webpack块，他们工作得很好：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01c11426461e75ab22.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;渲染块包含额外的依赖(react, redux, react-router, etc) ~ 73kb          &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;入口块包含渲染网页的主要代码（举例：通用模块，页面的外壳，redux 的 store） ~ 72KB&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;异步 路由块包含单独路由的代码 ~13–18KB&lt;/p&gt;        &lt;p&gt;          &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;网络瀑布的高亮显示了该如何逐步优化代码，从而避免了巨石般的打包：&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01504d86c44730f951.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;对于长期缓存，Pinterest也为每个文件名使用块特定的散列。&lt;/p&gt;    &lt;p&gt;Pinterest 使用 webpack 中的        &lt;a href="https://webpack.js.org/plugins/commons-chunk-plugin/"&gt;CommonsChunkPlugin&lt;/a&gt; 来分离出他们的可缓存的 vendor 包：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们也结合        &lt;a href="https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md"&gt;code-splitting&lt;/a&gt; 使用       &lt;a href="https://github.com/ReactTraining/react-router"&gt;React Router&lt;/a&gt; ：&lt;/p&gt;    &lt;h4&gt;对需要支持的浏览器使用 babel-preset-env 转换代码&lt;/h4&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 使用 Babel        &lt;a href="http://2ality.com/2017/02/babel-preset-env.html"&gt;babel-preset-env&lt;/a&gt; 转换所需支持的现代浏览器中不支持的 ES2015+ 特性。Pinterest 支持现代浏览器的最新两个版本，他们的 .babelrc 是这个样子的：&lt;/p&gt;    &lt;p&gt;还有进一步的优化，他们可以做，只有条件地满足需要的 polyfills (例如       &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl"&gt;Internationalization API&lt;/a&gt;)，但这是未来的计划。      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;使用Webpack Bundle Analyzer分析改进空间&lt;/p&gt;    &lt;p&gt;      &lt;a href="https://github.com/webpack-contrib/webpack-bundle-analyzer"&gt;Webpack Bundle Analyzer&lt;/a&gt; 是一个非常棒的工具，它能真正的了解你发给用户 JavaScript 包之间的依赖关系。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;下面，你能看到很多紫色，粉色和蓝色的块，它们在 Pinterest 中是最早加载输出的。这些是用于延迟加载路由的异步块。Webpack Bundle Analyzer 允许可视化大部分这些块包含重复的代码：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01b9271a861619fb4f.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Webpack Bundle Analyzer 帮助可视化所有块之间的这个问题的比例。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 使用块中的重复代码信息进行调用。他们将异步块中的重复代码移动到其主块。 它将入口块的大小增加了20％，但将所有延迟加载块的大小减少了90％！&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t011113456bf39fad53.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;图片优化&lt;/h4&gt;    &lt;p&gt;Pinterest PWA中大多数内容的延迟加载是由无限的       &lt;a href="https://masonry.desandro.com/"&gt;Masonry&lt;/a&gt; 网格来处理的。 它内置了对虚拟化的支持，并且只挂载在视口中的子项。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01053b3e0be2ab905a.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest 也使用渐进式加载技术来处理 PWA 中的图像。 占位符在初始化每个 Pin 时占主导地位。 Pin 的图像使用 Progressive JPEGs（渐进式JPEG），可在每次扫描时提高图像质量：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01549541e380b26193.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;React 性能痛点&lt;/h4&gt;    &lt;p&gt;Pinterest 用 React 处理了一些渲染性能问题，       &lt;a href="https://masonry.desandro.com/"&gt;Masonry&lt;/a&gt; 网格作为他们的一部分。 安装和卸载组件（如 Pins）的大树可能会很慢。Pin 中有很多东西：&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01b44f474e8c886005.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;虽然在写 Pinterest 的时候正在使用 React 15.5.4，但是他们希望        &lt;a href="https://reactjs.org/blog/2017/09/26/react-v16.0.html"&gt;React 16&lt;/a&gt; （Fiber）能够减少花费在卸载上的时间。 与此同时，虚拟化网格显着帮助了组件卸载时间。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;Pinterest还会限制 Pins 的插入，以便他们可以更快地测量/渲染第一个 Pins，但意味着设备CPU的整体工作量更大。&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;导航过渡&lt;/h4&gt;    &lt;p&gt;为了提高感知性，Pinterest 也更新独立于路由的导航栏图标的选定状态。 这使得从一个路由到另一个路由的导航不会因网络阻塞而感觉到缓慢。 用户在等待数据到达时快速获取可视化界面：&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01e7bab58e944de901.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;使用 Redux 的经验&lt;/h4&gt;    &lt;p&gt;Pinterest 对所有 API 数据使用        &lt;a href="https://github.com/paularmstrong/normalizr"&gt;normalizr&lt;/a&gt; (它根据模式规范化嵌套JSON) 。可以使用 Redux DevTools 中看到他们。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t0189941bd56090bf0c.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;这个过程的缺点是非规范化是缓慢的，所以他们最终严重依赖       &lt;a href="https://github.com/reactjs/reselect"&gt;reselect&lt;/a&gt; 的选择器模式来记忆渲染过程中的非规范化。 它们也总是在尽可能低的层次上进行非规范化处理，以确保单个更新不会导致大量的重新渲染。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;举个例子，他们的网格项目列表只是 Pin ID 与 Pin 组件非正常化本身。 如果任何给定 Pin 有变化，则完整的网格不必重新渲染。 权衡是在Pinterest PWA 中有很多 Redux 订阅，尽管这没有导致明显的性能问题。&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;通过 Service Workers 缓存资源&lt;/h4&gt;    &lt;p&gt;Pinteres t使用 Workbox 库来生成和管理他们的 Service Workers：&lt;/p&gt;    &lt;p&gt;今天，Pinterest 使用缓存优先策略缓存任何 JavaScript 或 CSS 包，并缓存其用户界面（应用程序外壳）。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t017a302a7f6cc4eebd.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;在缓存优先设置中，如果请求与缓存条目匹配，则以此作为响应。 否则，尝试从网络获取资源。 如果网络请求成功，更新缓存。 要了解更多有关使用 Service Worker 的缓存策略，请阅读       &lt;a href="https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/"&gt;Jake Archibald’s Offline Cookbook&lt;/a&gt; 的离线手册。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;它们为应用程序外壳（webpack的运行时，vendor 和入口块）加载的初始包定义了预缓存。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;由于 Pinterest 是具有全球影响力的网站，支持多种语言，因此它们还会生成每个语言环境的 Service Worker 配置，以便它们可以预先缓存语言环境包。 Pinterest 也使用 webpack 的命名块预缓存顶级异步路由包。&lt;/p&gt;    &lt;p&gt;这项工作是在几个较小的迭代中推出的。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;首先：Pinterest 的 Service Worker 只会在运行时缓存按需延迟加载的脚本。 这是利用V8的代码缓存，帮助跳过重复视图的一些分析/编译成本，使他们可以加载更快。 从 Service Worker 存在的高速缓存存储器提供的脚本可以及时的选择代码高速缓存，因为浏览器可能知道用户最终将在重复视图中使用这些资源。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01700857fb191420b0.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;之后，Pinterest 进行 预缓存他们的 vendor 和 入口块&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;接下来，Pinterest 开始 预缓存一些常用路由(像 主页， pin 页面，搜索页面 等)&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;最后，他们开始为每个语言环境生成一个 Service Worker，以便他们也可以缓存语言环境包。 这对于重复加载性能非常重要，同时也为大多数用户提供了基本的离线渲染功能。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;应用外壳的挑战&lt;/h4&gt;    &lt;p&gt;Pinterest发现实施他们的应用程序壳有点棘手。 由于桌面时代的假设，有多少数据可以通过有线连接发送，初始的有效负载很大，包含了很多非关键信息，如用户的实验组，用户信息，上下文信息等。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们不得不自问：“我们是否将这些内容缓存在应用程序外壳中？ 或者在渲染任何内容之前，先阻塞网络请求来获取它”。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们决定将其缓存在应用程序外壳中，这需要管理什么时候使应用程序外壳失效（注销，从设置等更新用户信息）。每个请求的响应都包含 appVersion — 如果应用程序版本发生变化，他们将取消注册 Service Worker，注册新的服务，然后在下一次路由更改时重新加载整个页面。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;将这些信息添加到应用程序外壳是有点棘手，但为了避免渲染被请求阻塞，是值得的。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;h4&gt;审计 和 Lighthouse&lt;/h4&gt;    &lt;p&gt;Pinterest 使用        &lt;a href="https://github.com/GoogleChrome/lighthouse/"&gt;Lighthouse&lt;/a&gt; 来验证他们的性能改进是正确的。 观察诸如“持续互动时间”等指标非常有用。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01cbb5484db6fa3bb6.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;明年，他们希望使用 Lighthouse 作为回归机制来验证页面加载速度是否保持快速。&lt;/p&gt;    &lt;h4&gt;      &lt;br /&gt;&lt;/h4&gt;    &lt;h4&gt;新特性&lt;/h4&gt;    &lt;p&gt;Pinterest 刚刚部署了支持 Web 推送消息的版本，并且在未登录（注销）的状态下的 PWA 也可以正常使用。&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="http://p0.qhimg.com/t01dea7dba90e18f1b0.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;他们有兴趣探索支持       &lt;a href="https://developers.google.com/web/fundamentals/performance/resource-prioritization"&gt;&amp;lt;link rel=preload&amp;gt;&lt;/a&gt; 预加载关键包并减少首次加载时向用户提供的未使用JavaScript的数量。 请继续关注未来更多精彩的演出！&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;      &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;原文链接：      &lt;a href="https://medium.com/dev-channel/a-pinterest-progressive-web-app-performance-case-study-3bd6ed2e6154"&gt;https://medium.com/dev-channel/a-pinterest-progressive-web-app-performance-case-study-3bd6ed2e6154&lt;/a&gt;      &lt;br /&gt;&lt;/p&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 />
      <guid isPermaLink="true">https://itindex.net/detail/57884-pinterest-%E7%BD%91%E9%A1%B5%E5%BA%94%E7%94%A8-%E6%80%A7%E8%83%BD</guid>
      <pubDate>Sun, 07 Jan 2018 17:55:03 CST</pubDate>
    </item>
    <item>
      <title>移动端网页布局注意事项及解决</title>
      <link>https://itindex.net/detail/57304-%E7%A7%BB%E5%8A%A8-%E7%BD%91%E9%A1%B5-%E5%B8%83%E5%B1%80</link>
      <description>1.winphone系统a、input标签被点击时产生的半透明灰色背景怎么去掉
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;meta name=&amp;quot;msapplication-tap-highlight&amp;quot; content=&amp;quot;no&amp;quot;&amp;gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;1、关闭iOS键盘首字母自动大写
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;input type=&amp;quot;text&amp;quot; autocapitalize=&amp;quot;off&amp;quot; /&amp;gt;
 &lt;br /&gt;
 &lt;br /&gt;2、禁止文本缩放
 &lt;br /&gt;
 &lt;br /&gt;html {
 &lt;br /&gt;-webkit-text-size-adjust: 100%;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;3、移动端如何清除输入框内阴影
 &lt;br /&gt;
 &lt;br /&gt;在iOS上，输入框默认有内部阴影，但无法使用 box-shadow 来清除，如果不需要阴影，可以这样关闭：
 &lt;br /&gt;
 &lt;br /&gt;input,
 &lt;br /&gt;textarea {
 &lt;br /&gt;border: 0;
 &lt;br /&gt;-webkit-appearance: none;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;4、忽略页面的数字为电话，忽略email识别
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;meta name=&amp;quot;format-detection&amp;quot; content=&amp;quot;telephone=no, email=no&amp;quot;/&amp;gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;5、快速回弹滚动
 &lt;br /&gt;
 &lt;br /&gt;.xxx {
 &lt;br /&gt;overflow: auto;
 &lt;br /&gt;-webkit-overflow-scrolling: touch;
 &lt;br /&gt;}
 &lt;br /&gt;PS：iScroll用过之后感觉不是很好，有一些诡异的bug，这里推荐另外一个 iDangero Swiper，这个插件集成了滑屏滚动的强大功能（支持3D），而且还有回弹滚动的内置滚动条，官方地址：
 &lt;br /&gt;
 &lt;br /&gt;http://www.idangero.us/sliders/swiper/index.php
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;6、移动端禁止选中内容
 &lt;br /&gt;
 &lt;br /&gt;div {
 &lt;br /&gt;-webkit-user-select: none;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;7、移动端取消touch高亮效果
 &lt;br /&gt;
 &lt;br /&gt;在做移动端页面时，会发现所有a标签在触发点击时或者所有设置了伪类 :active 的元素，默认都会在激活状态时，显示高亮框，如果不想要这个高亮，那么你可以通过css以下方法来禁止：
 &lt;br /&gt;
 &lt;br /&gt;.xxx {
 &lt;br /&gt;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;8、如何禁止保存或拷贝图像
 &lt;br /&gt;
 &lt;br /&gt;通常当你在手机或者pad上长按图像 img ，会弹出选项 存储图像 或者 拷贝图像，如果你不想让用户这么操作，那么你可以通过以下方法来禁止：
 &lt;br /&gt;
 &lt;br /&gt;img {
 &lt;br /&gt;-webkit-touch-callout: none;
 &lt;br /&gt;}
 &lt;br /&gt;PS：需要注意的是，该方法只在 iOS 上有效。
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;9、解决字体在移动端比例缩小后出现锯齿的问题：
 &lt;br /&gt;
 &lt;br /&gt;-webkit-font-smoothing: antialiased;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;10、栅格布局：
 &lt;br /&gt;
 &lt;br /&gt;box-sizing:border-box;可以改变盒子模型的计算方式方便你设置宽进行自适应流式布局
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;11、input[type=input]{-webkit-appearance:none;}移除ios的样式，但这个属性存在bug，会导致iso无法获取checkbox值，给这个元素重新赋上input[type=checkbox]{-webkit-appearance:checkbox;}就不会报错了。
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;12、按钮被按下效果的实现需要给a标签加a:active属性和添加一段空函数
 &lt;br /&gt;
 &lt;br /&gt;document.body.addEventListener(&amp;apos;touchend&amp;apos;, function () { });
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;13、解决去掉下边框：
 &lt;br /&gt;
 &lt;br /&gt;-webkit-border-bottom:none;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;14、英文文本换行(不拆分单词)：
 &lt;br /&gt;
 &lt;br /&gt;word-wrap:break-word
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;15、字体大小尽量使用pt或者em，rem，代替px。
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;16、设置input里面placeholder字体的大小
 &lt;br /&gt;
 &lt;br /&gt;::-webkit-input-placeholder{ font-size:10pt;}
 &lt;br /&gt;17、wap页面有img标签，记得加上display：block；属性来解决img的边缘空白间隙的1px像素。如果图片要适应不同的手机要设置width:100%;而且不能添加高度。
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;18. 移动端如何清除输入框内阴影
 &lt;br /&gt;
 &lt;br /&gt;在iOS上，输入框默认有内部阴影，但无法使用 box-shadow 来清除，如果不需要阴影，可以这样关闭：
 &lt;br /&gt;
 &lt;br /&gt;input,
 &lt;br /&gt;textarea {
 &lt;br /&gt;border: 0;
 &lt;br /&gt;-webkit-appearance: none;
 &lt;br /&gt;}
 &lt;br /&gt;19. 移动端禁止选中内容
 &lt;br /&gt;
 &lt;br /&gt;如果你不想用户可以选中页面中的内容，那么你可以在css中禁掉：
 &lt;br /&gt;
 &lt;br /&gt;.user-select-none {
 &lt;br /&gt;-webkit-user-select: none;
 &lt;br /&gt;-moz-user-select: none;
 &lt;br /&gt;-ms-user-select: none;
 &lt;br /&gt;}
 &lt;br /&gt;兼容IE6-9的写法：onselectstart=&amp;quot;return false;&amp;quot; unselectable=&amp;quot;on&amp;quot;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;20.audio元素和video元素在ios和andriod中无法自动播放
 &lt;br /&gt;
 &lt;br /&gt;应对方案：触屏即播
 &lt;br /&gt;
 &lt;br /&gt;$(&amp;apos;html&amp;apos;).one(&amp;apos;touchstart&amp;apos;,function(){
 &lt;br /&gt;audio.play()
 &lt;br /&gt;})
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;21.手机拍照和上传图片
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;input type=&amp;quot;file&amp;quot;&amp;gt;的accept 属性
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;!-- 选择照片 --&amp;gt;
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;input type=file accept=&amp;quot;image/*&amp;quot;&amp;gt;
 &lt;br /&gt;&amp;lt;!-- 选择视频 --&amp;gt;
 &lt;br /&gt;
 &lt;br /&gt;&amp;lt;input type=file accept=&amp;quot;video/*&amp;quot;&amp;gt;
 &lt;br /&gt;ios 有拍照、录像、选取本地图片功能
 &lt;br /&gt;
 &lt;br /&gt;部分android只有选取本地图片功能
 &lt;br /&gt;
 &lt;br /&gt;winphone不支持
 &lt;br /&gt;
 &lt;br /&gt;input控件默认外观丑陋
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;22. 消除transition闪屏
 &lt;br /&gt;
 &lt;br /&gt;.css{
 &lt;br /&gt;-webkit-transform-style: preserve-3d;
 &lt;br /&gt;-webkit-backface-visibility: hidden;
 &lt;br /&gt;}
 &lt;br /&gt;23.开启硬件加速   解决页面闪白   保证动画流畅
 &lt;br /&gt;
 &lt;br /&gt;.css {
 &lt;br /&gt;-webkit-transform: translate3d(0, 0, 0);
 &lt;br /&gt;-moz-transform: translate3d(0, 0, 0);
 &lt;br /&gt;-ms-transform: translate3d(0, 0, 0);
 &lt;br /&gt;transform: translate3d(0, 0, 0);
 &lt;br /&gt;}
 &lt;br /&gt;设计高性能CSS3动画的几个要素
 &lt;br /&gt;
 &lt;br /&gt;尽可能地使用合成属性transform和opacity来设计CSS3动画，
 &lt;br /&gt;
 &lt;br /&gt;不使用position的left和top来定位
 &lt;br /&gt;
 &lt;br /&gt;利用translate3D开启GPU加速
 &lt;br /&gt;
 &lt;br /&gt;**************************************************************************
 &lt;br /&gt;
 &lt;br /&gt;框架
 &lt;br /&gt;
 &lt;br /&gt;1. 移动端基础框架
 &lt;br /&gt;
 &lt;br /&gt;zepto.js 语法与jquery几乎一样，会jquery基本会zepto~
 &lt;br /&gt;
 &lt;br /&gt;iscroll.js 解决页面不支持弹性滚动，不支持fixed引起的问题~ 实现下拉刷新，滑屏，缩放等功能~
 &lt;br /&gt;
 &lt;br /&gt;underscore.js 该库提供了一整套函数式编程的实用功能，但是没有扩展任何JavaScript内置对象。
 &lt;br /&gt;
 &lt;br /&gt;fastclick 加快移动端点击响应时间
 &lt;br /&gt;
 &lt;br /&gt;animate.css CSS3动画效果库
 &lt;br /&gt;
 &lt;br /&gt;Normalize.css Normalize.css是一种现代的、CSS reset为HTML5准备的优质替代方案
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;2. 滑屏框架
 &lt;br /&gt;
 &lt;br /&gt;适合上下滑屏、左右滑屏等滑屏切换页面的效果
 &lt;br /&gt;
 &lt;br /&gt;slip.js
 &lt;br /&gt;
 &lt;br /&gt;iSlider.js
 &lt;br /&gt;
 &lt;br /&gt;fullpage.js
 &lt;br /&gt;
 &lt;br /&gt;swiper
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;3.瀑布流框架
 &lt;br /&gt;
 &lt;br /&gt;masonry
 &lt;br /&gt;
 &lt;br /&gt;工具推荐
 &lt;br /&gt;
 &lt;br /&gt;caniuse 各浏览器支持html5属性查询
 &lt;br /&gt;
 &lt;br /&gt;paletton 调色搭配
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;对于网站字体设置
 &lt;br /&gt;
 &lt;br /&gt;移动端项目：
 &lt;br /&gt;
 &lt;br /&gt;font-family:Tahoma,Arial,Roboto,&amp;quot;Droid Sans&amp;quot;,&amp;quot;Helvetica Neue&amp;quot;,&amp;quot;Droid Sans Fallback&amp;quot;,&amp;quot;Heiti SC&amp;quot;,sans-self;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;移动和pc端项目：
 &lt;br /&gt;
 &lt;br /&gt;font-family:Tahoma,Arial,Roboto,&amp;quot;Droid Sans&amp;quot;,&amp;quot;Helvetica Neue&amp;quot;,&amp;quot;Droid Sans Fallback&amp;quot;,&amp;quot;Heiti SC&amp;quot;,&amp;quot;Hiragino Sans GB&amp;quot;,Simsun,sans-self;
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;有关Flexbox弹性盒子布局一些属性
 &lt;br /&gt;
 &lt;br /&gt;不定宽高的水平垂直居中
 &lt;br /&gt;
 &lt;br /&gt;.xxx{
 &lt;br /&gt;position:absolute;
 &lt;br /&gt;top:50%;
 &lt;br /&gt;left:50%;
 &lt;br /&gt;z-index:3;
 &lt;br /&gt;-webkit-transform:translate(-50%，-50%)；
 &lt;br /&gt;border-radius:6px;
 &lt;br /&gt;background:#fff;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;[flexbox版]不定宽高的水平垂直居中
 &lt;br /&gt;
 &lt;br /&gt;.xx{
 &lt;br /&gt;justify-content:center;//子元素水平居中，
 &lt;br /&gt;align-items:center;//子元素垂直居中;
 &lt;br /&gt;display:-webkit-flex;
 &lt;br /&gt;}
 &lt;br /&gt;//单行文本溢出
 &lt;br /&gt;
 &lt;br /&gt;.xx{
 &lt;br /&gt;overflow:hidden;
 &lt;br /&gt;white-space:nowrap;
 &lt;br /&gt;text-overflow:ellipsis;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;多行文本溢出
 &lt;br /&gt;
 &lt;br /&gt;.xx{
 &lt;br /&gt;display:-webkit-box !importmort;
 &lt;br /&gt;overflow:hidden;
 &lt;br /&gt;text-overflow:ellipsis;
 &lt;br /&gt;word-break:break-all;
 &lt;br /&gt;-webkit-box-orient:vertical;
 &lt;br /&gt;-webkit-line-clamp:2;(数字2表示隐藏两行)
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;使用流体图片
 &lt;br /&gt;
 &lt;br /&gt;img{
 &lt;br /&gt;width:100%;
 &lt;br /&gt;height:auto;
 &lt;br /&gt;width:auto\9;
 &lt;br /&gt;}
 &lt;br /&gt;一像素边框（例子:移动端列表的下边框）
 &lt;br /&gt;
 &lt;br /&gt;.list-iteam:after{
 &lt;br /&gt;position: absolute;
 &lt;br /&gt;left: 0px;
 &lt;br /&gt;right: 0px;
 &lt;br /&gt;content: &amp;apos;&amp;apos;;
 &lt;br /&gt;height: 1px;
 &lt;br /&gt;transform: scaleY(0.5);
 &lt;br /&gt;-moz-transform: scaleY(0.5);
 &lt;br /&gt;-webkit-transform:scaleY(0.5);
 &lt;br /&gt;
 &lt;br /&gt;}
 &lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;针对适配等比缩放的方法:
 &lt;br /&gt;
 &lt;br /&gt;@media only screen and (min-width: 1024px){
 &lt;br /&gt;body{zoom:3.2;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 768px) and (max-width: 1023px) {
 &lt;br /&gt;body{zoom:2.4;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 640px) and (max-width: 767px) {
 &lt;br /&gt;body{zoom:2;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 540px) and (max-width: 639px) {
 &lt;br /&gt;body{zoom:1.68;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 480px) and (max-width: 539px) {
 &lt;br /&gt;body{zoom:1.5;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 414px) and (max-width: 479px) {
 &lt;br /&gt;body{zoom:1.29;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 400px) and (max-width: 413px) {
 &lt;br /&gt;body{zoom:1.25;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 375px) and (max-width: 413px) {
 &lt;br /&gt;body{zoom:1.17;}
 &lt;br /&gt;}
 &lt;br /&gt;@media only screen and (min-width: 360px) and (max-width:374px) {
 &lt;br /&gt;body{zoom:1.125;}
 &lt;br /&gt;}
          
           &lt;br /&gt; &lt;br /&gt;
          
             &lt;a href="http://wyr123789.iteye.com/blog/2387944#comments"&gt;已有   &lt;strong&gt;0&lt;/strong&gt; 人发表留言，猛击-&amp;gt;&amp;gt;  &lt;strong&gt;这里&lt;/strong&gt;&amp;lt;&amp;lt;-参与讨论&lt;/a&gt;
          
           &lt;br /&gt; &lt;br /&gt; &lt;br /&gt;
ITeye推荐
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="http://www.iteye.com/clicks/433" target="_blank"&gt;—软件人才免语言低担保 赴美带薪读研！— &lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;br /&gt; &lt;br /&gt; &lt;br /&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 />
      <guid isPermaLink="true">https://itindex.net/detail/57304-%E7%A7%BB%E5%8A%A8-%E7%BD%91%E9%A1%B5-%E5%B8%83%E5%B1%80</guid>
      <pubDate>Tue, 01 Aug 2017 17:10:41 CST</pubDate>
    </item>
    <item>
      <title>抓取网页内容生成Kindle电子书</title>
      <link>https://itindex.net/detail/57199-%E7%BD%91%E9%A1%B5-kindle-%E7%94%B5%E5%AD%90%E4%B9%A6</link>
      <description>&lt;p&gt;自从买了kindle后，总是想着如何最大效用发挥其效用。虽然多看上有很多书可以购买，网上也有很多免费的电子书，但是仍然有很多感兴趣的内容是以网页的形式存在的。例如  &lt;a href="http://chimera.labs.oreilly.com/books/"&gt;O’Reilly Atlas&lt;/a&gt;就提供了诸多电子书，但是只提供免费的在线阅读；另外还有很多资料或文档都只有网页形式。于是就希望通过某种方法讲这些在线资料转为epub或mobi格式，以便在kindle上阅读。这篇文章介绍了如何借助calibre并编写少量代码来达到这个目的。&lt;/p&gt;
 &lt;h1&gt;Calibre&lt;/h1&gt;
 &lt;h2&gt;Calibre简介&lt;/h2&gt;
 &lt;p&gt;  &lt;a href="http://calibre-ebook.com/"&gt;Calibre&lt;/a&gt;是一个免费的电子书管理工具，可以兼容Windows, OS X及Linux，令人欣喜的是，除了GUI外，calibre还提供了诸多命令行工具，其中的ebook-convert命令可以根据用户编写的recipes文件（实际是python代码）抓取指定页面内容并生成mobi等格式的电子书。通过编写recipes可以自定制抓取行为，以适应不同的网页结构。&lt;/p&gt;
 &lt;h2&gt;安装Calibre&lt;/h2&gt;
 &lt;p&gt;Calibre的下载地址是  &lt;a href="http://calibre-ebook.com/download"&gt;http://calibre-ebook.com/download&lt;/a&gt;，可以根据自己的操作系统下载相应的安装程序。&lt;/p&gt;
 &lt;p&gt;如果是Linux操作系统，还可以通过软件仓库安装：&lt;/p&gt;
 &lt;p&gt;Archlinux：&lt;/p&gt;
 &lt;pre&gt;pacman -S calibre
&lt;/pre&gt;
 &lt;p&gt;Debian/Ubuntu：&lt;/p&gt;
 &lt;pre&gt;apt-get install calibre
&lt;/pre&gt;
 &lt;p&gt;RedHat/Fedora/CentOS：&lt;/p&gt;
 &lt;pre&gt;yum -y install calibre
&lt;/pre&gt;
 &lt;p&gt;注意，如果你使用OSX，需要单独安装  &lt;a href="http://manual.calibre-ebook.com/cli/cli-index.html"&gt;Command Line Tool&lt;/a&gt;。&lt;/p&gt;
 &lt;h1&gt;抓取网页生成电子书&lt;/h1&gt;
 &lt;p&gt;下面以  &lt;a href="http://chimera.labs.oreilly.com/books/1230000000561"&gt;Git Pocket Guide&lt;/a&gt;为例，说明如何通过calibre从网页生成电子书。&lt;/p&gt;
 &lt;h2&gt;找到index页&lt;/h2&gt;
 &lt;p&gt;要抓取整本书，第一件事就是找到index页，这个页面一般是Table of Contents，也就是目录页，其中每个目录项链接到相应内容页。index页将会指导抓取哪些页面以及生成电子书时内容组织顺序。在这个例子中，index页面是  &lt;a href="http://chimera.labs.oreilly.com/books/1230000000561/index.html"&gt;http://chimera.labs.oreilly.com/books/1230000000561/index.html&lt;/a&gt;。&lt;/p&gt;
 &lt;h2&gt;编写recipes&lt;/h2&gt;
 &lt;p&gt;Recipes是一个以recipe为扩展名的脚本，内容实际上是一段python代码，用来定义calibre抓取页面的范围和行为，下面是用于抓取Git Pocket Guide的recipes：&lt;/p&gt;
 &lt;pre&gt;from calibre.web.feeds.recipes import BasicNewsRecipe

class Git_Pocket_Guide(BasicNewsRecipe):

    title = &amp;apos;Git Pocket Guide&amp;apos;
    description = &amp;apos;&amp;apos;
    cover_url = &amp;apos;http://akamaicovers.oreilly.com/images/0636920024972/lrg.jpg&amp;apos;

    url_prefix = &amp;apos;http://chimera.labs.oreilly.com/books/1230000000561/&amp;apos;
    no_stylesheets = True
    keep_only_tags = [{ &amp;apos;class&amp;apos;: &amp;apos;chapter&amp;apos; }]

    def get_title(self, link):
        return link.contents[0].strip()

    def parse_index(self):
        soup = self.index_to_soup(self.url_prefix + &amp;apos;index.html&amp;apos;)

        div = soup.find(&amp;apos;div&amp;apos;, { &amp;apos;class&amp;apos;: &amp;apos;toc&amp;apos; })

        articles = []
        for link in div.findAll(&amp;apos;a&amp;apos;):
            if &amp;apos;#&amp;apos; in link[&amp;apos;href&amp;apos;]:
                continue

            if not &amp;apos;ch&amp;apos; in link[&amp;apos;href&amp;apos;]:
                continue

            til = self.get_title(link)
            url = self.url_prefix + link[&amp;apos;href&amp;apos;]
            a = { &amp;apos;title&amp;apos;: til, &amp;apos;url&amp;apos;: url }

            articles.append(a)

        ans = [(&amp;apos;Git_Pocket_Guide&amp;apos;, articles)]

        return ans
&lt;/pre&gt;
 &lt;p&gt;下面分别解释代码中不同部分。&lt;/p&gt;
 &lt;h3&gt;总体结构&lt;/h3&gt;
 &lt;p&gt;总体来看，一个recipes就是一个python class，只不过这个class必须继承calibre.web.feeds.recipes.BasicNewsRecipe。&lt;/p&gt;
 &lt;h3&gt;parse_index&lt;/h3&gt;
 &lt;p&gt;整个recipes的核心方法是parse_index，也是recipes唯一必须实现的方法。这个方法的目标是通过分析index页面的内容，返回一个稍显复杂的数据结构（稍后介绍），这个数据结构定义了整个电子书的内容及内容组织顺序。&lt;/p&gt;
 &lt;h3&gt;总体属性设置&lt;/h3&gt;
 &lt;p&gt;在class的开始，定义了一些全局属性：&lt;/p&gt;
 &lt;pre&gt;title = &amp;apos;Git Pocket Guide&amp;apos;
description = &amp;apos;&amp;apos;
cover_url = &amp;apos;http://akamaicovers.oreilly.com/images/0636920024972/lrg.jpg&amp;apos;

url_prefix = &amp;apos;http://chimera.labs.oreilly.com/books/1230000000561/&amp;apos;
no_stylesheets = True
keep_only_tags = [{ &amp;apos;class&amp;apos;: &amp;apos;chapter&amp;apos; }]
&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;title：电子书标题&lt;/li&gt;
  &lt;li&gt;description：电子书描述&lt;/li&gt;
  &lt;li&gt;cover_url：电子书的封面图片&lt;/li&gt;
  &lt;li&gt;url_prefix：这是我自用的属性，是内容页面的前缀，用于后面拼装内容页的完整url&lt;/li&gt;
  &lt;li&gt;no_stylesheets：不要使用页面CSS样式&lt;/li&gt;
  &lt;li&gt;keep_only_tags：这一行告诉calibre分析index页时仅考虑class属性为“chapter”的DOM元素，如果你看index页的源码会发现这对应一级标题。之所以这样是因为在这个例子中，index页面每个一级标题对应一个独立内容页，而二级标题仅链接到页面中某个锚点（anchor），所以仅需考虑一级标题&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;parse_index返回值&lt;/h3&gt;
 &lt;p&gt;下面介绍parse_index需要通过分析index页面返回的数据结构。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/01.png"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;总体返回数据结构是一个list，其中每个元素是一个tuple，一个tuple表示一卷（volume）。在这个例子中只有一卷，所以list中只有一个tuple。&lt;/p&gt;
 &lt;p&gt;每个tuple有两个元素，第一个元素是卷名，第二个元素是一个list，list中每个元素是一个map，表示一章（chapter），map中有两个元素：title和url，title是章节标题，url是章节所在内容页的url。&lt;/p&gt;
 &lt;p&gt;Calibre会根据parse_index的返回结果抓取并组织整个书，并且会自行抓取并处理内容中外链的图片。&lt;/p&gt;
 &lt;p&gt;整个parse_index使用soup解析index页并生成上述数据结构。&lt;/p&gt;
 &lt;h3&gt;更多&lt;/h3&gt;
 &lt;p&gt;上面是最基本的recipes，想了解更多的使用方法，可以参考  &lt;a href="http://manual.calibre-ebook.com/news_recipe.html"&gt;API文档&lt;/a&gt;。&lt;/p&gt;
 &lt;h2&gt;生成mobi&lt;/h2&gt;
 &lt;p&gt;编写好recipes后，在命令行下通过如下命令即可生成电子书：&lt;/p&gt;
 &lt;pre&gt;ebook-convert Git_Pocket_Guide.recipe Git_Pocket_Guide.mobi
&lt;/pre&gt;
 &lt;p&gt;即可生成mobi格式的电子书。ebook-convert会根据recipes代码自行抓取相关内容并组织结构。&lt;/p&gt;
 &lt;h1&gt;最终效果&lt;/h1&gt;
 &lt;p&gt;下面是在kindle上看到的效果。&lt;/p&gt;
 &lt;p&gt;目录&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/02.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;内容一&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/03.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;内容二&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/04.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;含有图片的页&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/05.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;实际效果&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://blog.codinglabs.org/uploads/pictures/convert-html-to-kindle-book/06.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h1&gt;我的recipes仓库&lt;/h1&gt;
 &lt;p&gt;我在github上建了一个  &lt;a href="https://github.com/ericzhang-cn/kindle-open-books/"&gt;kindle-open-books&lt;/a&gt;，里面放了一些recipes，有我写的，也有其他同学贡献的。欢迎任何人贡献的recipes。&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 />
      <guid isPermaLink="true">https://itindex.net/detail/57199-%E7%BD%91%E9%A1%B5-kindle-%E7%94%B5%E5%AD%90%E4%B9%A6</guid>
      <pubDate>Thu, 27 Mar 2014 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>网页性能管理详解</title>
      <link>https://itindex.net/detail/54347-%E7%BD%91%E9%A1%B5-%E6%80%A7%E8%83%BD-%E7%AE%A1%E7%90%86</link>
      <description>&lt;p&gt;你遇到过性能很差的网页吗？&lt;/p&gt;

         &lt;p&gt;这种网页响应非常缓慢，占用大量的CPU和内存，浏览起来常常有卡顿，页面的动画效果也不流畅。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091508.jpg" title=""&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;h2&gt;一、网页生成的过程&lt;/h2&gt;

 &lt;p&gt;要理解网页性能为什么不好，就要了解网页是怎么生成的。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091501.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;网页的生成过程，大致可以分成五步。&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ol start="1"&gt;
   &lt;li&gt;HTML代码转化成DOM&lt;/li&gt;
   &lt;li&gt;CSS代码转化成CSSOM（CSS Object Model）&lt;/li&gt;
   &lt;li&gt;结合DOM和CSSOM，生成一棵渲染树（包含每个节点的视觉信息）&lt;/li&gt;
   &lt;li&gt;生成布局（layout），即将所有渲染树的所有节点进行平面合成&lt;/li&gt;
   &lt;li&gt;将布局绘制（paint）在屏幕上&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

 &lt;p&gt;这五步里面，第一步到第三步都非常快，耗时的是第四步和第五步。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;&amp;quot;生成布局&amp;quot;（flow）和&amp;quot;绘制&amp;quot;（paint）这两步，合称为&amp;quot;渲染&amp;quot;（render）。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091502.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;二、重排和重绘&lt;/h2&gt;

 &lt;p&gt;  &lt;strong&gt;网页生成的时候，至少会渲染一次。用户访问的过程中，还会不断重新渲染。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;以下三种情况，会导致网页重新渲染。&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;修改DOM&lt;/li&gt;
   &lt;li&gt;修改样式表&lt;/li&gt;
   &lt;li&gt;用户事件（比如鼠标悬停、页面滚动、输入框键入文字、改变窗口大小等等）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;p&gt;  &lt;strong&gt;重新渲染，就需要重新生成布局和重新绘制。前者叫做&amp;quot;重排&amp;quot;（reflow），后者叫做&amp;quot;重绘&amp;quot;（repaint）。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;需要注意的是，  &lt;strong&gt;&amp;quot;重绘&amp;quot;不一定需要&amp;quot;重排&amp;quot;&lt;/strong&gt;，比如改变某个网页元素的颜色，就只会触发&amp;quot;重绘&amp;quot;，不会触发&amp;quot;重排&amp;quot;，因为布局没有改变。但是，  &lt;strong&gt;&amp;quot;重排&amp;quot;必然导致&amp;quot;重绘&amp;quot;&lt;/strong&gt;，比如改变一个网页元素的位置，就会同时触发&amp;quot;重排&amp;quot;和&amp;quot;重绘&amp;quot;，因为布局改变了。&lt;/p&gt;

 &lt;h2&gt;三、对于性能的影响&lt;/h2&gt;

 &lt;p&gt;重排和重绘会不断触发，这是不可避免的。但是，它们非常耗费资源，是导致网页性能低下的根本原因。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;提高网页性能，就是要降低&amp;quot;重排&amp;quot;和&amp;quot;重绘&amp;quot;的频率和成本，尽量少触发重新渲染。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;前面提到，DOM变动和样式变动，都会触发重新渲染。但是，浏览器已经很智能了，会尽量把所有的变动集中在一起，排成一个队列，然后一次性执行，尽量避免多次重新渲染。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
div.style.color = &amp;apos;blue&amp;apos;;
div.style.marginTop = &amp;apos;30px&amp;apos;;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，div元素有两个样式变动，但是浏览器只会触发一次重排和重绘。&lt;/p&gt;

 &lt;p&gt;如果写得不好，就会触发两次重排和重绘。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
div.style.color = &amp;apos;blue&amp;apos;;
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + &amp;apos;px&amp;apos;;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码对div元素设置背景色以后，第二行要求浏览器给出该元素的位置，所以浏览器不得不立即重排。&lt;/p&gt;

 &lt;p&gt;一般来说，样式的写操作之后，如果有下面这些属性的读操作，都会引发浏览器立即重新渲染。&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;offsetTop/offsetLeft/offsetWidth/offsetHeight&lt;/li&gt;
   &lt;li&gt;scrollTop/scrollLeft/scrollWidth/scrollHeight&lt;/li&gt;
   &lt;li&gt;clientTop/clientLeft/clientWidth/clientHeight&lt;/li&gt;
   &lt;li&gt;getComputedStyle()&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;p&gt;所以，从性能角度考虑，尽量不要把读操作和写操作，放在一个语句里面。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
// bad
div.style.left = div.offsetLeft + 10 + &amp;quot;px&amp;quot;;
div.style.top = div.offsetTop + 10 + &amp;quot;px&amp;quot;;

// good
var left = div.offsetLeft;
var top  = div.offsetTop;
div.style.left = left + 10 + &amp;quot;px&amp;quot;;
div.style.top = top + 10 + &amp;quot;px&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;一般的规则是：&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;样式表越简单，重排和重绘就越快。&lt;/li&gt;
   &lt;li&gt;重排和重绘的DOM元素层级越高，成本就越高。&lt;/li&gt;
   &lt;li&gt;table元素的重排和重绘成本，要高于div元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;h2&gt;四、提高性能的九个技巧&lt;/h2&gt;

 &lt;p&gt;有一些技巧，可以降低浏览器重新渲染的频率和成本。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第一条&lt;/strong&gt;是上一节说到的，DOM 的多个读操作（或多个写操作），应该放在一起。不要两个读操作之间，加入一个写操作。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第二条&lt;/strong&gt;，如果某个样式是通过重排得到的，那么最好缓存结果。避免下一次用到的时候，浏览器又要重排。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第三条&lt;/strong&gt;，不要一条条地改变样式，而要通过改变class，或者csstext属性，一次性地改变样式。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
// bad
var left = 10;
var top = 10;
el.style.left = left + &amp;quot;px&amp;quot;;
el.style.top  = top  + &amp;quot;px&amp;quot;;

// good 
el.className += &amp;quot; theclassname&amp;quot;;

// good
el.style.cssText += &amp;quot;; left: &amp;quot; + left + &amp;quot;px; top: &amp;quot; + top + &amp;quot;px;&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;  &lt;strong&gt;第四条&lt;/strong&gt;，尽量使用离线DOM，而不是真实的网面DOM，来改变元素样式。比如，操作Document Fragment对象，完成后再把这个对象加入DOM。再比如，使用 cloneNode() 方法，在克隆的节点上进行操作，然后再用克隆的节点替换原始节点。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第五条&lt;/strong&gt;，先将元素设为 display: none （需要1次重排和重绘），然后对这个节点进行100次操作，最后再恢复显示（需要1次重排和重绘）。这样一来，你就用两次重新渲染，取代了可能高达100次的重新渲染。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第六条&lt;/strong&gt;，position属性为absolute或fixed的元素，重排的开销会比较小，因为不用考虑它对其他元素的影响。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第七条&lt;/strong&gt;，只在必要的时候，才将元素的display属性为可见，因为不可见的元素不影响重排和重绘。另外，visibility : hidden 的元素只对重排有影响，不影响重绘。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第八条&lt;/strong&gt;，使用虚拟DOM的脚本库，比如React等。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;第九条&lt;/strong&gt;，使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法调节重新渲染（详见后文）。&lt;/p&gt;

 &lt;h2&gt;五、刷新率&lt;/h2&gt;

 &lt;p&gt;很多时候，密集的重新渲染是无法避免的，比如scroll事件的回调函数和网页动画。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;网页动画的每一帧（frame）都是一次重新渲染。&lt;/strong&gt;每秒低于24帧的动画，人眼就能感受到停顿。  &lt;strong&gt;一般的网页动画，需要达到每秒30帧到60帧的频率，才能比较流畅。&lt;/strong&gt;如果能达到每秒70帧甚至80帧，就会极其流畅。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091509.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;大多数显示器的刷新频率是60Hz，为了与系统一致，以及节省电力，浏览器会自动按照这个频率，刷新动画（如果可以做到的话）。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091510.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;所以，如果网页动画能够做到每秒60帧，就会跟显示器同步刷新，达到最佳的视觉效果。这意味着，  &lt;strong&gt;一秒之内进行60次重新渲染，每次重新渲染的时间不能超过16.66毫秒。&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091511.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;一秒之间能够完成多少次重新渲染，这个指标就被称为&amp;quot;刷新率&amp;quot;，英文为FPS（frame per second）。&lt;/strong&gt;60次重新渲染，就是60FPS。&lt;/p&gt;

 &lt;h2&gt;六、开发者工具的Timeline面板&lt;/h2&gt;

 &lt;p&gt;Chrome浏览器开发者工具的Timeline面板，是查看&amp;quot;刷新率&amp;quot;的最佳工具。这一节介绍如何使用这个工具。&lt;/p&gt;

 &lt;p&gt;首先，按下 F12 打开&amp;quot;开发者工具&amp;quot;，切换到Timeline面板。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091512.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;左上角有一个灰色的圆点，这是录制按钮，按下它会变成红色。然后，在网页上进行一些操作，再按一次按钮完成录制。&lt;/p&gt;

 &lt;p&gt;Timeline面板提供两种查看方式：横条的是&amp;quot;事件模式&amp;quot;（Event Mode），显示重新渲染的各种事件所耗费的时间；竖条的是&amp;quot;帧模式&amp;quot;（Frame Mode），显示每一帧的时间耗费在哪里。&lt;/p&gt;

 &lt;p&gt;先看&amp;quot;事件模式&amp;quot;，你可以从中判断，性能问题发生在哪个环节，是JavaScript的执行，还是渲染？&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091514.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;不同的颜色表示不同的事件。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091503.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;蓝色：网络通信和HTML解析&lt;/li&gt;
   &lt;li&gt;黄色：JavaScript执行&lt;/li&gt;
   &lt;li&gt;紫色：样式计算和布局，即重排&lt;/li&gt;
   &lt;li&gt;绿色：重绘&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;p&gt;哪种色块比较多，就说明性能耗费在那里。色块越长，问题越大。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091515.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091516.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;帧模式（Frames mode）用来查看单个帧的耗时情况。每帧的色柱高度越低越好，表示耗时少。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091505.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;你可以看到，帧模式有两条水平的参考线。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091517.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;下面的一条是60FPS，低于这条线，可以达到每秒60帧；上面的一条是30FPS，低于这条线，可以达到每秒30次渲染。如果色柱都超过30FPS，这个网页就有性能问题了。&lt;/p&gt;

 &lt;p&gt;此外，还可以查看某个区间的耗时情况。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091518.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;或者点击每一帧，查看该帧的时间构成。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015091519.png" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;七、window.requestAnimationFrame()&lt;/h2&gt;

 &lt;p&gt;有一些JavaScript方法可以调节重新渲染，大幅提高网页性能。&lt;/p&gt;

 &lt;p&gt;其中最重要的，就是 window.requestAnimationFrame() 方法。它可以将某些代码放到下一次重新渲染时执行。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
function doubleHeight(element) {
  var currentHeight = element.clientHeight;
  element.style.height = (currentHeight * 2) + &amp;apos;px&amp;apos;;
}
elements.forEach(doubleHeight);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面的代码使用循环操作，将每个元素的高度都增加一倍。可是，每次循环都是，读操作后面跟着一个写操作。这会在短时间内触发大量的重新渲染，显然对于网页性能很不利。&lt;/p&gt;

 &lt;p&gt;我们可以使用  &lt;code&gt;window.requestAnimationFrame()&lt;/code&gt;，让读操作和写操作分离，把所有的写操作放到下一次重新渲染。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
function doubleHeight(element) {
  var currentHeight = element.clientHeight;
  window.requestAnimationFrame(function () {
    element.style.height = (currentHeight * 2) + &amp;apos;px&amp;apos;;
  });
}
elements.forEach(doubleHeight);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;页面滚动事件（scroll）的监听函数，就很适合用 window.requestAnimationFrame() ，推迟到下一次重新渲染。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
$(window).on(&amp;apos;scroll&amp;apos;, function() {
   window.requestAnimationFrame(scrollHandler);
});
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;当然，最适用的场合还是网页动画。下面是一个旋转动画的例子，元素每一帧旋转1度。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
var rAF = window.requestAnimationFrame;

var degrees = 0;
function update() {
  div.style.transform = &amp;quot;rotate(&amp;quot; + degrees + &amp;quot;deg)&amp;quot;;
  console.log(&amp;apos;updated to degrees &amp;apos; + degrees);
  degrees = degrees + 1;
  rAF(update);
}
rAF(update);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;h2&gt;八、window.requestIdleCallback()&lt;/h2&gt;

 &lt;p&gt;还有一个函数  &lt;a href="https://w3c.github.io/requestidlecallback/"&gt;window.requestIdleCallback()&lt;/a&gt;，也可以用来调节重新渲染。&lt;/p&gt;

 &lt;p&gt;它指定只有当一帧的末尾有空闲时间，才会执行回调函数。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
requestIdleCallback(fn);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，只有当前帧的运行时间小于16.66ms时，函数fn才会执行。否则，就推迟到下一帧，如果下一帧也没有空闲时间，就推迟到下下一帧，以此类推。&lt;/p&gt;

 &lt;p&gt;它还可以接受第二个参数，表示指定的毫秒数。如果在指定
的这段时间之内，每一帧都没有空闲时间，那么函数fn将会强制执行。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
requestIdleCallback(fn, 5000);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面的代码表示，函数fn最迟会在5000毫秒之后执行。&lt;/p&gt;

 &lt;p&gt;函数 fn 可以接受一个 deadline 对象作为参数。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
requestIdleCallback(function someHeavyComputation(deadline) {
  while(deadline.timeRemaining() &amp;gt; 0) {
    doWorkIfNeeded();
  }

  if(thereIsMoreWorkToDo) {
    requestIdleCallback(someHeavyComputation);
  }
});
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码中，回调函数 someHeavyComputation 的参数是一个 deadline 对象。&lt;/p&gt;

 &lt;p&gt;deadline对象有一个方法和一个属性：timeRemaining() 和 didTimeout。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;（1）timeRemaining() 方法&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;timeRemaining() 方法返回当前帧还剩余的毫秒。这个方法只能读，不能写，而且会动态更新。因此可以不断检查这个属性，如果还有剩余时间的话，就不断执行某些任务。一旦这个属性等于0，就把任务分配到下一轮  &lt;code&gt;requestIdleCallback&lt;/code&gt;。&lt;/p&gt;

 &lt;p&gt;前面的示例代码之中，只要当前帧还有空闲时间，就不断调用doWorkIfNeeded方法。一旦没有空闲时间，但是任务还没有全执行，就分配到下一轮  &lt;code&gt;requestIdleCallback&lt;/code&gt;。&lt;/p&gt;

 &lt;p&gt;  &lt;strong&gt;（2）didTimeout属性&lt;/strong&gt;&lt;/p&gt;

 &lt;p&gt;deadline对象的   &lt;code&gt;didTimeout&lt;/code&gt; 属性会返回一个布尔值，表示指定的时间是否过期。这意味着，如果回调函数由于指定时间过期而触发，那么你会得到两个结果。&lt;/p&gt;

 &lt;blockquote&gt;
    &lt;ul&gt;
   &lt;li&gt;timeRemaining方法返回0&lt;/li&gt;
   &lt;li&gt;didTimeout 属性等于 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

 &lt;p&gt;因此，如果回调函数执行了，无非是两种原因：当前帧有空闲时间，或者指定时间到了。&lt;/p&gt;

 &lt;blockquote&gt;  &lt;pre&gt;   &lt;code&gt;
function myNonEssentialWork (deadline) {
  while ((deadline.timeRemaining() &amp;gt; 0 || deadline.didTimeout) &amp;amp;&amp;amp; tasks.length &amp;gt; 0)
    doWorkIfNeeded();

  if (tasks.length &amp;gt; 0)
    requestIdleCallback(myNonEssentialWork);
}

requestIdleCallback(myNonEssentialWork, 5000);
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;

 &lt;p&gt;上面代码确保了，doWorkIfNeeded 函数一定会在将来某个比较空闲的时间（或者在指定时间过期后）得到反复执行。&lt;/p&gt;

 &lt;p&gt;requestIdleCallback 是一个很新的函数，刚刚引入标准，目前只有Chrome支持。&lt;/p&gt;

 &lt;h2&gt;九、参考链接&lt;/h2&gt;

 &lt;ul&gt;
  &lt;li&gt;Domenico De Felice,    &lt;a href="http://domenicodefelice.blogspot.sg/2015/08/how-browsers-work.html"&gt;How browsers work&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Stoyan Stefanov,    &lt;a href="http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/"&gt;Rendering: repaint, reflow/relayout, restyle&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Addy Osmani,    &lt;a href="http://addyosmani.com/blog/performance-optimisation-with-timeline-profiles/"&gt;Improving Web App Performance With the Chrome DevTools Timeline and Profiles&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Tom Wiltzius,    &lt;a href="http://www.html5rocks.com/en/tutorials/speed/rendering/"&gt;Jank Busting for Better Rendering Performance&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Paul Lewis,    &lt;a href="https://developers.google.com/web/updates/2015/08/27/using-requestidlecallback?hl=en"&gt;Using requestIdleCallback&lt;/a&gt;&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;发表日期： 2015年9月17日&lt;/li&gt;
   &lt;li&gt;更多内容：     &lt;a href="http://www.ruanyifeng.com/blog/archives.html" target="_blank"&gt; 档案&lt;/a&gt;  » 
    &lt;a href="http://www.ruanyifeng.com/blog/developer/"&gt; 开发者手册&lt;/a&gt; 
&lt;/li&gt;
   &lt;li&gt;购买文集：    &lt;a href="http://www.ruanyifeng.com/blog/2014/12/my-blog-book.html" target="_blank"&gt;     &lt;img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAKOWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanZZ3VFTXFofPvXd6oc0wAlKG3rvAANJ7k15FYZgZYCgDDjM0sSGiAhFFRJoiSFDEgNFQJFZEsRAUVLAHJAgoMRhFVCxvRtaLrqy89/Ly++Osb+2z97n77L3PWhcAkqcvl5cGSwGQyhPwgzyc6RGRUXTsAIABHmCAKQBMVka6X7B7CBDJy82FniFyAl8EAfB6WLwCcNPQM4BOB/+fpFnpfIHomAARm7M5GSwRF4g4JUuQLrbPipgalyxmGCVmvihBEcuJOWGRDT77LLKjmNmpPLaIxTmns1PZYu4V8bZMIUfEiK+ICzO5nCwR3xKxRoowlSviN+LYVA4zAwAUSWwXcFiJIjYRMYkfEuQi4uUA4EgJX3HcVyzgZAvEl3JJS8/hcxMSBXQdli7d1NqaQffkZKVwBALDACYrmcln013SUtOZvBwAFu/8WTLi2tJFRbY0tba0NDQzMv2qUP91829K3NtFehn4uWcQrf+L7a/80hoAYMyJarPziy2uCoDOLQDI3fti0zgAgKSobx3Xv7oPTTwviQJBuo2xcVZWlhGXwzISF/QP/U+Hv6GvvmckPu6P8tBdOfFMYYqALq4bKy0lTcinZ6QzWRy64Z+H+B8H/nUeBkGceA6fwxNFhImmjMtLELWbx+YKuGk8Opf3n5r4D8P+pMW5FonS+BFQY4yA1HUqQH7tBygKESDR+8Vd/6NvvvgwIH554SqTi3P/7zf9Z8Gl4iWDm/A5ziUohM4S8jMX98TPEqABAUgCKpAHykAd6ABDYAasgC1wBG7AG/iDEBAJVgMWSASpgA+yQB7YBApBMdgJ9oBqUAcaQTNoBcdBJzgFzoNL4Bq4AW6D+2AUTIBnYBa8BgsQBGEhMkSB5CEVSBPSh8wgBmQPuUG+UBAUCcVCCRAPEkJ50GaoGCqDqqF6qBn6HjoJnYeuQIPQXWgMmoZ+h97BCEyCqbASrAUbwwzYCfaBQ+BVcAK8Bs6FC+AdcCXcAB+FO+Dz8DX4NjwKP4PnEIAQERqiihgiDMQF8UeikHiEj6xHipAKpAFpRbqRPuQmMorMIG9RGBQFRUcZomxRnqhQFAu1BrUeVYKqRh1GdaB6UTdRY6hZ1Ec0Ga2I1kfboL3QEegEdBa6EF2BbkK3oy+ib6Mn0K8xGAwNo42xwnhiIjFJmLWYEsw+TBvmHGYQM46Zw2Kx8lh9rB3WH8vECrCF2CrsUexZ7BB2AvsGR8Sp4Mxw7rgoHA+Xj6vAHcGdwQ3hJnELeCm8Jt4G749n43PwpfhGfDf+On4Cv0CQJmgT7AghhCTCJkIloZVwkfCA8JJIJKoRrYmBRC5xI7GSeIx4mThGfEuSIemRXEjRJCFpB+kQ6RzpLuklmUzWIjuSo8gC8g5yM/kC+RH5jQRFwkjCS4ItsUGiRqJDYkjiuSReUlPSSXK1ZK5kheQJyeuSM1J4KS0pFymm1HqpGqmTUiNSc9IUaVNpf+lU6RLpI9JXpKdksDJaMm4ybJkCmYMyF2TGKQhFneJCYVE2UxopFykTVAxVm+pFTaIWU7+jDlBnZWVkl8mGyWbL1sielh2lITQtmhcthVZKO04bpr1borTEaQlnyfYlrUuGlszLLZVzlOPIFcm1yd2WeydPl3eTT5bfJd8p/1ABpaCnEKiQpbBf4aLCzFLqUtulrKVFS48vvacIK+opBimuVTyo2K84p6Ss5KGUrlSldEFpRpmm7KicpFyufEZ5WoWiYq/CVSlXOavylC5Ld6Kn0CvpvfRZVUVVT1Whar3qgOqCmrZaqFq+WpvaQ3WCOkM9Xr1cvUd9VkNFw08jT6NF454mXpOhmai5V7NPc15LWytca6tWp9aUtpy2l3audov2Ax2yjoPOGp0GnVu6GF2GbrLuPt0berCehV6iXo3edX1Y31Kfq79Pf9AAbWBtwDNoMBgxJBk6GWYathiOGdGMfI3yjTqNnhtrGEcZ7zLuM/5oYmGSYtJoct9UxtTbNN+02/R3Mz0zllmN2S1zsrm7+QbzLvMXy/SXcZbtX3bHgmLhZ7HVosfig6WVJd+y1XLaSsMq1qrWaoRBZQQwShiXrdHWztYbrE9Zv7WxtBHYHLf5zdbQNtn2iO3Ucu3lnOWNy8ft1OyYdvV2o/Z0+1j7A/ajDqoOTIcGh8eO6o5sxybHSSddpySno07PnU2c+c7tzvMuNi7rXM65Iq4erkWuA24ybqFu1W6P3NXcE9xb3Gc9LDzWepzzRHv6eO7yHPFS8mJ5NXvNelt5r/Pu9SH5BPtU+zz21fPl+3b7wX7efrv9HqzQXMFb0ekP/L38d/s/DNAOWBPwYyAmMCCwJvBJkGlQXlBfMCU4JvhI8OsQ55DSkPuhOqHC0J4wybDosOaw+XDX8LLw0QjjiHUR1yIVIrmRXVHYqLCopqi5lW4r96yciLaILoweXqW9KnvVldUKq1NWn46RjGHGnIhFx4bHHol9z/RnNjDn4rziauNmWS6svaxnbEd2OXuaY8cp40zG28WXxU8l2CXsTphOdEisSJzhunCruS+SPJPqkuaT/ZMPJX9KCU9pS8Wlxqae5Mnwknm9acpp2WmD6frphemja2zW7Fkzy/fhN2VAGasyugRU0c9Uv1BHuEU4lmmfWZP5Jiss60S2dDYvuz9HL2d7zmSue+63a1FrWWt78lTzNuWNrXNaV78eWh+3vmeD+oaCDRMbPTYe3kTYlLzpp3yT/LL8V5vDN3cXKBVsLBjf4rGlpVCikF84stV2a9021DbutoHt5turtn8sYhddLTYprih+X8IqufqN6TeV33zaEb9joNSydP9OzE7ezuFdDrsOl0mX5ZaN7/bb3VFOLy8qf7UnZs+VimUVdXsJe4V7Ryt9K7uqNKp2Vr2vTqy+XeNc01arWLu9dn4fe9/Qfsf9rXVKdcV17w5wD9yp96jvaNBqqDiIOZh58EljWGPft4xvm5sUmoqbPhziHRo9HHS4t9mqufmI4pHSFrhF2DJ9NProje9cv+tqNWytb6O1FR8Dx4THnn4f+/3wcZ/jPScYJ1p/0Pyhtp3SXtQBdeR0zHYmdo52RXYNnvQ+2dNt293+o9GPh06pnqo5LXu69AzhTMGZT2dzz86dSz83cz7h/HhPTM/9CxEXbvUG9g5c9Ll4+ZL7pQt9Tn1nL9tdPnXF5srJq4yrndcsr3X0W/S3/2TxU/uA5UDHdavrXTesb3QPLh88M+QwdP6m681Lt7xuXbu94vbgcOjwnZHokdE77DtTd1PuvriXeW/h/sYH6AdFD6UeVjxSfNTws+7PbaOWo6fHXMf6Hwc/vj/OGn/2S8Yv7ycKnpCfVEyqTDZPmU2dmnafvvF05dOJZ+nPFmYKf5X+tfa5zvMffnP8rX82YnbiBf/Fp99LXsq/PPRq2aueuYC5R69TXy/MF72Rf3P4LeNt37vwd5MLWe+x7ys/6H7o/ujz8cGn1E+f/gUDmPP8kcBa2wAAADxQTFRFAAAAAAAAAgICBAQEBQUFBgYGBwcHCAgICQkJCgoKDQ0NExMTOTk5c3NzdXV1eHh4eXl5enp6e3t7fHx8rr9WJAAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYMADMaKH8BrQAAAGVJREFUGNONkI0KwCAIhE+32n/b2vu/6yqYCRnshMCPS/SA/6Ikk5vQWXQ7gpL6H1KV54OMbCiuCk0n9rOdma3i1JteuIGIJ1bI60Se2dG4qFWDddQsSeiZpRkILfQ6Hcmvk2NPLzksA4TnEeNNAAAAAElFTkSuQmCC"&gt;&lt;/img&gt; 《如何变得有思想》&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;社交媒体：    &lt;a href="https://twitter.com/ruanyf" target="_blank"&gt;     &lt;img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNqsVc9rE0EU/mZ3dhPNJl2bVIk/Km09aFBE8GLvnrwJXvwLxIsgCP4Fnit6E8Gb0IMHwUsL9SSNgiAoooJIpE1Joo1h82uT7IxvxmRN3Q32kAdvdrI773vfe+/bDTu9WioDcDAda3Ja8piepQ1avCkCeny0Y+R9KenKwOmHHLvPaKn7AtVOoO8dOWDCMpg+41gMQv5FNEJACgoE8MsPNLDBhgfoutMOsJDmWLmU037C4TApciljwQ/kHoohQxV06+wMXNvE7WINuaSpvdYVKLgWnl3O4zAxU3Z1IYVGT+B5qYWV9z2kbQaT/cOwTsxOUUYF+mB5Dkmi8HG3h2q9hxsFNwRTdvQgxxnXRrMvIdkEhllis/qtiWuLDm4WZnBlPoWnXz28LHdw0uGR7m9Wu3j0qYGEubfn4UlBfXO4gTZltRNMg9w9f0i7kNFxllsBlS0x75gYfxyWTBUjT2W5CSMSPBrQuG21+hiMDS8CeCzF8eSLp13uQ3Ab1IqMFZN8tFHPmgOBF99bYP8Be1Xp6t7OJszJgKpPSiZvaj7uf2hguz2IBQuozDvFH6RDBjuK9wdQDvt0nMpW+8efGyh5UcAeZb2+UcHbnz5Jx4wdlp4yJYU3kLiQtfBwOYeLc8nIwfXtNu69q2Oz4mMxbemYOOOjKarhvq51KUjg3GwiFPIuvcNF6pnSnWK0lOEabNLgQh1aJFAhlGB9rG110B2+oyRNPc0sZbRNFltmLKDKqj4Qrm1ojzOxDz2pyPQ0P7CK4c40/wJ+CzAAGYrXsvfFXR4AAAAASUVORK5CYII="&gt;&lt;/img&gt; twitter&lt;/a&gt;，    &lt;a href="http://weibo.com/ruanyf" target="_blank"&gt;     &lt;img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAABPZJREFUeNqMyXtM1WUcx/FnNTdreAVCEEThyEFUrM2kwkpS05qVl8rugVpRUelaJdZaWWktdZKHssImgV04osgBCbwAa2Whm6GWoXHxcD1wHiI4t9/z/H68+wO3av1R3+217/b+CFStsHq/W2V1HW+wOmq8VkeN/E+dR/7SUSOtrmOnLc93jxA6LsTw71XLrUsHsNwHsdxl/09HGVb7QawOF1bn4ZF26QDDsmqNsH7be9psKcJs+fy/tRZjXvwM//678BVdT6BsMarhxZHeug+reW+TMC84+s0LDv4tH7PZgdXqwGrLx7yYP9KaHISOvUCwYh3+4iUMOiIJVmSMbBfy+4X581Zp/ryVfzi/FfPXd1AntxNwvkvg6zfQZ7Zg/rIV8/x7qB9fJlT/JPrcm4SOPctg3ihCR5dhNu2QQjfmSt2Yi27MRf+Uiz67Ef1TLv7Ct5CPZtMzJ4N2243Ij7Kwmjdhnt+EUZeFr/BmhvYkoBqewl8yD9/eCejGTVLoUzlSn8pBn8xBn85B/fAcf7z6PJ7rl9KdNBdP6nwuJc2jL+9+jJrXGNr1On9seZHgkQ0ESq8l4EzBOLKMod1XoE6slUKfXCd1wxp0Qxb6RBYDLzxGz8xb8aSm05u2kL4bF9N902I89yzHe986PEseoGP2YjqfvpNQeTr+4rmEKucz6LgS9f1jUuiy+VIfvxvz1GoGN99Hz8wFeGan0zl3Ae4lK3A/vJb2x7PpWPEI3bevoHfJKtypt9H1UgbDZx5E//goofJF+AtmoasWSaG2jZE6P5zg9hQ8Nyyie1Y6rQ+vpafMhdHXx7BpgjWMr7sbT5kL9/IHcUck0X3LtfgLVqAP3YAumoHeHYvaPkYKlRcudf54Bh5KpjP5Fi69tplgIMCQabK/rIxtO3bgqqxkmJHzdXbRknYbrWIcrWEJ9D8RjXaMRe2ciNo5QQr1QYRUjnC8C1Nxr1pD0O/HFwyQmZmJ3W4nLS0Nm83G+vXrsSwLgP6vS2kWo/lNjKN57gTMTyJRH0Si8iZKoXZdI5Ujkt55SXRtfBuAffv2sXLlSpxOJ9nZ2SQkJBAWFobX6wVgoPooLWI0v4hxuFeFY30ahdoVhcqLkEJ9GCV1QRTehZNxZ9wPQNGXX5KZmUlbWxspKSkIIUhPT0dZFibQvSaHJjGKxujxBHZGoT+ehPpwEsoRIYUqmCR1YTTBd2NoGxtOz4Y3CPoDrNuwnuhpU7lq/HgW3LGU5l4P2h+g55XNnBOjORM/jsH3ozGLYlAFMag90ahPI6VQxZOlKo5BOyfjey+GliljaL0uA7l9D2f37uess5KBoyfwbvmIJnsajVePouXeCIKFsZglsajiyX8pvEYKoySm33DGYTjj0OVxhEri6N84kfalY2i/NRb3AhvNN0+i9Y4wPC+H4yuMxTocjzoYj7F/ymVxI/+ryH5huKaeMVzxGK54QuXT8B+y4atIZqB8Nt6SVDzFqXi+mIO39Dp+PziHwdIZDJVOJ3gogZArHsM1BcMVj1E+BcMVe1GY38auNqqnYdQkEKpOJPBNEv7DdnyVdnwVdoYqki+bge9wCv6qFAJVyQS/mU6oJhHjSCJG9VSM6kRUfXi2GK6fJYy6+CyjNrHRqLP1GbWJ0qizSfV39Tap6qdLVW+Xqt4ujfokadRdVpvgNWpt54y6qc8MV44Sfw4An+t+Gj1AKyYAAAAASUVORK5CYII="&gt;&lt;/img&gt; weibo&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;Feed订阅：     &lt;a href="http://www.ruanyifeng.com/feed.html" target="_blank"&gt;     &lt;img src="data:image/gif;base64,R0lGODlhFAAUANU2APV1GfmrePZ8IeR5VPq2ieNkLf7p3NpFJveCJ/NoFfibVPR1IexiJfvdzuZrNfXGuOhUEueTevaFOfzIpNxDGeZZJNU6IOubifiVTfOnid1aLuZpLOFSJf/28vJtIvGcefaLSd5kNu9iFv/z6//48//t4uFfLfNkCeJNGuGNc/rCof/+/PVxEeldI/V5KeKMbu9nIvB2OvzQtNxMKfOPXP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADYALAAAAAAUABQAAAb/QJstEjKZCsjCZsl0pIS2QWXaYsA8noVWIEAgFoNhhWNyMESi7Jbr/aZCFdRnZJAFJCwAt+vlOkwtEBk1hDUkExJ6bQJaGyYMLTQTDSOFHQF8jAseSFYiJwkuAQ2FE1xaHjBIMDAgARguJwABJIQTLJswDEgeIgSEJQQILAqVNQEiui1HHgkBhTUGCiwYtR0xLS0VJhtaEgoEBoQkGCfPNSoQY9xaCwAsCBPALgmkJA4cZN0CAAgY/PJqEIDwgVAECjM0bOAnwcAKGQJclKhRgoGDDjUeUDigkN8vQiBOfByAglSDAxw3IBCgYEU0FyJoEPpA4UGNDjMsaHDQR8EdPVYOMmQYQGHAhQgHLIRIsWAlgASsGMRBMWMGBQtYZ7yIsoBRKkhjOMxAiXJGGCEpHGxIYsSIhrcaQmy1EQQAOw=="&gt;&lt;/img&gt;&lt;/a&gt;&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>Developer</category>
      <guid isPermaLink="true">https://itindex.net/detail/54347-%E7%BD%91%E9%A1%B5-%E6%80%A7%E8%83%BD-%E7%AE%A1%E7%90%86</guid>
      <pubDate>Thu, 17 Sep 2015 08:26:22 CST</pubDate>
    </item>
    <item>
      <title>Chromium网页Graphics Layer Tree创建过程分析</title>
      <link>https://itindex.net/detail/55264-chromium-%E7%BD%91%E9%A1%B5-graphics</link>
      <description>&lt;p&gt;       在前面一文中，我们分析了网页Render Layer Tree的创建过程。在创建Render Layer的同时，WebKit还会为其创建Graphics Layer。这些Graphics Layer形成一个Graphics Layer Tree。Graphics Layer可看作是一个图形缓冲区，被若干Render Layer共用。本文接下来就分析Graphics Layer Tree的创建过程。&lt;/p&gt; &lt;p align="center"&gt;老罗的新浪微博：  &lt;a href="http://weibo.com/shengyangluo" target="_blank"&gt;http://weibo.com/shengyangluo&lt;/a&gt;，欢迎关注！  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;       网页的Render Layer Tree与Graphics Layer Tree的关系可以通过图1描述，如下所示：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="http://img.blog.csdn.net/20160214013252908"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;图1 Graphics Layer Tree与DOM Tree、Render Object Tree和Render Layer Tree的关系  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;       在WebKit中，Graphics Layer又称为Composited Layer。我们可以将Graphics Layer看作是Composited Layer的一种具体实现。这种具体实现是由WebKit的使用者Chromium提供的。Composited Layer描述的是一个具有后端存储的图层，因此可以将它看作是一个图形缓冲区。在软件渲染方式中，这个图形缓冲区就是一块系统内存；在硬件渲染方式中，这个图形缓冲区就是一个OpenGL里面的一个Frame Buffer Object（FBO）。&lt;/p&gt; &lt;p&gt;       Composited Layer涉及到的一个重要概念是“Layer Compositing”。Layer Compositing是现代UI框架普遍采用的一种渲染机制。例如，Android系统的UI子系统（Surface Flinger）就是通过Compositing Surface来获得最终要显示在屏幕上的内容的。这里的Surface就相当于是Chromium的Layer。关于Android系统的Surface Flinger的详细分析，可以参考  &lt;a href="http://blog.csdn.net/luoshengyang/article/details/8010977" target="_blank"&gt;Android系统Surface机制的SurfaceFlinger服务简要介绍和学习计划&lt;/a&gt;这个系列的文章。&lt;/p&gt; &lt;p&gt;       Layer Compositing的三个主要任务是：&lt;/p&gt; &lt;p&gt;       1. 确定哪些内容应该在哪些Composited Layer上绘制；&lt;/p&gt; &lt;p&gt;       2. 绘制每一个Composited Layer；&lt;/p&gt; &lt;p&gt;       3. 将所有已经绘制好的Composited Layer再次将绘制在一个最终的、可以显示在屏幕上进行显示的图形缓冲区中。&lt;/p&gt; &lt;p&gt;       其中，第1个任务它完成之后就可以获得一个Graphics Layer Tree，第3个任务要求按照一定的顺序对Composited Layer进行绘制。注意，这个绘制顺序非常重要，否则最终合成出来的UI就会出现不正确的Overlapping。同时，这个绘制顺序对理解Graphics Layer Tree的组成也非常重要。因此，接下来我们首先介绍与这个绘制顺序有关的概念。为了方便描述，本文将上述绘制顺序称为Composited Layer的绘制顺序。&lt;/p&gt; &lt;p&gt;       在介绍Composited Layer的绘制顺序之前，我们还需要回答一个问题：为什么要采用Layer Compositing这种UI渲染机制？主要有两个原因：&lt;/p&gt; &lt;p&gt;       1. 避免不必要的重绘。考虑一个网页有两个Layer。在网页的某一帧显示中，Layer 1的元素发生了变化，Layer 2的元素没有发生变化。这时候只需要重新绘制Layer 1的内容，然后再与Layer 2原有的内容进行Compositing，就可以得到整个网页的内容。这样就可以避免对没有发生变化的Layer 2进行不必要的绘制。&lt;/p&gt; &lt;p&gt;       2. 利用硬件加速高效实现某些UI特性。例如网页的某一个Layer设置了可滚动、3D变换、透明度或者滤镜，那么就可以通过GPU来高效实现。&lt;/p&gt; &lt;p&gt;       在默认情况下，网页元素的绘制是按照Render Object Tree的先序遍历顺序进行的，并且它们在空间上是按照各自的display属性值依次进行布局的。例如，如果一个网页元素的display属性值为&amp;quot;inline&amp;quot;，那么它就会以内联元素方式显示，也就是紧挨在前一个绘制的元素的后面进行显示。又如，如果一个网页元素的display属性值为&amp;quot;block&amp;quot;，那么它就会以块级元素进行显示，也就是它的前后会各有一个换行符。我们将这种网页元素绘制方式称为Normal Flow或者In Flow。&lt;/p&gt; &lt;p&gt;       有默认情况，就会例外情况。例如，如果一个网页元素同时设置了position和z-index属性，那么它可能就不会以In Flow的方式进行显示，而是以Out of Flow的方式进行显示。在默认情况下，一个网页元素的position和z-index属性值被设置为“static”和&amp;quot;auto&amp;quot;。网页元素的position属性还可以取值为“relative”、“absolute”和“fixed”，这一类网页元素称为Positioned元素。当一个Positioned元素的z-index属性值不等于&amp;quot;auto&amp;quot;时，它就会以Out of Flow的方式进行显示。&lt;/p&gt; &lt;p&gt;         &lt;a href="https://www.w3.org/TR/CSS21/visuren.html" target="_blank"&gt;CSS 2.1规范&lt;/a&gt;规定网页渲染引擎要为每一个z-index属性值不等于&amp;quot;auto&amp;quot;的Positioned元素创建一个Stacking Context。对于其它的元素，它们虽然没有自己的Stacking Context，但是它们会与最近的、具有自己的Stacking Context的元素共享同相同的Stacking Context。不同Stacking Context的元素的绘制顺序是不会相互交叉的。假设有两个Stacking Context，一个包含有A和B两个元素，另一个包含有C和D两个元素，那么A、B、C和D四个元素的绘制顺序只可能为：&lt;/p&gt; &lt;p&gt;       1. A、B、C、D&lt;/p&gt; &lt;p&gt;       2. B、A、C、D&lt;/p&gt; &lt;p&gt;       3. A、B、D、C&lt;/p&gt; &lt;p&gt;       4. B、A、D、C&lt;/p&gt; &lt;p&gt;       5. C、D、A、B&lt;/p&gt; &lt;p&gt;       6. C、D、B、A&lt;/p&gt; &lt;p&gt;       7. D、C、A、B&lt;/p&gt; &lt;p&gt;       8. D、C、B、A&lt;/p&gt; &lt;p&gt;       Stacking Context的这个特性，使得它可以成为一个观念上的原子类型绘制层（Atomic Conceptual Layer for Painting）。也就是说，只要我们定义好Stacking Context内部元素的绘制顺序，那么再根据拥有Stacking Context的元素的z-index属性值，那么就可以得到网页的所有元素的绘制顺序。&lt;/p&gt; &lt;p&gt;       我们可以通过图2所示的例子直观地理解Stacking Context的上述特性，如下所示：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" height="236" src="http://img.blog.csdn.net/20160215031944251" width="350"&gt;&lt;/img&gt;    &lt;img alt="" height="236" src="http://img.blog.csdn.net/20160215031950437" width="350"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;图2 Stacking Context&lt;/p&gt; &lt;p&gt;       在图2的左边，一共有4个Stacking Context。最下面的Stacking Context的z-index等于-1；中间的Stacking Context的z-index等于0；最上面的Stacking Context的z-index等于1，并且嵌套了另外一个z-index等于6的Stacking Context。我们观察被嵌套的z-index等于6的Stacking Context，它包含了另外一个z-index也是等于6的元素，但是这两个z-index的含义是不一样的。其中，Stacking Context的z-index值是放在父Stacking Context中讨论才有意义，而元素的z-index放在当前它所在的Stacking Context讨论才有意义。再者，我们是下面和中间的两个Stacking Context，虽然它们都包含有三个z-index分别等于7、8和9的元素，但是它们是完全不相干的。&lt;/p&gt; &lt;p&gt;      如果我们将图2左边中间的Stacking Context的z-index修改为2，那么它就会变成最上面的Stacking Context，并且会重叠在z-index等于1的Stacking Context上，以及嵌套在这个Stacking Context里面的那个Stacking Context。&lt;/p&gt; &lt;p&gt;      这样，我们就得到了Stacking Context的绘制顺序。如前所述，接下来只要定义好Stacking Context内的元素的绘制顺序，那么就可以网页的所有元素的绘制顺序。Stacking Context内的元素的绘制顺序如下所示：&lt;/p&gt; &lt;p&gt;      1. 背景（Backgrounds）和边界（Borders），也就是拥有Stacking Context的元素的背景和边界。&lt;/p&gt; &lt;p&gt;      2. Z-index值为负数的子元素。&lt;/p&gt; &lt;p&gt;      3. 内容（Contents），也就是拥有Stacking Context的元素的内容。&lt;/p&gt; &lt;p&gt;      4. Normal Flow类型的子元素。&lt;/p&gt; &lt;p&gt;      5. Z-index值为正数的子元素。&lt;/p&gt; &lt;p&gt;      以上就是与Composited Layer的绘制顺序有关的背景知识。这些背景知识在后面分析Graphics Layer Tree的创建过程时就会用到。&lt;/p&gt; &lt;p&gt;      从图1可以看到，Graphics Layer Tree是根据Render Layer Tree创建的。也就是说，Render Layer与Graphics Layer存在对应关系，如下所示：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="http://img.blog.csdn.net/20160216011427129"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;图3 Render Layer Tree与Graphics Layer的关系&lt;/p&gt; &lt;p&gt;       原则上，Render Layer Tree中的每一个Render Layer都对应有一个Composited Layer Mapping，每一个Composited Layer Mapping又包含有若干个Graphics Layer。但是这样将会导致创建大量的Graphics Layer。创建大量的Graphics Layer意味着需要耗费大量的内存资源。这个问题称为”Layer Explosion“问题。&lt;/p&gt; &lt;p&gt;       为了解决“Layer Explosion”问题，每一个需要创建Composited Layer Mapping的Render Layer都需要给出一个理由。这个理由称为“Compositing Reason”，它描述的实际上是Render Layer的特证。例如，如果一个Render Layer关联的Render Object设置了3D Transform属性，那么就需要为该Render Layer创建一个Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       WebKit一共定义了54个Compositing Reason，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// Intrinsic reasons that can be known right away by the layer
const uint64_t CompositingReason3DTransform                              = UINT64_C(1) &amp;lt;&amp;lt; 0;
const uint64_t CompositingReasonVideo                                    = UINT64_C(1) &amp;lt;&amp;lt; 1;
const uint64_t CompositingReasonCanvas                                   = UINT64_C(1) &amp;lt;&amp;lt; 2;
const uint64_t CompositingReasonPlugin                                   = UINT64_C(1) &amp;lt;&amp;lt; 3;
const uint64_t CompositingReasonIFrame                                   = UINT64_C(1) &amp;lt;&amp;lt; 4;
const uint64_t CompositingReasonBackfaceVisibilityHidden                 = UINT64_C(1) &amp;lt;&amp;lt; 5;
const uint64_t CompositingReasonActiveAnimation                          = UINT64_C(1) &amp;lt;&amp;lt; 6;
const uint64_t CompositingReasonTransitionProperty                       = UINT64_C(1) &amp;lt;&amp;lt; 7;
const uint64_t CompositingReasonFilters                                  = UINT64_C(1) &amp;lt;&amp;lt; 8;
const uint64_t CompositingReasonPositionFixed                            = UINT64_C(1) &amp;lt;&amp;lt; 9;
const uint64_t CompositingReasonOverflowScrollingTouch                   = UINT64_C(1) &amp;lt;&amp;lt; 10;
const uint64_t CompositingReasonOverflowScrollingParent                  = UINT64_C(1) &amp;lt;&amp;lt; 11;
const uint64_t CompositingReasonOutOfFlowClipping                        = UINT64_C(1) &amp;lt;&amp;lt; 12;
const uint64_t CompositingReasonVideoOverlay                             = UINT64_C(1) &amp;lt;&amp;lt; 13;
const uint64_t CompositingReasonWillChangeCompositingHint                = UINT64_C(1) &amp;lt;&amp;lt; 14;

// Overlap reasons that require knowing what&amp;apos;s behind you in paint-order before knowing the answer
const uint64_t CompositingReasonAssumedOverlap                           = UINT64_C(1) &amp;lt;&amp;lt; 15;
const uint64_t CompositingReasonOverlap                                  = UINT64_C(1) &amp;lt;&amp;lt; 16;
const uint64_t CompositingReasonNegativeZIndexChildren                   = UINT64_C(1) &amp;lt;&amp;lt; 17;
const uint64_t CompositingReasonScrollsWithRespectToSquashingLayer       = UINT64_C(1) &amp;lt;&amp;lt; 18;
const uint64_t CompositingReasonSquashingSparsityExceeded                = UINT64_C(1) &amp;lt;&amp;lt; 19;
const uint64_t CompositingReasonSquashingClippingContainerMismatch       = UINT64_C(1) &amp;lt;&amp;lt; 20;
const uint64_t CompositingReasonSquashingOpacityAncestorMismatch         = UINT64_C(1) &amp;lt;&amp;lt; 21;
const uint64_t CompositingReasonSquashingTransformAncestorMismatch       = UINT64_C(1) &amp;lt;&amp;lt; 22;
const uint64_t CompositingReasonSquashingFilterAncestorMismatch          = UINT64_C(1) &amp;lt;&amp;lt; 23;
const uint64_t CompositingReasonSquashingWouldBreakPaintOrder            = UINT64_C(1) &amp;lt;&amp;lt; 24;
const uint64_t CompositingReasonSquashingVideoIsDisallowed               = UINT64_C(1) &amp;lt;&amp;lt; 25;
const uint64_t CompositingReasonSquashedLayerClipsCompositingDescendants = UINT64_C(1) &amp;lt;&amp;lt; 26;

// Subtree reasons that require knowing what the status of your subtree is before knowing the answer
const uint64_t CompositingReasonTransformWithCompositedDescendants       = UINT64_C(1) &amp;lt;&amp;lt; 27;
const uint64_t CompositingReasonOpacityWithCompositedDescendants         = UINT64_C(1) &amp;lt;&amp;lt; 28;
const uint64_t CompositingReasonMaskWithCompositedDescendants            = UINT64_C(1) &amp;lt;&amp;lt; 29;
const uint64_t CompositingReasonReflectionWithCompositedDescendants      = UINT64_C(1) &amp;lt;&amp;lt; 30;
const uint64_t CompositingReasonFilterWithCompositedDescendants          = UINT64_C(1) &amp;lt;&amp;lt; 31;
const uint64_t CompositingReasonBlendingWithCompositedDescendants        = UINT64_C(1) &amp;lt;&amp;lt; 32;
const uint64_t CompositingReasonClipsCompositingDescendants              = UINT64_C(1) &amp;lt;&amp;lt; 33;
const uint64_t CompositingReasonPerspectiveWith3DDescendants             = UINT64_C(1) &amp;lt;&amp;lt; 34;
const uint64_t CompositingReasonPreserve3DWith3DDescendants              = UINT64_C(1) &amp;lt;&amp;lt; 35;
const uint64_t CompositingReasonReflectionOfCompositedParent             = UINT64_C(1) &amp;lt;&amp;lt; 36;
const uint64_t CompositingReasonIsolateCompositedDescendants             = UINT64_C(1) &amp;lt;&amp;lt; 37;

// The root layer is a special case that may be forced to be a layer, but also it needs to be
// a layer if anything else in the subtree is composited.
const uint64_t CompositingReasonRoot                                     = UINT64_C(1) &amp;lt;&amp;lt; 38;

// CompositedLayerMapping internal hierarchy reasons
const uint64_t CompositingReasonLayerForAncestorClip                     = UINT64_C(1) &amp;lt;&amp;lt; 39;
const uint64_t CompositingReasonLayerForDescendantClip                   = UINT64_C(1) &amp;lt;&amp;lt; 40;
const uint64_t CompositingReasonLayerForPerspective                      = UINT64_C(1) &amp;lt;&amp;lt; 41;
const uint64_t CompositingReasonLayerForHorizontalScrollbar              = UINT64_C(1) &amp;lt;&amp;lt; 42;
const uint64_t CompositingReasonLayerForVerticalScrollbar                = UINT64_C(1) &amp;lt;&amp;lt; 43;
const uint64_t CompositingReasonLayerForScrollCorner                     = UINT64_C(1) &amp;lt;&amp;lt; 44;
const uint64_t CompositingReasonLayerForScrollingContents                = UINT64_C(1) &amp;lt;&amp;lt; 45;
const uint64_t CompositingReasonLayerForScrollingContainer               = UINT64_C(1) &amp;lt;&amp;lt; 46;
const uint64_t CompositingReasonLayerForSquashingContents                = UINT64_C(1) &amp;lt;&amp;lt; 47;
const uint64_t CompositingReasonLayerForSquashingContainer               = UINT64_C(1) &amp;lt;&amp;lt; 48;
const uint64_t CompositingReasonLayerForForeground                       = UINT64_C(1) &amp;lt;&amp;lt; 49;
const uint64_t CompositingReasonLayerForBackground                       = UINT64_C(1) &amp;lt;&amp;lt; 50;
const uint64_t CompositingReasonLayerForMask                             = UINT64_C(1) &amp;lt;&amp;lt; 51;
const uint64_t CompositingReasonLayerForClippingMask                     = UINT64_C(1) &amp;lt;&amp;lt; 52;
const uint64_t CompositingReasonLayerForScrollingBlockSelection          = UINT64_C(1) &amp;lt;&amp;lt; 53;&lt;/pre&gt;      这些Compositing Reason定义在文件external/chromium_org/third_party/WebKit/Source/platform/graphics/CompositingReasons.h中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      其中，有3个Compositing Reason比较特殊，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;const uint64_t CompositingReasonComboSquashableReasons =
    CompositingReasonOverlap
    | CompositingReasonAssumedOverlap
    | CompositingReasonOverflowScrollingParent;&lt;/pre&gt;       它们是CompositingReasonOverlap、CompositingReasonAssumedOverlap和CompositingReasonOverflowScrollingParent，称为Squashable Reason。WebKit不会为具有这三种特征的Render Layer之一的Render Layer创建Composited Layer Mapping。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       如果启用了Overlap Testing，那么WebKit会根据上述的Stacking Context顺序计算每一个Render Layer的后面是否有其它的Render Layer与其重叠。如果有，并且与其重叠的Render Layer有一个对应的Composited Layer Mapping，那么就会将位于上面的Render Layer的Compositing Reason设置为CompositingReasonOverlap。&lt;/p&gt; &lt;p&gt;       如果没有启用Overlap Testing，那么WebKit会根据上述的Stacking Context顺序检查每一个Render Layer的后面是否有一个具有Composited Layer Mapping的Render Layer。只要有，不管它们是否重叠，那么就会将位于上面的Render Layer的Compositing Reason设置为CompositingReasonAssumedOverlap。&lt;/p&gt; &lt;p&gt;       最后，如果一个Render Layer包含在一个具有overflow属性为&amp;quot;scroll&amp;quot;的Render Block中，并且该Render Block所对应的Render Layer具有Composited Layer Mapping，那么该Render Layer的Compositing Reason就会被设置为CompositingReasonOverflowScrollingParent。&lt;/p&gt; &lt;p&gt;       WebKit会将位于一个具有Composited Layer Mapping的Render Layer的上面的那些有着Squashable Reason的Render Layer绘制在同一个Graphics Layer中。这种Graphics Layer称为Squashing Graphics Layer。这种机制也相应地称为“Layer Squashing”。通过Layer Squashing机制，就可以在一定程度上减少Graphics Layer的数量，从而在一定程度上解决“Layer Explosion”问题。&lt;/p&gt; &lt;p&gt;       我们思考一下，为什么WebKit会将具有上述3种Compositing Reason的Render Layer绘制在一个Squashing Graphics Layer中？考虑具有CompositingReasonOverlap和CompositingReasonAssumedOverlap的Render Layer，当它们需要重绘，或者它们下面的具有Composited Layer Mapping的Render Layer重绘时，都不可避免地对它们以及它们下面的具有Composited Layer Mapping的Render Layer进行Compositing。这是由于它们相互之间存在重叠区域，只要其中一个发生变化，就会牵一发而动全身。类似地，当一个overflow属性为&amp;quot;scroll&amp;quot;的Render Block滚动时，包含在该Render Block内的Render Layer在执行完成重绘操作之后，需要参与到Compositing操作去。&lt;/p&gt; &lt;p&gt;       WebKit定义了两个函数，用来判断一个Render Layer是需要Compositing还是Squashing，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// Any reasons other than overlap or assumed overlap will require the layer to be separately compositing.
inline bool requiresCompositing(CompositingReasons reasons)
{
    return reasons &amp;amp; ~CompositingReasonComboSquashableReasons;
}

// If the layer has overlap or assumed overlap, but no other reasons, then it should be squashed.
inline bool requiresSquashing(CompositingReasons reasons)
{
    return !requiresCompositing(reasons) &amp;amp;&amp;amp; (reasons &amp;amp; CompositingReasonComboSquashableReasons);
}&lt;/pre&gt;      这两个函数定义在文件external/chromium_org/third_party/WebKit/Source/platform/graphics/CompositingReasons.h中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      参数reasons描述的是一个Render Layer的Compositing Reason，函数requiresCompositing判断该Render Layer是否需要Compositing，也就是是否要为该Render Layer创建一个Composited Layer Mapping，而函数requiresSquashing判断该Render Layer需要Squashing，也就是绘制在一个Squashing Graphics Layer中。&lt;/p&gt; &lt;p&gt;      除了Compositing Render Layer和Squashing Render Layer，剩下的其它Render Layer称为Non-Compositing Render Layer。这些Render Layer将会与离其最近的具有Composited Layer Mapping的父Render Layer绘制同样的Graphics Layer中。&lt;/p&gt; &lt;p&gt;      WebKit是根据Graphics Layer Tree来绘制网页内容的。在绘制一个Graphics Layer的时候，除了绘制Graphics Layer本身所有的内容之外，还会在Render Layer Tree中。找到与该Graphics Layer对应的Render Layer，并且从该Render Layer开始，将那些Non-Compositing类型的子Render Layer也一起绘制，直到遇到一个具有Composited Layer Mapping的子Render Layer为止。这个过程在后面的文章中分析网页内容的绘制过程时就会看到。&lt;/p&gt; &lt;p&gt;      前面提到，Composited Layer Mapping包含有若干个Graphics Layer，这些Graphics Layer在Composited Layer Mapping，也是形成一个Graphics Layer Sub Tree的，如图4所示：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="http://img.blog.csdn.net/20160219021246990"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;图4 Composited Layer Mapping&lt;/p&gt; &lt;p&gt;       图4的左边是一个Render Layer Tree。其中红色的Render Layer有对应的Composited Layer Mapping。每一个Composited Layer Mapping内部都有一个Graphics Layer Sub Tree。同时，这些Graphics Layer Sub Tree又会组合在一起，从而形成整个网页的Graphics Layer Tree。&lt;/p&gt; &lt;p&gt;       一个典型的Composited Layer Mapping对应的部分Graphics Layer Sub Tree如图5所示：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="http://img.blog.csdn.net/20160220032309201"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;图5 一个Composited Layer Mapping对应的Graphics Layer Sub Tree的一部分&lt;/p&gt; &lt;p&gt;       注意，图5描述的是仅仅是一个Composited Layer Mapping对应的Graphics Layer Sub Tree的一部分。例如，如果拥有该Composited Layer Mapping的Render Layer的上面存在Squashing Render Layer，那么上述Graphics Layer Sub Tree还包含有一个Squashing Graphics Layer。不过这一部分Graphics Layer Sub Tree已经足于让我们理解Composited Layer Mapping的组成。&lt;/p&gt; &lt;p&gt;       在图5中，只有Main Layer是必须存在的，它用来绘制一个Render Layer自身的内容。其它的Graphics Layer是可选，其中：&lt;/p&gt; &lt;p&gt;       1. 如果一个Render Layer被父Render Layer设置了裁剪区域，那么就会存在Clip Layer。&lt;/p&gt; &lt;p&gt;       2. 如果一个Render Layer为子Render Layer设置了裁剪区域，那么就会存在Children Clip Layer。&lt;/p&gt; &lt;p&gt;       3. 如果一个Render Layer是可滚动的，那么就会存在Scrolling Container。&lt;/p&gt; &lt;p&gt;       4. Negative z-order children、Normal flow children和Positive z-order children描述的是按照Stacking Context规则排序的子Render Layer对应的Composited Layer Mapping描述的Graphics Layer Sub Tree，它们均以父Render Layer的Scrolling Container为父Graphics Layer。&lt;/p&gt; &lt;p&gt;       5. 如果一个Render Layer是根Render Layer，并且它的背景被设置为固定的，即网页的body标签的CSS属性background-attachment被设置为“fixed”，那么就会存在Background Layer。&lt;/p&gt; &lt;p&gt;       6. 当Negative z-order children存在时，就会存在Foreground Layer。从前面描述的Stacking Context规则可以知道，Negative z-order childrenc对应的Graphics Layer Sub Tree先于当前Graphics Layer Sub Tree绘制。Negative z-order childrenc对应的Graphics Layer Sub Tree在绘制的时候可能会设置了偏移位置。这些偏移位置不能影响后面的Normal flow children和Positive z-order children对应的Graphics Layer Sub Tree的绘制，因此就需要在中间插入一个Foreground Layer，用来抵消Negative z-order children对应的Graphics Layer Sub Tree设置的偏移位置。&lt;/p&gt; &lt;p&gt;       7. 如果一个Render Layer的上面存在Squashing Render Layer，那么就会存在Squashing Layer。&lt;/p&gt; &lt;p&gt;       了解了Composited Layer Mapping对应的Graphics Layer Sub Tree的结构之后，接下来我们就可以结合源码分析网页的Graphics Layer Tree的创建过程了。网页的Graphics Layer Tree的创建主要是分三步进行：&lt;/p&gt; &lt;p&gt;       1. 计算各个Render Layer Tree中的Render Layer的Compositing Reason;&lt;/p&gt; &lt;p&gt;       2. 为有需要的Render Layer创建Composited Layer Mapping；&lt;/p&gt; &lt;p&gt;       3. 将各个Composited Layer Mapping描述Graphics Layer Sub Tree连接起来形成Graphics Layer Tree。&lt;/p&gt; &lt;p&gt;       上述过程主要是发生在网页的Layout过程中。对网页进行Layout是网页渲染过程的一个重要步骤，以后我们分析网页的渲染过程时就会看到这一点。在WebKit中，每一个正在加载的网页都关联有一个FrameView对象。当需要对网页进行Layout时，就会调用这个FrameView对象的成员函数updateLayoutAndStyleForPainting，它的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void FrameView::updateLayoutAndStyleForPainting()
{
    // Updating layout can run script, which can tear down the FrameView.
    RefPtr&amp;lt;FrameView&amp;gt; protector(this);

    updateLayoutAndStyleIfNeededRecursive();

    if (RenderView* view = renderView()) {
        ......

        view-&amp;gt;compositor()-&amp;gt;updateIfNeededRecursive();

        ......
    }

    ......
}&lt;/pre&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/FrameView.cpp中。&lt;/p&gt; &lt;p&gt;      FrameView类的成员函数updateLayoutAndStyleForPainting是通过调用另外一个成员函数updateLayoutAndStyleIfNeededRecursive对网页进行Layout的。&lt;/p&gt; &lt;p&gt;      执行完成Layout操作之后，FrameView类的成员函数updateLayoutAndStyleForPainting又调用成员函数renderView获得一个RenderView对象。从前面  &lt;a href="http://blog.csdn.net/luoshengyang/article/details/50558942" target="_blank"&gt;Chromium网页DOM Tree创建过程分析&lt;/a&gt;一文可以知道，网页的DOM Tree的根节点对应的Render Object就是一个RenderView对象。因此，前面获得的RenderView对象描述的就是正在加载的网页的Render Layer Tree的根节点。&lt;/p&gt; &lt;p&gt;      再接下来，FrameView类的成员函数updateLayoutAndStyleForPainting又调用上述RenderView对象的成员函数compositor获得一个RenderLayerCompositor对象，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;RenderLayerCompositor* RenderView::compositor()
{
    if (!m_compositor)
        m_compositor = adoptPtr(new RenderLayerCompositor(*this));

    return m_compositor.get();
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。 &lt;p&gt;      这个RenderLayerCompositor对象负责管理网页的Render Layer Tree，以及根据Render Layer Tree创建Graphics Layer Tree。&lt;/p&gt; &lt;p&gt;      回到FrameView类的成员函数updateLayoutAndStyleForPainting中，它获得了正在加载的网页对应的RenderLayerCompositor对象之后，接下来就调用这个RenderLayerCompositor对象的成员函数updateIfNeededRecursive根据Render Layer Tree创建或者更新Graphics Layer Tree。&lt;/p&gt; &lt;p&gt;      RenderLayerCompositor类的成员函数updateIfNeededRecursive的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void RenderLayerCompositor::updateIfNeededRecursive()
{
    ......

    updateIfNeeded();
    
    ......
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;br /&gt; &lt;p&gt;      RenderLayerCompositor类的成员函数updateIfNeededRecursive调用另外一个成员函数updateIfNeeded创建Graphics Layer Tree，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void RenderLayerCompositor::updateIfNeeded()
{
    CompositingUpdateType updateType = m_pendingUpdateType;
    m_pendingUpdateType = CompositingUpdateNone;

    if (!hasAcceleratedCompositing() || updateType == CompositingUpdateNone)
        return;

    RenderLayer* updateRoot = rootRenderLayer();

    Vector&amp;lt;RenderLayer*&amp;gt; layersNeedingRepaint;

    if (updateType &amp;gt;= CompositingUpdateAfterCompositingInputChange) {
        bool layersChanged = false;
        ......

        CompositingRequirementsUpdater(m_renderView, m_compositingReasonFinder).update(updateRoot);

        {
            ......
            CompositingLayerAssigner(this).assign(updateRoot, layersChanged, layersNeedingRepaint);
        }

        ......

        if (layersChanged)
            updateType = std::max(updateType, CompositingUpdateRebuildTree);
    }

    if (updateType != CompositingUpdateNone) {
        ......
        GraphicsLayerUpdater updater;
        updater.update(layersNeedingRepaint, *updateRoot);

        if (updater.needsRebuildTree())
            updateType = std::max(updateType, CompositingUpdateRebuildTree);

        ......
    }

    if (updateType &amp;gt;= CompositingUpdateRebuildTree) {
        GraphicsLayerVector childList;
        {
            ......
            GraphicsLayerTreeBuilder().rebuild(*updateRoot, childList);
        }

        if (childList.isEmpty())
            destroyRootLayer();
        else
            m_rootContentLayer-&amp;gt;setChildren(childList);

        ......
    }

    ......
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      当RenderLayerCompositor类的成员变量pendingUpdateType的值不等于CompositingUpdateNone的时候，就表明网页的Graphics Layer Tree需要进行更新。此外，要对网页的Graphics Layer Tree需要进行更新，还要求浏览器开启硬件加速合成。当调用RenderLayerCompositor类的成员函数hasAcceleratedCompositing得到的返回值等于true的时候，就表明浏览器开启了硬件加速合成。&lt;/p&gt; &lt;p&gt;      当RenderLayerCompositor类的成员变量pendingUpdateType的值大于等于CompositingUpdateAfterCompositingInputChange的时候，表示Graphics Layer Tree的输入发生了变化，例如，Render Layer Tree中的某一个Render Layer的内容发生了变化。在这种情况下，RenderLayerCompositor类的成员函数updateIfNeeded会构造一个CompositingRequirementsUpdater对象，并且调用这个CompositingRequirementsUpdater对象的成员函数update从网页的Render Layer Tree的根节点开始，递归计算每一个Render Layer的Compositing Reason，主要就是根据各个Render Layer包含的Render Object的CSS属性来计算。  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;      计算好网页的Render Layer Tree中的每一个Render Layer的Compositing Reason之后，RenderLayerCompositor类的成员函数updateIfNeeded接着再构造一个CompositingLayerAssigner对象，并且调用这个CompositingLayerAssigner对象的成员函数assign根据每一个Render Layer新的Compositing Reason决定是否需要为它创建一个新的Composited Layer Mapping或者删除它原来拥有的Composited Layer Mapping。如果有的Render Layer原来是没有Composited Layer Mapping的，现在有了Composited Layer Mapping，或者原来有Composited Layer Mapping，现在没有了Composited Layer Mapping，那么本地变量layersChanged的值就会被设置为true。这时候本地变量updateType的值会被更新为CompositingUpdateRebuildTree。&lt;/p&gt; &lt;p&gt;       在本地变量updateType的值是否不等于CompositingUpdateNone的情况下，RenderLayerCompositor类的成员函数updateIfNeeded接下来又会构造一个GraphicsLayerUpdater对象，并且调用这个GraphicsLayerUpdater对象的成员函数update检查每一个拥有Composited Layer Mapping的Render Layer更新它的Composited Layer Mapping所描述的Graphics Layer Sub Tree。如果有Render Layer更新了它的Composited Layer Mapping所描述的Graphics Layer Sub Tree，那么调用上述GraphicsLayerUpdater对象的成员函数needsRebuildTree获得的返回值就会等于true。这时候本地变量updateType的值也会被更新为CompositingUpdateRebuildTree。&lt;/p&gt; &lt;p&gt;       一旦本地变量updateType的值被更新为CompositingUpdateRebuildTree，或者它本来的值，也就是RenderLayerCompositor类的成员变量pendingUpdateType的值，原本就等于CompositingUpdateRebuildTree，那么RenderLayerCompositor类的成员函数updateIfNeeded又会构造一个GraphicsLayerTreeBuilder对象，并且调用这个GraphicsLayerTreeBuilder对象的成员函数rebuild从Render Layer Tree的根节点开始，递归创建一个新的Graphics Layer Tree。&lt;/p&gt; &lt;p&gt;       注意，前面调用GraphicsLayerTreeBuilder类的成员函数rebuild的时候，传递进去的第一个参数updateRoot是Render Layer Tree的根节点，第二个参数childList是一个输出参数，它里面保存的是Graphics Layer Tree的根节点的子节点。Graphics Layer Tree的根节点由RenderLayerCompositor类的成员函数m_rootContentLayer指向的GraphicsLayer对象描述，因此当参数childList描述的Vector不为空时，它里面所保存的Graphics Layer都会被设置为RenderLayerCompositor类的成员函数m_rootContentLayer指向的GraphicsLayer对象的子Graphics Layer。&lt;/p&gt; &lt;p&gt;       接下来我们主要分析CompositingLayerAssigner类的成员函数assign、GraphicsLayerUpdater类的成员函数update以及GraphicsLayerTreeBuilder类的成员函数rebuild的实现，以及了解Graphics Layer Tree的创建过程。&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员函数assign主要是为Render Layer创建或者删除Composited Layer Mapping，它的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void CompositingLayerAssigner::assign(RenderLayer* updateRoot, bool&amp;amp; layersChanged, Vector&amp;lt;RenderLayer*&amp;gt;&amp;amp; layersNeedingRepaint)
{
    SquashingState squashingState;
    assignLayersToBackingsInternal(updateRoot, squashingState, layersChanged, layersNeedingRepaint);
    ......
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositingLayerAssigner.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       从前面的分析可以知道，参数updateRoot描述的是Render Layer Tree的根节点，CompositingLayerAssigner类的成员函数assign主要是调用另外一个成员函数assignLayersToBackingsInternal从这个根节点开始，递归是否需要为每一个Render Layer创建或者删除Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void CompositingLayerAssigner::assignLayersToBackingsInternal(RenderLayer* layer, SquashingState&amp;amp; squashingState, bool&amp;amp; layersChanged, Vector&amp;lt;RenderLayer*&amp;gt;&amp;amp; layersNeedingRepaint)
{
    ......

    CompositingStateTransitionType compositedLayerUpdate = computeCompositedLayerUpdate(layer);

    if (m_compositor-&amp;gt;allocateOrClearCompositedLayerMapping(layer, compositedLayerUpdate)) {
        layersNeedingRepaint.append(layer);
        layersChanged = true;
    }

    // Add this layer to a squashing backing if needed.
    if (m_layerSquashingEnabled) {
        if (updateSquashingAssignment(layer, squashingState, compositedLayerUpdate, layersNeedingRepaint))
            layersChanged = true;

        ......
    }

    if (layer-&amp;gt;stackingNode()-&amp;gt;isStackingContext()) {
        RenderLayerStackingNodeIterator iterator(*layer-&amp;gt;stackingNode(), NegativeZOrderChildren);
        while (RenderLayerStackingNode* curNode = iterator.next())
            assignLayersToBackingsInternal(curNode-&amp;gt;layer(), squashingState, layersChanged, layersNeedingRepaint);
    }

    if (m_layerSquashingEnabled) {
        // At this point, if the layer is to be &amp;quot;separately&amp;quot; composited, then its backing becomes the most recent in paint-order.
        if (layer-&amp;gt;compositingState() == PaintsIntoOwnBacking || layer-&amp;gt;compositingState() == HasOwnBackingButPaintsIntoAncestor) {
            ASSERT(!requiresSquashing(layer-&amp;gt;compositingReasons()));
            squashingState.updateSquashingStateForNewMapping(layer-&amp;gt;compositedLayerMapping(), layer-&amp;gt;hasCompositedLayerMapping());
        }
    }

    RenderLayerStackingNodeIterator iterator(*layer-&amp;gt;stackingNode(), NormalFlowChildren | PositiveZOrderChildren);
    while (RenderLayerStackingNode* curNode = iterator.next())
        assignLayersToBackingsInternal(curNode-&amp;gt;layer(), squashingState, layersChanged, layersNeedingRepaint);

    ......
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositingLayerAssigner.cpp中。 &lt;br /&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal首先调用成员函数computeCompositedLayerUpdate计算参数layer描述的Render Layer的Compositing State Transition，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;CompositingStateTransitionType CompositingLayerAssigner::computeCompositedLayerUpdate(RenderLayer* layer)
{
    CompositingStateTransitionType update = NoCompositingStateChange;
    if (needsOwnBacking(layer)) {
        if (!layer-&amp;gt;hasCompositedLayerMapping()) {
            update = AllocateOwnCompositedLayerMapping;
        }
    } else {
        if (layer-&amp;gt;hasCompositedLayerMapping())
            update = RemoveOwnCompositedLayerMapping;

        if (m_layerSquashingEnabled) {
            if (!layer-&amp;gt;subtreeIsInvisible() &amp;amp;&amp;amp; requiresSquashing(layer-&amp;gt;compositingReasons())) {
                // We can&amp;apos;t compute at this time whether the squashing layer update is a no-op,
                // since that requires walking the render layer tree.
                update = PutInSquashingLayer;
            } else if (layer-&amp;gt;groupedMapping() || layer-&amp;gt;lostGroupedMapping()) {
                update = RemoveFromSquashingLayer;
            }
        }
    }
    return update;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositingLayerAssigner.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       一个Render Layer的Compositing State Transition分为4种：&lt;/p&gt; &lt;p&gt;       1. 它需要Compositing，但是还没有创建Composited Layer Mapping，这时候Compositing State Transition设置为AllocateOwnCompositedLayerMapping，表示要创建一个新的Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       2. 它不需要Compositing，但是之前已经创建有Composited Layer Mapping，这时候Compositing State Transition设置为RemoveOwnCompositedLayerMapping，表示要删除之前创建的Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       3. 它需要Squashing，这时候Compositing State Transition设置为PutInSquashingLayer，表示要将它绘制离其最近的一个Render Layer的Composited Layer Mapping里面的一个Squashing Layer上。&lt;/p&gt; &lt;p&gt;       4. 它不需要Squashing，这时候Compositing State Transition设置为RemoveFromSquashingLayer，表示要将它从原来对应的Squashing Layer上删除。&lt;/p&gt; &lt;p&gt;       注意，后面2种Compositing State Transition，只有在CompositingLayerAssigner类的成员变量layerSquashingEnabled的值在true的时候才会进行设置。默认情况下，浏览器是开启Layer Squashing机制的，不过可以通过设置“disable-layer-squashing”选项进行关闭，或者通过设置“enable-layer-squashing”选项显式开启。&lt;/p&gt; &lt;p&gt;       判断一个Render Layer是否需要Compositing，是通过调用CompositingLayerAssigner类的成员函数needsOwnBacking进行的，它的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool CompositingLayerAssigner::needsOwnBacking(const RenderLayer* layer) const
{
    if (!m_compositor-&amp;gt;canBeComposited(layer))
        return false;

    // If squashing is disabled, then layers that would have been squashed should just be separately composited.
    bool needsOwnBackingForDisabledSquashing = !m_layerSquashingEnabled &amp;amp;&amp;amp; requiresSquashing(layer-&amp;gt;compositingReasons());

    return requiresCompositing(layer-&amp;gt;compositingReasons()) || needsOwnBackingForDisabledSquashing || (m_compositor-&amp;gt;staleInCompositingMode() &amp;amp;&amp;amp; layer-&amp;gt;isRootLayer());
}&lt;/pre&gt; &lt;p&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositingLayerAssigner.cpp中。&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员变量m_compositor指向的是一个RenderLayerCompositor对象，CompositingLayerAssigner类的成员函数needsOwnBacking首先调用它的成员函数canBeComposited判断参数layer描述的Render Layer是否需要Compositing，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool RenderLayerCompositor::canBeComposited(const RenderLayer* layer) const
{
    // FIXME: We disable accelerated compositing for elements in a RenderFlowThread as it doesn&amp;apos;t work properly.
    // See http://webkit.org/b/84900 to re-enable it.
    return m_hasAcceleratedCompositing &amp;amp;&amp;amp; layer-&amp;gt;isSelfPaintingLayer() &amp;amp;&amp;amp; !layer-&amp;gt;subtreeIsInvisible() &amp;amp;&amp;amp; layer-&amp;gt;renderer()-&amp;gt;flowThreadState() == RenderObject::NotInsideFlowThread;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       一个Render Layer可以Compositing，需要同时满足以下4个条件：&lt;/p&gt; &lt;p&gt;       1. 浏览器开启硬件加速合成，即RenderLayerCompositor类的成员变量m_hasAcceleratedCompositing的值等于true。&lt;/p&gt; &lt;p&gt;       2. Render Layer本身有Compositing绘制的需求，也就是调用它的成员函数isSelfPaitingLayer得到的返回值为true。从前面  &lt;a href="http://blog.csdn.net/luoshengyang/article/details/50648792" target="_blank"&gt;Chromium网页Render Layer Tree创建过程分析&lt;/a&gt;一文可以知道，Render Layer的类型一般为NormalLayer，但是如果将overflow属性设置为“hidden‘，那么Render Layer的类型被设置为OverflowClipLayer。对于类型为OverflowClipLayer的Render Layer，如果它的内容没有出现overflow，那么就没有必要对它进行Compositing。&lt;/p&gt; &lt;p&gt;       3. Render Layer描述的网页内容是可见的，也就是调用它的成员函数subtreeIsInvisible得到的返回值等于false。&lt;/p&gt; &lt;p&gt;       4. Render Layer的内容不是渲染在一个RenderFlowThread中，也就是与Render Layer关联的Render Object的Flow Thread State等于RenderObject::NotInsideFlowThread。从注释可以知道，在RenderFlowThread中渲染的元素是禁用硬件加速合成的，因为不能正确地使用。RenderFlowThread是CSS 3定义的一种元素显示方式，更详细的信息可以参考CSS文档：  &lt;a href="https://drafts.csswg.org/css-regions-1/#named-flow" target="_blank"&gt;CSS Regions Module Level 1&lt;/a&gt;。&lt;/p&gt; &lt;p&gt;       回到CompositingLayerAssigner类的成员函数needsOwnBacking中，如果RenderLayerCompositor类的成员函数canBeComposited告诉它参数layer描述的Render Layer不可进行Compositing，那么就不需要为它创建一个Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       另一方面，如果RenderLayerCompositor类的成员函数canBeComposited告诉CompositingLayerAssigner类的成员函数needsOwnBacking，参数layer描述的Render Layer可以进行Compositing，那么CompositingLayerAssigner类的成员函数needsOwnBacking还需要进一步判断该Render Layer是否真的需要进行Compositing。&lt;/p&gt; &lt;p&gt;       如果参数layer描述的Render Layer满足以下3个条件之一，那么CompositingLayerAssigner类的成员函数needsOwnBacking就会认为它需要进行Compositing：&lt;/p&gt; &lt;p&gt;       1. Render Layer的Compositing Reason表示它需要Compositing，这是通过调用前面提到的函数requiresCompositing判断的。&lt;/p&gt; &lt;p&gt;       2. Render Layer的Compositing Reason表示它需要Squashing，但是浏览器禁用了“Layer Squashing”机制。当浏览器禁用“Layer Squashing”机制时，CompositingLayerAssigner类的成员变量m_layerSquashingEnabled会等于false。调用前面提到的函数requiresSquashing可以判断一个Render Layer是否需要Squashing。&lt;/p&gt; &lt;p&gt;       3. Render Layer是Render Layer Tree的根节点，并且Render Layer Compositor处于Compositing模式中。除非设置了Render Layer Tree的根节点无条件Compositing，否则的话，当在Render Layer Tree根节点的子树中，没有任何Render Layer需要Compositing时， Render Layer Tree根节点也不需要Compositing，这时候Render Layer Compositor就会被设置为非Compositing模式。判断一个Render Layer是否是Render Layer Tree的根节点，调用它的成员函数isRootLayer即可，而判断一个Render Layer Compositor是否处于Compositing模式，调用它的成员函数staleInCompositingMode即可。&lt;/p&gt; &lt;p&gt;       回到CompositingLayerAssigner类的成员函数computeCompositedLayerUpdate中，当它调用结束后，再返回到CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal中，这时候CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal就知道了参数layer描述的Render Layer的Compositing State Transition Type。&lt;/p&gt; &lt;p&gt;       知道了参数layer描述的Render Layer的Compositing State Transition Type之后，CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal接下来调用成员变量m_compositor描述的一个RenderLayerCompositor对象的成员函数allocateOrClearCompositedLayerMapping为其创建或者删除Composited Layer Mapping，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool RenderLayerCompositor::allocateOrClearCompositedLayerMapping(RenderLayer* layer, const CompositingStateTransitionType compositedLayerUpdate)
{
    bool compositedLayerMappingChanged = false;
    ......

    switch (compositedLayerUpdate) {
    case AllocateOwnCompositedLayerMapping:
        ......

        layer-&amp;gt;ensureCompositedLayerMapping();
        compositedLayerMappingChanged = true;

        ......
        break;
    case RemoveOwnCompositedLayerMapping:
    // PutInSquashingLayer means you might have to remove the composited layer mapping first.
    case PutInSquashingLayer:
        if (layer-&amp;gt;hasCompositedLayerMapping()) {
            ......

            layer-&amp;gt;clearCompositedLayerMapping();
            compositedLayerMappingChanged = true;
        }

        break;
    case RemoveFromSquashingLayer:
    case NoCompositingStateChange:
        // Do nothing.
        break;
    }

    ......

    return compositedLayerMappingChanged || nonCompositedReasonChanged;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       RenderLayerCompositor类的成员函数allocateOrClearCompositedLayerMapping主要是根据Render Layer的Compositing State Transition Type决定是要为其创建Composited Layer Mapping，还是删除mposited Layer Mapping。&lt;/p&gt; &lt;p&gt;       对于Compositing State Transition Type等于AllocateOwnCompositedLayerMapping的Render Layer，RenderLayerCompositor类的成员函数allocateOrClearCompositedLayerMapping会调用它的成员函数ensureCompositedLayerMapping为其创建一个Composited Layer Mapping&lt;/p&gt; &lt;p&gt;       对于Compositing State Transition Type等于RemoveOwnCompositedLayerMapping或者PutInSquashingLayer的Render Layer，RenderLayerCompositor类的成员函数allocateOrClearCompositedLayerMapping会调用它的成员函数clearCompositedLayerMapping删除原来为它创建的Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       对于Compositing State Transition Type其它值的Render Layer，则不需要进行特别的处理。&lt;/p&gt; &lt;p&gt;       这一步执行完成之后，回到CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal中，它接下来判断成员变量m_layerSquashingEnabled的值是否等于true。如果等于true，那么就说明浏览器开启了”Layer Squashing“机制。这时候就需要调用成员函数updateSquashingAssignment判断是否需要将参数layer描述的Render Layer绘制在一个Squashing Graphics Layer中。&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员函数updateSquashingAssignment的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool CompositingLayerAssigner::updateSquashingAssignment(RenderLayer* layer, SquashingState&amp;amp; squashingState, const CompositingStateTransitionType compositedLayerUpdate,
    Vector&amp;lt;RenderLayer*&amp;gt;&amp;amp; layersNeedingRepaint)
{
    ......
    if (compositedLayerUpdate == PutInSquashingLayer) {
        ......

        bool changedSquashingLayer =
            squashingState.mostRecentMapping-&amp;gt;updateSquashingLayerAssignment(layer, squashingState.mostRecentMapping-&amp;gt;owningLayer(), squashingState.nextSquashedLayerIndex);
        ......

        return true;
    }
    if (compositedLayerUpdate == RemoveFromSquashingLayer) {
        if (layer-&amp;gt;groupedMapping()) {
            ......
            layer-&amp;gt;setGroupedMapping(0);
        }

        ......
        return true;
    }

    return false;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositingLayerAssigner.cpp中。 &lt;br /&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;       CompositingLayerAssigner类的成员函数updateSquashingAssignment也是根据Render Layer的Compositing State Transition Type决定是否要将它绘制在一个Squashing Graphics Layer中，或者将它从一个Squashing Graphics Layer中删除。&lt;/p&gt; &lt;p&gt;       对于Compositing State Transition Type等于PutInSquashingLayer的Render Layer，它将会绘制在一个Squashing Graphics Layer中。这个Squashing Graphics Layer保存在一个Composited Layer Mapping中。这个Composited Layer Mapping关联的Render Layer处于要Squashing的Render Layer的下面，并且前者离后者是最近的，记录在参数squashingState描述的一个SquashingState对象的成员变量mostRecentMapping中。通过调用CompositedLayerMapping类的成员函数updateSquashingLayerAssignment可以将一个Render Layer绘制在一个Composited Layer Mapping内部维护的一个quashing Graphics Layer中。&lt;/p&gt; &lt;p&gt;       对于Compositing State Transition Type等于RemoveFromSquashingLayer的Render Layer，如果它之前已经被设置绘制在一个Squashing Graphics Layer中，那么就需要将它从这个Squashing Graphics Layer中删除。如果一个Render Layer之前被设置绘制在一个Squashing Graphics Layer中，那么调用它的成员函数groupedMapping就可以获得一个Grouped Mapping。这个Grouped Mapping描述的也是一个Composited Layer Mapping，并且Render Layer所绘制在的Squashing Graphics Layer就是由这个Composited Layer Mapping维护的。因此，要将一个Render Layer从一个Squashing Graphics Layer中删除，只要将它的Grouped Mapping设置为0即可。这是通过调用RenderLayer类的成员函数setGroupedMapping实现的。&lt;/p&gt; &lt;p&gt;       再回到CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal中，它接下来判断参数layer描述的Render Layer所关联的Render Object是否是一个Stacking Context。如果是的话，那么就递归调用成员函数assignLayersToBackingsInternal遍历那些z-index为负的子Render Object对应的Render Layer，确定是否需要为它们创建Composited Layer Mapping。&lt;/p&gt; &lt;p&gt;       到目前为止，CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal就处理完成参数layer描述的Render Layer，以及那些z-index为负数的子Render Layer。这时候，参数layer描述的Render Layer可能会作为那些z-index为0或者正数的子Render Layer的Grouped Mapping，因此在继续递归处理z-index为0或者正数的子Render Layer之前，CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal需要将参数layer描述的Render Layer对应的Composited Layer Mapping记录下来，前提是这个Render Layer拥有Composited Layer Mapping。这是通过调用参数squashingState描述的一个SquashingState对象的成员函数updateSquashingStateForNewMapping实现的，实际上就是记录在该SquashingState对象的成员变量mostRecentMapping中。这样前面分析的CompositingLayerAssigner类的成员函数updateSquashingAssignment就可以知道将其参数layer描述的Render Layer绘制在哪一个Squashing Graphics Layer中。&lt;/p&gt; &lt;p&gt;       最后，CompositingLayerAssigner类的成员函数assignLayersToBackingsInternal就递归调用自己处理那些z-index为0或者正数的子Render Layer。递归调有完成之后，整个Render Layer Tree就处理完毕了。这时候哪些Render Layer具有Composited Layer Mapping就可以确定了。&lt;/p&gt; &lt;p&gt;       前面分析RenderLayerCompositor类的成员函数allocateOrClearCompositedLayerMapping时提到，调用RenderLayer类的成员函数ensureCompositedLayerMapping可以为一个Render Layer创建一个Composited Layer Mapping，接下来我们就继续分析这个函数的实现，以便了解Composited Layer Mapping的创建过程。&lt;/p&gt; &lt;p&gt;       RenderLayer类的成员函数ensureCompositedLayerMapping的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;CompositedLayerMappingPtr RenderLayer::ensureCompositedLayerMapping()
{
    if (!m_compositedLayerMapping) {
        m_compositedLayerMapping = adoptPtr(new CompositedLayerMapping(*this));
        ......
    }
    return m_compositedLayerMapping.get();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       RenderLayer类的成员变量compositedLayerMapping描述的就是一个Composited Layer Mapping。如果这个Composited Layer Mapping还没有创建，那么当RenderLayer类的成员函数ensureCompositedLayerMapping被调用时，就会进行创建。&lt;/p&gt; &lt;p&gt;       Composited Layer Mapping的创建过程，也就是CompositedLayerMapping类的构造函数的实现，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;CompositedLayerMapping::CompositedLayerMapping(RenderLayer&amp;amp; layer)
    : m_owningLayer(layer)
    , ......
{
    ......

    createPrimaryGraphicsLayer();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       正在创建的Composited Layer Mapping被参数layer描述的Render Layer拥有，因此这个Render Layer将会被保存在在创建的Composited Layer Mapping的成员变量m_owningLayer中。&lt;/p&gt; &lt;p&gt;       从图5可以知道，一个Composited Layer Mapping一定存在一个Main Graphics Layer。这个Main Graphics Layer是CompositedLayerMapping类的构造函数通过调用另外一个成员函数createPrimaryGraphicsLayer创建的，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void CompositedLayerMapping::createPrimaryGraphicsLayer()
{
    m_graphicsLayer = createGraphicsLayer(m_owningLayer.compositingReasons());

    ......
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       Composited Layer Mapping中的Main Graphics Layer由成员变量m_graphicsLayer描述，并且这个Main Graphics Layer是通过调用成员函数createGraphicsLayer创建的。如果我们分析CompositedLayerMapping类的其它代码，就会发现Composited Layer Mapping中的其它Graphics Layer也是通过调用成员函数createGraphicsLayer创建的。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数createGraphicsLayer的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;PassOwnPtr&amp;lt;GraphicsLayer&amp;gt; CompositedLayerMapping::createGraphicsLayer(CompositingReasons reasons)
{
    GraphicsLayerFactory* graphicsLayerFactory = 0;
    if (Page* page = renderer()-&amp;gt;frame()-&amp;gt;page())
        graphicsLayerFactory = page-&amp;gt;chrome().client().graphicsLayerFactory();

    OwnPtr&amp;lt;GraphicsLayer&amp;gt; graphicsLayer = GraphicsLayer::create(graphicsLayerFactory, this);

    graphicsLayer-&amp;gt;setCompositingReasons(reasons);
    if (Node* owningNode = m_owningLayer.renderer()-&amp;gt;generatingNode())
        graphicsLayer-&amp;gt;setOwnerNodeId(InspectorNodeIds::idForNode(owningNode));

    return graphicsLayer.release();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数createGraphicsLayer在创建一个Graphics Layer之前，首先会获得一个GraphicsLayerFactory对象。这个GraphicsLayerFactory对象是由WebKit的使用者提供的。在我们这个情景中，WebKit的使用者就是Chromium，它提供的GraphicsLayerFactory对象的实际类型为GraphicsLayerFactoryChromium。&lt;/p&gt; &lt;p&gt;       获得了GraphicsLayerFactory对象之后，CompositedLayerMapping类的成员函数createGraphicsLayer接下来就以它为参数，调用GraphicsLayer类的静态成员函数create创建一个Graphics Layer，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;PassOwnPtr&amp;lt;GraphicsLayer&amp;gt; GraphicsLayer::create(GraphicsLayerFactory* factory, GraphicsLayerClient* client)
{
    return factory-&amp;gt;createGraphicsLayer(client);
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/platform/graphics/GraphicsLayer.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       GraphicsLayer类的静态成员函数create调用参数factory描述的一个GraphicsLayerFactoryChromium对象的成员函数createGraphicsLayer创建一个Graphics Layer，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;PassOwnPtr&amp;lt;GraphicsLayer&amp;gt; GraphicsLayerFactoryChromium::createGraphicsLayer(GraphicsLayerClient* client)
{
    OwnPtr&amp;lt;GraphicsLayer&amp;gt; layer = adoptPtr(new GraphicsLayer(client));
    ......
    return layer.release();
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/GraphicsLayerFactoryChromium.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      从这里可以看到，GraphicsLayerFactoryChromium类的成员函数createGraphicsLayer返回的是一个GraphicsLayer对象。&lt;/p&gt; &lt;p&gt;      这一步执行完成后，一个Composited Layer Mapping及其内部的Main Graphics Layer就创建完成了。&lt;/p&gt; &lt;p&gt;      前面分析CompositingLayerAssigner类的成员函数updateSquashingAssignment时提到，调用CompositedLayerMapping类的成员函数updateSquashingLayerAssignment可以将一个Render Layer绘制在其内部维护的一个Squashing Graphics Layer中。CompositedLayerMapping类的成员函数updateSquashingLayerAssignment的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool CompositedLayerMapping::updateSquashingLayerAssignment(RenderLayer* squashedLayer, const RenderLayer&amp;amp; owningLayer, size_t nextSquashedLayerIndex)
{
    ......

    GraphicsLayerPaintInfo paintInfo;
    paintInfo.renderLayer = squashedLayer;
    ......

    // Change tracking on squashing layers: at the first sign of something changed, just invalidate the layer.
    // FIXME: Perhaps we can find a tighter more clever mechanism later.
    bool updatedAssignment = false;
    if (nextSquashedLayerIndex &amp;lt; m_squashedLayers.size()) {
        if (!paintInfo.isEquivalentForSquashing(m_squashedLayers[nextSquashedLayerIndex])) {
            ......
            updatedAssignment = true;
            m_squashedLayers[nextSquashedLayerIndex] = paintInfo;
        }
    } else {
        ......
        m_squashedLayers.append(paintInfo);
        updatedAssignment = true;
    }
    squashedLayer-&amp;gt;setGroupedMapping(this);
    return updatedAssignment;
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/GraphicsLayerFactoryChromium.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      CompositedLayerMapping类有一个成员变量m_squashedLayers，它描述的是一个类型为GraphicsLayerPaintInfo的Vector，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;class CompositedLayerMapping FINAL : public GraphicsLayerClient {
    WTF_MAKE_NONCOPYABLE(CompositedLayerMapping); WTF_MAKE_FAST_ALLOCATED;
    ......

private:
    ......

    OwnPtr&amp;lt;GraphicsLayer&amp;gt; m_squashingLayer; // Only used if any squashed layers exist, this is the backing that squashed layers paint into.
    Vector&amp;lt;GraphicsLayerPaintInfo&amp;gt; m_squashedLayers;
    
    ......
};&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/GraphicsLayerFactoryChromium.h中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       上述Vector中保存在的每一个GraphicsLayerPaintInfo描述的都是一个Squashing Render Layer，这些Squashing Render Layer最终将会绘制在CompositedLayerMapping类的成员变量m_squashingLayer描述的一个Graphics Layer中。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数updateSquashingLayerAssignment所做的事情就是将参数squashedLayer描述的一个Squashing Render Layer封装在一个GraphicsLayerPaintInfo对象，然后将这个GraphicsLayerPaintInfo对象保存在CompositedLayerMapping类的成员变量m_squashedLayers描述的一个Vector中。&lt;/p&gt; &lt;p&gt;       这样，我们就分析完成了为Render Layer Tree中的Render Layer创建Composited Layer Mapping的过程，也就是CompositingLayerAssigner类的成员函数assign的实现。回到RenderLayerCompositor类的成员函数updateIfNeeded中，它接下来调用GraphicsLayerUpdater类的成员函数update为Composited Layer Mapping创建Graphics Layer Sub Tree。&lt;/p&gt; &lt;p&gt;       GraphicsLayerUpdater类的成员函数update的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void GraphicsLayerUpdater::update(Vector&amp;lt;RenderLayer*&amp;gt;&amp;amp; layersNeedingPaintInvalidation, RenderLayer&amp;amp; layer, UpdateType updateType, const UpdateContext&amp;amp; context)
{
    if (layer.hasCompositedLayerMapping()) {
        CompositedLayerMappingPtr mapping = layer.compositedLayerMapping();

        ......

        if (mapping-&amp;gt;updateGraphicsLayerConfiguration(updateType))
            m_needsRebuildTree = true;

        ......
    }

    UpdateContext childContext(context, layer);
    for (RenderLayer* child = layer.firstChild(); child; child = child-&amp;gt;nextSibling())
        update(layersNeedingPaintInvalidation, *child, updateType, childContext);
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/GraphicsLayerUpdater.cpp。 &lt;br /&gt; &lt;p&gt;       从前面的调用过程可以知道，参数layer描述的Render Layer是Render Layer Tree的根节点，GraphicsLayerUpdater类的成员函数update检查它是否拥有Composited Layer Mapping。如果有的话，那么就会调用这个Composited Layer Mapping的成员函数updateGraphicsLayerConfiguration更新它内部的Graphics Layer Sub Tree。GraphicsLayerUpdater类的成员函数update最后还会递归调用自身遍历Render Layer Tree的根节点的子孙节点，这样就可以对所有的Graphics Layer Sub Tree进行更新。&lt;/p&gt; &lt;p&gt;       接下来我们继续分析CompositedLayerMapping类的成员函数updateGraphicsLayerConfiguration的实现，以便了解每一个Graphics Layer Sub Tree的更新过程，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool CompositedLayerMapping::updateGraphicsLayerConfiguration(GraphicsLayerUpdater::UpdateType updateType)
{
    ......

    bool layerConfigChanged = false;
    ......

    // The background layer is currently only used for fixed root backgrounds.
    if (updateBackgroundLayer(m_backgroundLayerPaintsFixedRootBackground))
        layerConfigChanged = true;

    if (updateForegroundLayer(compositor-&amp;gt;needsContentsCompositingLayer(&amp;amp;m_owningLayer)))
        layerConfigChanged = true;

    bool needsDescendantsClippingLayer = compositor-&amp;gt;clipsCompositingDescendants(&amp;amp;m_owningLayer);
    ......

    bool needsAncestorClip = compositor-&amp;gt;clippedByNonAncestorInStackingTree(&amp;amp;m_owningLayer);
    ......

    if (updateClippingLayers(needsAncestorClip, needsDescendantsClippingLayer))
        layerConfigChanged = true;

    ......

    if (updateScrollingLayers(m_owningLayer.needsCompositedScrolling())) {
        layerConfigChanged = true;
        ......
    }

    bool hasPerspective = false;
    if (RenderStyle* style = renderer-&amp;gt;style())
        hasPerspective = style-&amp;gt;hasPerspective();
    bool needsChildTransformLayer = hasPerspective &amp;amp;&amp;amp; (layerForChildrenTransform() == m_childTransformLayer.get()) &amp;amp;&amp;amp; renderer-&amp;gt;isBox();
    if (updateChildTransformLayer(needsChildTransformLayer))
        layerConfigChanged = true;

    ......

    if (updateSquashingLayers(!m_squashedLayers.isEmpty()))
        layerConfigChanged = true;

    if (layerConfigChanged)
        updateInternalHierarchy();

    .......
}&lt;/pre&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数updateGraphicsLayerConfiguration依次检查它内部维护的Background Layer、Foreground Layer、Clip Layer、Scrolling Layer、Child Transform Layer和Squashing Layer是否需要更新，也就是创建或者删除等。只要其中的一个Graphics Layer发生更新，本地变量layerConfigChanged的值就会被设置为true，这时候CompositedLayerMapping类的另外一个成员函数updateInternalHierarchy就会被调用来更新内部的Graphics Layer Sub Tree。&lt;/p&gt; &lt;p&gt;       接下来我们以Squashing Layer的更新过程为例，即CompositedLayerMapping类的成员函数updateSquashingLayers的实现，分析CompositedLayerMapping类内部维护的Graphics Layer的更新过程，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;bool CompositedLayerMapping::updateSquashingLayers(bool needsSquashingLayers)
{
    bool layersChanged = false;

    if (needsSquashingLayers) {
        ......

        if (!m_squashingLayer) {
            m_squashingLayer = createGraphicsLayer(CompositingReasonLayerForSquashingContents);
            ......
            layersChanged = true;
        }

        if (m_ancestorClippingLayer) {
            if (m_squashingContainmentLayer) {
                m_squashingContainmentLayer-&amp;gt;removeFromParent();
                m_squashingContainmentLayer = nullptr;
                layersChanged = true;
            }
        } else {
            if (!m_squashingContainmentLayer) {
                m_squashingContainmentLayer = createGraphicsLayer(CompositingReasonLayerForSquashingContainer);
                layersChanged = true;
            }
        }

        ......
    } else {
        if (m_squashingLayer) {
            m_squashingLayer-&amp;gt;removeFromParent();
            m_squashingLayer = nullptr;
            layersChanged = true;
        }
        if (m_squashingContainmentLayer) {
            m_squashingContainmentLayer-&amp;gt;removeFromParent();
            m_squashingContainmentLayer = nullptr;
            layersChanged = true;
        }
        ......
    }

    return layersChanged;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       从前面的调用过程可以知道，当CompositedLayerMapping类的成员变量m_squashedLayers描述的Vector不等于空时，这里的参数needsSquashingLayers的值就会等于true，表示需要对CompositedLayerMapping类内部维护的Squashing Layer进行更新。更新过程如下所示：&lt;/p&gt; &lt;p&gt;       1. 如果Squashing Layer还没有创建，那么就会调用我们前面分析过的CompositedLayerMapping类的成员函数createGraphicsLayer进行创建，并且保存在成员变量m_squashingLayer中。&lt;/p&gt; &lt;p&gt;       2. CompositedLayerMapping类的成员变量m_ancestorClippingLayer描述的是图5所示的Clip Layer。当存在Squashing Layer时，但Clip Layer又不存在的时候，需要创建一个Squashing Containment Layer，用来作为Squashing Layer的父Graphics Layer。否则的话，Squashing Layer的父Graphics Layer就是Clip Layer。&lt;/p&gt; &lt;p&gt;       3. 基于上述第2点，当Clip Layer存在时，若Squashing Containment Layer存在，则需要将它从Graphics Layer Sub Tree中移除。另一方面，当Clip Layer不存在时，若Squashing Containment Layer也不存在，则需要创建Squashing Containment Layer。换句话说，当存在Squashing Layer时，Clip Layer和Squashing Containment Layer至少存在一个，并且只能存在一个，Clip Layer优先Squashing Containment Layer存在。&lt;/p&gt; &lt;p&gt;       如果CompositedLayerMapping类的成员变量m_squashedLayers描述的Vector等于空时，参数needsSquashingLayers的值就会等于false，表示需CompositedLayerMapping类不需要在内部维护一个Squashing Layer。这时候如果存在Squashing Layer，那么就需要将它从Graphics Layer Sub Tree中移除。如果Squashing Containment Layer也存在，那么也要将它一起从Graphics Layer Sub Tree中移除。这是因为Squashing Containment Layer本来就是为Squashing Layer创建的，现在既然Squashing Layer不需要了，那么它自然也不再需要了。&lt;/p&gt; &lt;p&gt;       回到CompositedLayerMapping类的成员函数updateGraphicsLayerConfiguration中，接下来我们继续分析它调用另外一个成员函数updateInternalHierarchy更新内部维护的raphics Layer Sub Tree的过程，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void CompositedLayerMapping::updateInternalHierarchy()
{
    // m_foregroundLayer has to be inserted in the correct order with child layers,
    // so it&amp;apos;s not inserted here.
    if (m_ancestorClippingLayer)
        m_ancestorClippingLayer-&amp;gt;removeAllChildren();

    m_graphicsLayer-&amp;gt;removeFromParent();

    if (m_ancestorClippingLayer)
        m_ancestorClippingLayer-&amp;gt;addChild(m_graphicsLayer.get());

    if (m_childContainmentLayer)
        m_graphicsLayer-&amp;gt;addChild(m_childContainmentLayer.get());
    else if (m_childTransformLayer)
        m_graphicsLayer-&amp;gt;addChild(m_childTransformLayer.get());

    if (m_scrollingLayer) {
        GraphicsLayer* superLayer = m_graphicsLayer.get();

        if (m_childContainmentLayer)
            superLayer = m_childContainmentLayer.get();

        if (m_childTransformLayer)
            superLayer = m_childTransformLayer.get();

        superLayer-&amp;gt;addChild(m_scrollingLayer.get());
    }

    // The clip for child layers does not include space for overflow controls, so they exist as
    // siblings of the clipping layer if we have one. Normal children of this layer are set as
    // children of the clipping layer.
    if (m_layerForHorizontalScrollbar)
        m_graphicsLayer-&amp;gt;addChild(m_layerForHorizontalScrollbar.get());
    if (m_layerForVerticalScrollbar)
        m_graphicsLayer-&amp;gt;addChild(m_layerForVerticalScrollbar.get());
    if (m_layerForScrollCorner)
        m_graphicsLayer-&amp;gt;addChild(m_layerForScrollCorner.get());

    // The squashing containment layer, if it exists, becomes a no-op parent.
    if (m_squashingLayer) {
        ASSERT(compositor()-&amp;gt;layerSquashingEnabled());
        ASSERT((m_ancestorClippingLayer &amp;amp;&amp;amp; !m_squashingContainmentLayer) || (!m_ancestorClippingLayer &amp;amp;&amp;amp; m_squashingContainmentLayer));

        if (m_squashingContainmentLayer) {
            m_squashingContainmentLayer-&amp;gt;removeAllChildren();
            m_squashingContainmentLayer-&amp;gt;addChild(m_graphicsLayer.get());
            m_squashingContainmentLayer-&amp;gt;addChild(m_squashingLayer.get());
        } else {
            // The ancestor clipping layer is already set up and has m_graphicsLayer under it.
            m_ancestorClippingLayer-&amp;gt;addChild(m_squashingLayer.get());
        }
    }
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       从这里我们就可以看到，通过调用GraphicsLayer类的成员函数addChild，就可以将一个Graphics Layer作为另外一个Graphics Layer的子Graphics Layer，这样就可以形成一个Graphics Layer Sub Tree。CompositedLayerMapping类的成员函数updateInternalHierarchy构建出来的Graphics Layer Sub Tree大致就如图5所示。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类内部维护的Graphics Layer Sub Tree，除了前面描述的Clip Layer和Squashing Containment Layer不能并存之外，另外两个Layer，即Child Containment Layer和Child Tranform Layer，也是不能并存的。这些Graphics Layer的具体作用，以及什么在情况下会存在，可以通过阅读ompositedLayerMapping类的代码获悉，这里就不再展开描述。&lt;/p&gt; &lt;p&gt;       还有一点需要注意的是，CompositedLayerMapping类内部维护的Background Layer和Foreground Layer也是属于CompositedLayerMapping类描述的Graphics Layer Sub Tree的一部分，但是它们不是由CompositedLayerMapping类的成员函数updateInternalHierarchy插入到Graphics Layer Sub Tree中去的。等到将Graphics Layer Sub Tree连接在一起形成整个Graphics Layer Tree的时候，它们才会插入到各自的Graphics Layer Sub Tree中去，因为处理它们需要更多的信息。&lt;/p&gt; &lt;p&gt;       由于各个Graphics Layer Sub Tree需要连接在一起形成一个完整的Graphics Layer Tree，因此每一个Graphics Layer Sub Tree都需要提供两个对外的Graphics Layer，一个作为其父Graphics Layer Sub Tree的子Graphics Layer，另一个作为其子Graphics Layer Sub Tree的父Graphics Layer。CompositedLayerMapping类提供了两个成员函数childForSuperlayers和parentForSublayers，分别提供上述两个Graphics Layer。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数childForSuperlayers的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;GraphicsLayer* CompositedLayerMapping::childForSuperlayers() const
{
    if (m_squashingContainmentLayer)
        return m_squashingContainmentLayer.get();

    return localRootForOwningLayer();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       从这里可以看到，如果存在Squashing Containment Layer，那么它就会作为父Graphics Layer Sub Tree的子Graphics Layer。另一方面，如果不存在Squashing Containment Layer，那么CompositedLayerMapping类的成员函数childForSuperlayers调用另外一个成员函数localRootForOwningLayer返回另外一个Graphics Layer作为父Graphics Layer Sub Tree的子Graphics Layer。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数localRootForOwningLayer的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;GraphicsLayer* CompositedLayerMapping::localRootForOwningLayer() const
{
    if (m_ancestorClippingLayer)
        return m_ancestorClippingLayer.get();

    return m_graphicsLayer.get();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       从这里可以看到，如果存在Clip Layer，那么它就会作为父Graphics Layer Sub Tree的子Graphics Layer。否则的话，Main Layer就会作为父Graphics Layer Sub Tree的子Graphics Layer。从图5可以知道，Main Layer是一定会存在的，因此就一定可以找到一个Graphics Layer，作为父Graphics Layer Sub Tree的子Graphics Layer。&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数parentForSublayers的实现如下所示：  &lt;br /&gt;&lt;/p&gt; &lt;pre&gt;GraphicsLayer* CompositedLayerMapping::parentForSublayers() const
{
    if (m_scrollingBlockSelectionLayer)
        return m_scrollingBlockSelectionLayer.get();

    if (m_scrollingContentsLayer)
        return m_scrollingContentsLayer.get();

    if (m_childContainmentLayer)
        return m_childContainmentLayer.get();

    if (m_childTransformLayer)
        return m_childTransformLayer.get();

    return m_graphicsLayer.get();
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/CompositedLayerMapping.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       CompositedLayerMapping类的成员函数parentForSublayers按照Scrolling Block Selection Layer、Scrolling Contents Layer、Child Containment Layer、Child Transform Layer和Main Layer的顺序检查，先检查的Graphics Layer若存在，那么它就优先作为子Graphics Layer Sub Tree的父Graphics Layer。同样，由于最后检查的Main Layer是一定存在的，因此就一定可以找到一个Graphics Layer，作为子Graphics Layer Sub Tree的父Graphics Layer。&lt;/p&gt; &lt;p&gt;       了解了Graphics Layer Sub Tree的构建过程之后，回到RenderLayerCompositor类的成员函数updateIfNeeded中，它最后就可以调用GraphicsLayerTreeBuilder类的成员函数rebuild将所有的Graphics Layer Sub Tree连接起来形成一个完整的Graphics Layer Tree了。&lt;/p&gt; &lt;p&gt;        GraphicsLayerTreeBuilder类的成员函数rebuild的实现如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void GraphicsLayerTreeBuilder::rebuild(RenderLayer&amp;amp; layer, GraphicsLayerVector&amp;amp; childLayersOfEnclosingLayer)
{
    ......

    const bool hasCompositedLayerMapping = layer.hasCompositedLayerMapping();
    CompositedLayerMappingPtr currentCompositedLayerMapping = layer.compositedLayerMapping();

    // If this layer has a compositedLayerMapping, then that is where we place subsequent children GraphicsLayers.
    // Otherwise children continue to append to the child list of the enclosing layer.
    GraphicsLayerVector layerChildren;
    GraphicsLayerVector&amp;amp; childList = hasCompositedLayerMapping ? layerChildren : childLayersOfEnclosingLayer;
    
    ......

    if (layer.stackingNode()-&amp;gt;isStackingContext()) {
        RenderLayerStackingNodeIterator iterator(*layer.stackingNode(), NegativeZOrderChildren);
        while (RenderLayerStackingNode* curNode = iterator.next())
            rebuild(*curNode-&amp;gt;layer(), childList);

        // If a negative z-order child is compositing, we get a foreground layer which needs to get parented.
        if (hasCompositedLayerMapping &amp;amp;&amp;amp; currentCompositedLayerMapping-&amp;gt;foregroundLayer())
            childList.append(currentCompositedLayerMapping-&amp;gt;foregroundLayer());
    }

    RenderLayerStackingNodeIterator iterator(*layer.stackingNode(), NormalFlowChildren | PositiveZOrderChildren);
    while (RenderLayerStackingNode* curNode = iterator.next())
        rebuild(*curNode-&amp;gt;layer(), childList);

    if (hasCompositedLayerMapping) {
        bool parented = false;
        if (layer.renderer()-&amp;gt;isRenderPart())
            parented = RenderLayerCompositor::parentFrameContentLayers(toRenderPart(layer.renderer()));

        if (!parented)
            currentCompositedLayerMapping-&amp;gt;parentForSublayers()-&amp;gt;setChildren(layerChildren);

        ......

        if (shouldAppendLayer(layer))
            childLayersOfEnclosingLayer.append(currentCompositedLayerMapping-&amp;gt;childForSuperlayers());
    }
}&lt;/pre&gt; &lt;p&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/GraphicsLayerTreeBuilder.cpp中。&lt;/p&gt; &lt;p&gt;       从前面的调用过程可以知道，参数layer描述的是网页Render Layer Tree的根节点，另外一个参数childLayersOfEnclosingLayer是一个输出参数，用来保存参数layer描述的Render Layer的子Render Layer的Child For Super Layers，也就是前面描述的一个Composited Layer Mapping中作为父Graphics Layer Sub Tree的子Graphics Layer。&lt;/p&gt; &lt;p&gt;       GraphicsLayerTreeBuilder类的成员函数rebuild根据Stacking Context顺序从Render Layer Tree的根节点开始，不断地递归调用自己，不过只会处理那些具有Composited Layer Mapping的Render Layer。对于那些不具有Composited Layer Mapping的Render Layer，仅仅是用作跳板找到具有Composited Layer Mapping的Render Layer。这一点容易理解，因为GraphicsLayerTreeBuilder类的成员函数rebuild是用来构建整个网页的Graphics Layer Tree的，只有具有Composited Layer Mapping的Render Layer才对应的Graphics Layer。&lt;/p&gt; &lt;p&gt;       GraphicsLayerTreeBuilder类的成员函数rebuild处理具有Composited Layer Mapping的Render Layer的过程如下所示：&lt;/p&gt; &lt;p&gt;       1. 收集z-index为负数的子Render Layer的Child For Super Layers，并且保存在本地变量layerChildren描述的一个Vector中。&lt;/p&gt; &lt;p&gt;       2. 如果正在处理的Render Layer具有z-index为负数的子Render Layer，那么根据前面的分析可以知道，正在处理的Render Layer的Composited Layer Mapping内部有一个Foreground Layer。这个Foreground Layer也会保存在本地变量layerChildren描述的一个Vector中，并且是位于那些z-index为负数的子Render Layer的Child For Super Layers之后。这就是为什么Foreground Layer不是由CompositedLayerMapping类的成员函数updateInternalHierarchy直接插入到Graphics Layer Sub Tree去的原因，因为CompositedLayerMapping类不知道一个Render Layer有哪些z-index为负数的子Render Layer。&lt;/p&gt; &lt;p&gt;       3. 收集z-index为0和正数的子Render Layer的Child For Super Layers，并且保存在本地变量layerChildren描述的一个Vector中。&lt;/p&gt; &lt;p&gt;       4. 经过前面三个收集操作，当前正在处理的Render Layer的Foreground Layer，以及它所有的子Render Layer的Child For Super Layers，就都保存在了本地变量layerChildren描述的一个Vector中。这时候只要找到当前正在处理的Render Layer的Parent For Sub Layers，再将前者作为后者的Children，就可以将具有父子关系的Graphics Layer Sub Tree连接起来。从前面的分析可以知道，当前正在处理的Render Layer的Parent For Sub Layers，可以通过调用它的Composited Layer Mapping的成员函数parentForSublayers获得。&lt;/p&gt; &lt;p&gt;       5. 当前正在处理的Render Layer的Child For Super Layers，要保存在参数childLayersOfEnclosingLayer描述的Vector中，以便作为其父Render Layer的Parent For Sub Layers的Children。&lt;/p&gt; &lt;p&gt;       其中，第4步对应的代码为：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;        bool parented = false;
        if (layer.renderer()-&amp;gt;isRenderPart())
            parented = RenderLayerCompositor::parentFrameContentLayers(toRenderPart(layer.renderer()));

        if (!parented)
            currentCompositedLayerMapping-&amp;gt;parentForSublayers()-&amp;gt;setChildren(layerChildren);&lt;/pre&gt;       它的执行有一个前提条件，就是本地变量parented的值为false。这是什么意思呢？当一个Render Layer的宿主Render Object对应的HTML Element是一个frame/iframe或者embed标签时，该Render Object是从RenderPart类继承下来的，称为Render Part。Render Part可能会具有自己的Render Layer Compositor，这可以通过调用enderLayerCompositor类的静态成员函数parentFrameContentLayers进行判断。如果一个Render Part具有自己的Render Layer Compositor，那么它的子Render Object就由这个Render Layer Compositor进行具体的绘制，绘制好之后再交给Render Part的父Render Object对应的Render Layer Compositor进行合成。因此，在这种情况下，Render Part的子Render Object所对应的Graphics Layer就不会插入在Render Part的父Render Object所对应的Graphics Layer Tree中。从另外一个角度理解就是，每一个Render Layer Compositor都有一个Graphics Layer Tree，而一个Graphics Layer不能同时位于两个Graphics Layer Tree中。 &lt;br /&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;       第5步对应的代码为：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;        if (shouldAppendLayer(layer))
            childLayersOfEnclosingLayer.append(currentCompositedLayerMapping-&amp;gt;childForSuperlayers());&lt;/pre&gt;       它的执行也有一个前提条件，就是当前正在处理的Render Layer对应的Graphcis Layer需要插入Graphics Layer Tree的时候才会执行，这可以通过调用函数shouldAppendLayer进行判断，如下所示： &lt;p&gt;&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;static bool shouldAppendLayer(const RenderLayer&amp;amp; layer)
{
    if (!RuntimeEnabledFeatures::overlayFullscreenVideoEnabled())
        return true;
    Node* node = layer.renderer()-&amp;gt;node();
    if (node &amp;amp;&amp;amp; isHTMLMediaElement(*node) &amp;amp;&amp;amp; toHTMLMediaElement(node)-&amp;gt;isFullscreen())
        return false;
    return true;
}&lt;/pre&gt;       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/GraphicsLayerTreeBuilder.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;       当一个Render Layer的宿主Render Object对应的HTML Element是一个audio或者video标签时，并且它们是全屏播放、以及浏览器允许全屏播放时，那么它对应的Graphcis Layer就不需要插入Graphics Layer Tree中去，因为毕竟就只有它是需要渲染的，其他的网页内容都是不可见的。&lt;/p&gt; &lt;p&gt;       GraphicsLayerTreeBuilder类的成员函数rebuild递归执行完成后，网页的Graphics Layer Tree就创建完成了。不过细心的读者会发现，还有一种类型的Graphics Layer还没有被插入到Graphics Layer Tree中去，就是图5所示的Background Layer。&lt;/p&gt; &lt;p&gt;       前面提到，只有根Render Layer的Composited Layer Mapping，才可能存在Background Layer。也就是只有当body标签的CSS属性background-attachment被设置为“fixed”时，根Render Layer的Composited Layer Mapping才会存在Background Layer。其他的Render Layer的Composited Layer Mapping，都不可能存在Background Layer。  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;       前面还提到，一个Graphics Layer Tree是由一个Render Layer Compositor进行管理。Render Layer Compositor内部也维护有一个Graphics Layer Sub Tree，充当根Graphics Layer Sub Tree的角色。这个Graphics Layer Sub Tree的结构如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;+Overflow Controls Host Layer
   +Container Layer
      +Background Layer
      +Scroll Layer
         +Root Content Layer
&lt;/pre&gt;       其中，GraphicsLayerTreeBuilder类的成员函数rebuild构建的Graphics Layer Tree的根节点是Root Content Layer，而Container Layer充当图5所示的Clip Layer的角色，这时候Background Layer就作为它的子Graphics Layer。Render Layer Compositor对应的Graphics Layer Sub Tree是由RenderLayerCompositor类的成员函数ensureRootLayer构建的，如下所示： &lt;p&gt;&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void RenderLayerCompositor::ensureRootLayer()
{
    ......

    if (!m_rootContentLayer) {
        m_rootContentLayer = GraphicsLayer::create(graphicsLayerFactory(), this);
        ......
    }

    if (!m_overflowControlsHostLayer) {
        ......

        // Create a layer to host the clipping layer and the overflow controls layers.
        m_overflowControlsHostLayer = GraphicsLayer::create(graphicsLayerFactory(), this);

        // Create a clipping layer if this is an iframe or settings require to clip.
        m_containerLayer = GraphicsLayer::create(graphicsLayerFactory(), this);
        ......

        m_scrollLayer = GraphicsLayer::create(graphicsLayerFactory(), this);
        ......
        // Hook them up
        m_overflowControlsHostLayer-&amp;gt;addChild(m_containerLayer.get());
        m_containerLayer-&amp;gt;addChild(m_scrollLayer.get());
        m_scrollLayer-&amp;gt;addChild(m_rootContentLayer.get());

        ......
    }

    ......
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;p&gt;&lt;/p&gt; &lt;p&gt;      当网页的body标签的CSS属性background-attachment被设置为“fixed”时，RenderLayerCompositor类的成员函数rootFixedBackgroundsChanged就会被调用，用来插入Background Layer，如下所示：&lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;pre&gt;void RenderLayerCompositor::rootFixedBackgroundsChanged()
{
    ......

    if (GraphicsLayer* backgroundLayer = fixedRootBackgroundLayer())
        m_containerLayer-&amp;gt;addChildBelow(backgroundLayer, m_scrollLayer.get());
}&lt;/pre&gt;      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/compositing/RenderLayerCompositor.cpp中。 &lt;p&gt;      RenderLayerCompositor类的成员函数rootFixedBackgroundsChanged调用另外一个成员函数fixedRootBackgroundLayer获得Background Layer，然后再将它作为上述Container Layer的Child，并且位于Scroll Layer的下面。&lt;/p&gt; &lt;p&gt;      这样，我们就分析完成网页Graphics Layer Tree的构建过程了，它是通过连接Graphics Layer Sub Tree得来的。每一个Graphics Layer Sub Tree又是由一个Composited Layer Mapping维护的。每一个需要Compositing的Render Layer都具有一个Composited Layer Mapping。这就意味着网页的Graphics Layer Tree是根据Render Layer Tree的内容构建的，并且Render Layer和Graphics Layer是多对一的关系。&lt;/p&gt; &lt;p&gt;     至此，我们就学习完成Chromium网页加载过程这个系列的文章，重新学习可以参考  &lt;a href="http://blog.csdn.net/luoshengyang/article/details/50414848" target="_blank"&gt;Chromium网页加载过程简要介绍和学习计划&lt;/a&gt;一文。这个过程主要是由WebKit完成的，一共构建了五个Tree，分别Frame Tree、DOM Tree、Render Object Tree、Render Layer Tree和Graphics Layer Tree。其中，最终输出给Chromium的是Graphics Layer Tree。有了Graphics Layer Tree之后，Chromium就可以绘制/渲染网页的UI了，这个过程我们在后面系列的文章再进行分析。敬请关注！更多的信息也可以关注老罗的新浪微博：  &lt;a href="http://weibo.com/shengyangluo" target="_blank"&gt;http://weibo.com/shengyangluo&lt;/a&gt;。&lt;/p&gt;
 &lt;div&gt;
    作者：Luoshengyang 发表于2016/2/29 0:59:09   &lt;a href="http://blog.csdn.net/luoshengyang/article/details/50661553"&gt;原文链接&lt;/a&gt;
&lt;/div&gt;
 &lt;div&gt;
    阅读：140 评论：0   &lt;a href="http://blog.csdn.net/luoshengyang/article/details/50661553#comments" target="_blank"&gt;查看评论&lt;/a&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 />
      <guid isPermaLink="true">https://itindex.net/detail/55264-chromium-%E7%BD%91%E9%A1%B5-graphics</guid>
      <pubDate>Mon, 29 Feb 2016 08:59:09 CST</pubDate>
    </item>
    <item>
      <title>初探 performance – 监控网页与程序性能</title>
      <link>https://itindex.net/detail/54300-performance-%E7%9B%91%E6%8E%A7-%E7%BD%91%E9%A1%B5</link>
      <description>&lt;h1&gt;
	使用 window.performance 提供了一组精确的数据，经过简单的计算就能得出一些网页性能数据。  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	配合上报一些客户端浏览器的设备类型等数据，就可以实现简单的统计啦！
&lt;/p&gt;
 &lt;p&gt;
	额，先看下兼容性如何：  &lt;a href="http://caniuse.com/#feat=nav-timing"&gt;http://caniuse.com/#feat=nav-timing&lt;/a&gt;
&lt;/p&gt;
 &lt;p&gt;
	这篇文章中 Demo 的运行环境为最新的 Chrome 的控制台，如果你用的是其他浏览器，自查兼容性哈~
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	先来看看在 Chrome 浏览器控制台中执行 window.performance 会出现什么：
&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072454pGM.jpg"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;h1&gt;
	简单解释下 performance 中的属性：  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	先看下一个请求发出的整个过程中，各种环节的时间顺序：
&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072455NuJ.png"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 获取 performance 数据
var performance = {  
    // memory 是非标准属性，只在 Chrome 有
    // 财富问题：我有多少内存
    memory: {
        usedJSHeapSize:  16100000, // JS 对象（包括V8引擎内部对象）占用的内存，一定小于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使用的内存
        jsHeapSizeLimit: 793000000 // 内存大小限制
    },

    //  哲学问题：我从哪里来？
    navigation: {
        redirectCount: 0, // 如果有重定向的话，页面通过几次重定向跳转而来
        type: 0           // 0   即 TYPE_NAVIGATENEXT 正常进入的页面（非刷新、非重定向等）
                          // 1   即 TYPE_RELOAD       通过 window.location.reload() 刷新的页面
                          // 2   即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面（历史记录）
                          // 255 即 TYPE_UNDEFINED    非以上方式进入的页面
    },

    timing: {
        // 在同一个浏览器上下文中，前一个网页（与当前页面不一定同域）unload 的时间戳，如果无前一个网页 unload ，则与 fetchStart 值相等
        navigationStart: 1441112691935,

        // 前一个网页（与当前页面同域）unload 的时间戳，如果无前一个网页 unload 或者前一个网页与当前页面不同域，则值为 0
        unloadEventStart: 0,

        // 和 unloadEventStart 相对应，返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
        unloadEventEnd: 0,

        // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算，否则值为 0 
        redirectStart: 0,

        // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算，否则值为 0 
        redirectEnd: 0,

        // 浏览器准备好使用 HTTP 请求抓取文档的时间，这发生在检查本地缓存之前
        fetchStart: 1441112692155,

        // DNS 域名查询开始的时间，如果使用了本地缓存（即无 DNS 查询）或持久连接，则与 fetchStart 值相等
        domainLookupStart: 1441112692155,

        // DNS 域名查询完成的时间，如果使用了本地缓存（即无 DNS 查询）或持久连接，则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,

        // HTTP（TCP） 开始建立连接的时间，如果是持久连接，则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接，则这里显示的是新建立的连接开始的时间
        connectStart: 1441112692155,

        // HTTP（TCP） 完成建立连接的时间（完成握手），如果是持久连接，则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接，则这里显示的是新建立的连接完成的时间
        // 注意这里握手结束，包括安全连接建立完成、SOCKS 授权通过
        connectEnd: 1441112692155,

        // HTTPS 连接开始的时间，如果不是安全连接，则值为 0
        secureConnectionStart: 0,

        // HTTP 请求读取真实文档开始的时间（完成建立连接），包括从本地读取缓存
        // 连接错误重连时，这里显示的也是新建立连接的时间
        requestStart: 1441112692158,

        // HTTP 开始接收响应的时间（获取到第一个字节），包括从本地读取缓存
        responseStart: 1441112692686,

        // HTTP 响应全部接收完成的时间（获取到最后一个字节），包括从本地读取缓存
        responseEnd: 1441112692687,

        // 开始解析渲染 DOM 树的时间，此时 Document.readyState 变为 loading，并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,

        // 完成解析 DOM 树的时间，Document.readyState 变为 interactive，并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成，这时候并没有开始加载网页内的资源
        domInteractive: 1441112693093,

        // DOM 解析完成后，网页内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发生
        domContentLoadedEventStart: 1441112693093,

        // DOM 解析完成后，网页内资源加载完成的时间（如 JS 脚本加载执行完毕）
        domContentLoadedEventEnd: 1441112693101,

        // DOM 树解析完成，且资源也准备就绪的时间，Document.readyState 变为 complete，并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,

        // load 事件发送给文档，也即 load 回调函数开始执行的时间
        // 注意如果没有绑定 load 事件，值为 0
        loadEventStart: 1441112693214,

        // load 事件的回调函数执行完毕的时间
        loadEventEnd: 1441112693215

        // 字母顺序
        // connectEnd: 1441112692155,
        // connectStart: 1441112692155,
        // domComplete: 1441112693214,
        // domContentLoadedEventEnd: 1441112693101,
        // domContentLoadedEventStart: 1441112693093,
        // domInteractive: 1441112693093,
        // domLoading: 1441112692690,
        // domainLookupEnd: 1441112692155,
        // domainLookupStart: 1441112692155,
        // fetchStart: 1441112692155,
        // loadEventEnd: 1441112693215,
        // loadEventStart: 1441112693214,
        // navigationStart: 1441112691935,
        // redirectEnd: 0,
        // redirectStart: 0,
        // requestStart: 1441112692158,
        // responseEnd: 1441112692687,
        // responseStart: 1441112692686,
        // secureConnectionStart: 0,
        // unloadEventEnd: 0,
        // unloadEventStart: 0
    }
};&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	具体的含义都在注释里说明了，接下来我们看下能用这些数据做什么？
&lt;/p&gt;
 &lt;h1&gt;
	使用 performance.timing 信息简单计算出网页性能数据  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	在注释中，我用【重要】标注了我个人认为比较有用的数据，用【原因】标注了为啥要重点关注这个数据
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 计算加载时间
function getPerformanceTiming () {  
    var performance = window.performance;

    if (!performance) {
        // 当前浏览器不支持
        console.log(&amp;apos;你的浏览器不支持 performance 接口&amp;apos;);
        return;
    }

    var t = performance.timing;
    var times = {};

    //【重要】页面加载完成的时间
    //【原因】这几乎代表了用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;

    //【重要】解析 DOM 树结构的时间
    //【原因】反省下你的 DOM 树嵌套是不是太多了！
    times.domReady = t.domComplete - t.responseEnd;

    //【重要】重定向的时间
    //【原因】拒绝重定向！比如，http://example.com/ 就不该写成 http://example.com
    times.redirect = t.redirectEnd - t.redirectStart;

    //【重要】DNS 查询时间
    //【原因】DNS 预加载做了么？页面内是不是使用了太多不同的域名导致域名查询的时间太长？
    // 可使用 HTML5 Prefetch 预查询 DNS ，见：[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)            
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

    //【重要】读取页面第一个字节的时间
    //【原因】这可以理解为用户拿到你的资源占用的时间，加异地机房了么，加CDN 处理了么？加带宽了么？加 CPU 运算速度了么？
    // TTFB 即 Time To First Byte 的意思
    // 维基百科：https://en.wikipedia.org/wiki/Time_To_First_Byte
    times.ttfb = t.responseStart - t.navigationStart;

    //【重要】内容加载完成的时间
    //【原因】页面内容经过 gzip 压缩了么，静态资源 css/js 等压缩了么？
    times.request = t.responseEnd - t.requestStart;

    //【重要】执行 onload 回调函数的时间
    //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了，考虑过延迟加载、按需加载的策略么？
    times.loadEvent = t.loadEventEnd - t.loadEventStart;

    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;

    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;

    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;

    return times;
}&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;h1&gt;
	使用performance.getEntries() 获取所有资源请求的时间数据  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	这个函数返回的将是一个数组，包含了页面中所有的 HTTP 请求，这里拿第一个请求 window.performance.getEntries()[0] 举例。 注意 HTTP 请求有可能命中本地缓存，所以请求响应的间隔将非常短 可以看到，与 performance.timing 对比： 没有与 DOM 相关的属性：
&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;
			navigationStart
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			unloadEventStart
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			unloadEventEnd
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			domLoading
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			domInteractive
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			domContentLoadedEventStart
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			domContentLoadedEventEnd
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			domComplete
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			loadEventStart
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			loadEventEnd
		&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;
	新增属性：
&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;
			name
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			entryType
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			initiatorType
		&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;
			duration
		&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;
	与 window.performance.timing 中包含的属性就不再介绍了：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;var entry = {  
    // 资源名称，也是资源的绝对路径
    name: &amp;quot;http://cdn.alloyteam.com/wp-content/themes/alloyteam/style.css&amp;quot;,
    // 资源类型
    entryType: &amp;quot;resource&amp;quot;,
    // 谁发起的请求
    initiatorType: &amp;quot;link&amp;quot;, // link 即 &amp;lt;link&amp;gt; 标签
                           // script 即 &amp;lt;script&amp;gt;
                           // redirect 即重定向
    // 加载时间
    duration: 18.13399999809917,

    redirectStart: 0,
    redirectEnd: 0,

    fetchStart: 424.57699999795295,

    domainLookupStart: 0,
    domainLookupEnd: 0,

    connectStart: 0,
    connectEnd: 0,

    secureConnectionStart: 0,

    requestStart: 0,

    responseStart: 0,
    responseEnd: 442.7109999960521,

    startTime: 424.57699999795295
};&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	可以像 getPerformanceTiming 获取网页的时间一样，获取某个资源的时间：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 计算加载时间
function getEntryTiming (entry) {  
    var t = entry;
    var times = {};

    // 重定向的时间
    times.redirect = t.redirectEnd - t.redirectStart;

    // DNS 查询时间
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

    // 内容加载完成的时间
    times.request = t.responseEnd - t.requestStart;

    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;

    // 挂载 entry 返回
    times.name = entry.name;
    times.entryType = entry.entryType;
    times.initiatorType = entry.initiatorType;
    times.duration = entry.duration;

    return times;
}

// test
// var entries = window.performance.getEntries();
// entries.forEach(function (entry) {
//     var times = getEntryTiming(entry);
//     console.log(times);
// });&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;h1&gt;
	使用 performance.now() 精确计算程序执行时间  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	performance.now() 与 Date.now() 不同的是，返回了以微秒（百万分之一秒）为单位的时间，更加精准。
&lt;/p&gt;
 &lt;p&gt;
	并且与 Date.now() 会受系统程序执行阻塞的影响不同，performance.now() 的时间是以恒定速率递增的，不受系统时间的影响（系统时间可被人为或软件调整）。
&lt;/p&gt;
 &lt;p&gt;
	注意 Date.now() 输出的是 UNIX 时间，即距离 1970 的时间，而 performance.now() 输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间。
&lt;/p&gt;
 &lt;p&gt;
	使用 Date.now() 的差值并非绝对精确，因为计算时间时受系统限制（可能阻塞）。但使用 performance.now() 的差值，并不影响我们计算程序执行的精确时间。
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 计算程序执行的精确时间
function getFunctionTimeWithDate (func) {  
    var timeStart = Data.now();

    // 执行开始
    func();
    // 执行结束
    var timeEnd = Data.now();

    // 返回执行时间
    return (timeEnd - timeStart);
}
function getFunctionTimeWithPerformance (func) {  
    var timeStart = window.performance.now();

    // 执行开始
    func();
    // 执行结束
    var timeEnd = window.performance.now();

    // 返回执行时间
    return (timeEnd - timeStart);
}&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;h1&gt;
	使用 performance.mark() 也可以精确计算程序执行时间  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	使用 performance.mark() 标记各种时间戳（就像在地图上打点），保存为各种测量值（测量地图上的点之间的距离），便可以批量地分析这些数据了。
&lt;/p&gt;
 &lt;p&gt;
	直接上示例代码看注释便明白：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;function randomFunc (n) {  
    if (!n) {
        // 生成一个随机数
        n = ~~(Math.random() * 10000);
    }
    var nameStart = &amp;apos;markStart&amp;apos; + n; 
    var nameEnd   = &amp;apos;markEnd&amp;apos; + n; 
    // 函数执行前做个标记
    window.performance.mark(nameStart);

    for (var i = 0; i &amp;lt; n; i++) {
        // do nothing
    }

    // 函数执行后再做个标记
    window.performance.mark(nameEnd);

    // 然后测量这个两个标记间的时间距离，并保存起来
    var name = &amp;apos;measureRandomFunc&amp;apos; + n;
    window.performance.measure(name, nameStart, nameEnd);
}

// 执行三次看看
randomFunc();  
randomFunc();  
// 指定一个名字
randomFunc(888);&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 看下保存起来的标记 mark
var marks = window.performance.getEntriesByType(&amp;apos;mark&amp;apos;);  
console.log(marks);&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072504WLm.jpg"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 看下保存起来的测量 measure
var measure = window.performance.getEntriesByType(&amp;apos;measure&amp;apos;);  
console.log(measure);&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072509yhq.jpg"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 看下我们自定义的测量
var entries = window.performance.getEntriesByName(&amp;apos;measureRandomFunc888&amp;apos;);  
console.log(entries);&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072511gsM.jpg"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;p&gt;
	可以看到，for 循环 measureRandomFunc888 的时候
&lt;/p&gt;
 &lt;p&gt;
	结束时间为: 4875.1199999969685
&lt;/p&gt;
 &lt;p&gt;
	开始时间为：4875.112999987323
&lt;/p&gt;
 &lt;p&gt;
	执行时间为：4875.1199999969685 – 4875.112999987323 = 0.00700000964
&lt;/p&gt;
 &lt;p&gt;
	标记和测量用完了可以清除掉：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 清除指定标记
window.performance.clearMarks(&amp;apos;markStart888&amp;apos;);  
// 清除所有标记
window.performance.clearMarks();

// 清除指定测量
window.performance.clearMeasures(&amp;apos;measureRandomFunc&amp;apos;);  
// 清除所有测量
window.performance.clearMeasures();&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	当然 performance.mark() 只是提供了一些简便的测量方式，比如之前我们测量 domReady 是这么测的：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;// 计算 domReady 时间
var t = performance.timing  
var domReadyTime = t.domComplete - t.responseEnd;  
console.log(domReadyTime)&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	其实就可以写成：
&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt; &lt;pre&gt;window.performance.measure(&amp;apos;domReady&amp;apos;,&amp;apos;responseEnd&amp;apos; , &amp;apos;domComplete&amp;apos;);  
var domReadyMeasure = window.performance.getEntriesByName(&amp;apos;domReady&amp;apos;);  
console.log(domReadyMeasure);&lt;/pre&gt; &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;
	  &lt;img alt="" src="http://cdn.alloyteam.com/wp-content/uploads/auto_save_image/2015/09/072513gda.jpg"&gt;&lt;/img&gt;
&lt;/p&gt;
 &lt;h1&gt;
	抛砖引玉：performance 数据能干啥用？  &lt;br /&gt;
&lt;/h1&gt;
 &lt;p&gt;
	熟悉 Chrome 开发者工具的朋友应该知道：在开发环境下，其实我们自己打开 Chrome 的开发者工具，切换到网络面板，就能很详细的看到网页性能相关的数据。但当我们需要统计分析用户打开我们网页时的性能如何时，我们将 performance 原始信息或通过简单计算后的信息(如上面写到的 getPerformanceTiming() 和 getEntryTiming()) 上传到服务器，配合其他信息（如 HTTP 请求头信息），就完美啦~&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 Web 前端优化 Web开发</category>
      <guid isPermaLink="true">https://itindex.net/detail/54300-performance-%E7%9B%91%E6%8E%A7-%E7%BD%91%E9%A1%B5</guid>
      <pubDate>Thu, 03 Sep 2015 15:26:47 CST</pubDate>
    </item>
    <item>
      <title>让网页加载快1秒的深远影响</title>
      <link>https://itindex.net/detail/55527-%E7%BD%91%E9%A1%B5-%E5%8A%A0%E8%BD%BD</link>
      <description>&lt;p&gt;全球内容交付网络（CDN）服务领头羊 Akamai 近日公布的《互联网状况》指出，2015 年第 4 季度全球平均网速为 5.6Mbps，同比增长 23% 环比增长 8.6%。宽带发展联盟发布的第 11 期《中国宽带速率状况报告》显示，2016 年第 1 季度我国固定宽带网络下载速率达到 9.46 Mbit/s，环比提升 13.4%，而且工信部表示将继续提速年底之前希望平均接入速度达到 30M。&lt;/p&gt; &lt;p&gt;在网络不断提速的大背景下，日常生活中我们依然不时发出「网络太慢了」这样的感慨。自然这里指的「慢」并非是拨号时代需要等待数分钟时间才能完成网页加载的「慢」，而是哪怕拥有高速网络接入，页面载入在感官体验上依然存在延迟--在移动设备上表现的尤为明显。根据   &lt;a href="http://googleresearch.blogspot.com/2009/06/speed-matters.html"&gt;Google 的内部研究&lt;/a&gt;这对于互联网商家来说是极其不利的，哪怕一丝的延迟都能导致客户流失。&lt;/p&gt; &lt;p&gt;马修·普林斯（Matthew Prince）和他的初创公司   &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt; 已经找到了解决问题的方法。Cloudflare 提供的一项服务是通过在自家服务器上缓存页面来改善网站的速度和安全，避免网站受到黑客攻击的同时也尽可能的为合法用户带来更快的页面载入。  &lt;strong&gt;现在，为了让网站加载更快，Cloudflare 正部署推出基于 HTTP 2.0 网页标准的服务器推送（Server Push），普林斯表示相比较传统的客户端请求（Client Pull）来说这能够让页面载入时间缩短 1 秒。&lt;/strong&gt;&lt;/p&gt; &lt;div&gt;  &lt;strong&gt;&lt;/strong&gt;
      &lt;img alt="" src="http://dn-noman.qbox.me/FuFhsdGR0my3P2aHVZ7Lx-nY6WoO"&gt;&lt;/img&gt;
        
&lt;/div&gt; &lt;p&gt;也许这缩短的 1 秒时间听上去并不是很多，但根据 Google 的研究，哪怕五分之一秒的延迟都能减少用户愿意消耗在网站上的浏览时间。一个网页应用让你感觉像是本地应用一样流畅还是延迟的无法使用，这两种使用体验之间的差别其实只有数百毫秒。因此普林斯认为服务器推送必然会带来巨大的差异体验，HTTP 2.0 能够让开发者在网络上实现此前认为不可能的事情。&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;/p&gt; &lt;p&gt;导致网站变慢的原因并非仅仅只是网页体积。事实上在打开网页的过程中，下载每个独立元素都需要你的网页浏览器向托管网站的服务器发出独立的请求。如果其中任何一个请求出现 Stalled（阻塞）状态，自然就会影响整个页面的加载，更有可能导致页面显示不正确。&lt;/p&gt; &lt;p&gt;尽管裁切掉这部分的请求或许有些帮助，但对于那些注重图片或者交互功能的网站来说依然需要处理大量的服务器响应。这也意味着网页浏览器同样需要在不受阻碍的方式下使用更好的方式来请求所有元素。&lt;/p&gt; &lt;p&gt;这也是 HTTP 标准继任者--HTTP 2.0 尝试改变的地方。HTTP 2.0 中新增的数项功能能够更快更高效的下载和显示网页，例如能够在一个报文（或者 HTTP 响应）中包含多项数据项。&lt;/p&gt; &lt;p&gt;服务器推送是今后 Cloudflare 公司大力推进的标准，也是公司产品的主打功能。它能够让网页服务器告诉网页浏览器需要在前期页面中所涵盖的所有不同元素，而不再需要现在一样按照顺序载入网页内容。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;让网页多任务&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;普林斯以本地银行网站为例进一步说明。当你登陆的时候，银行网站在生成页面并发送到你的网页浏览器之前必须请求一个关于近期交易清单并计算你银行存款余额的列表，只有当你的浏览器获得页面之后才会进一步请求例如银行 LOGO 和表格样式等其他元素，并告诉浏览器如何格式化网页。  &lt;strong&gt;而在部署服务器推送之后，银行网站就可以在计算账户余额的时候发送 LOGO 和表格样式。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;包括 Google Chrome 和 Mozilla Firefox 在内的部分浏览器都已经支持 HTTP 2.0。很多其他网页浏览器也已经支持它的前身--Google 的 SPDY（发音和 Speed 相同）协议。不过现在的问题是只有少量的网页真的支持 HTTP 2.0。普林斯表示，网站在部署服务器推送方面进程非常缓慢。&lt;/p&gt; &lt;p&gt;自今天开始，Cloudflare 的客户已经开始在网站上部署服务器推送，哪怕他们自己的服务器或者网站主机并不支持。而伴随着这个进程不断加快，我们日常访问的网站可能变得更快。普林斯表示目前已经有超过 200 万家网站使用 Cloudflare 所提供的服务，这其中包括全球顶级网站前 100 万家中的 7%。从理论上来说，Cloudflare 能够让客户更充分发挥服务器推送的能力，同时也为客户新增了部分工作内容。例如，WordPress 用户将需要安装  &lt;a href="https://wordpress.org/plugins/http2-server-push/"&gt;服务器推送插件&lt;/a&gt;以便充分利用这项功能。&lt;/p&gt; &lt;p&gt;普林斯表示该项目的真正意义是，将这把利器交到早期适配者的手中，从而让网页应用翻开新的篇章，尤其是在手机领域。让网页更像一款本地应用样流畅使用，为互联网的未来带来更美好的前景。&lt;/p&gt; &lt;div&gt;
	
		  &lt;div&gt;
			   &lt;div&gt;  &lt;/div&gt;
		&lt;/div&gt;
	
	
&lt;/div&gt; &lt;p&gt;文章来源：  &lt;a href="http://www.wired.com/2016/04/cloudflare-faster-web/"&gt;Wired&lt;/a&gt;，TECH2IPO / 创见 泡沫 编译，首发于创见科技（http://tech2ipo.com/），转载请注明出处。&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>产品快报</category>
      <guid isPermaLink="true">https://itindex.net/detail/55527-%E7%BD%91%E9%A1%B5-%E5%8A%A0%E8%BD%BD</guid>
      <pubDate>Sat, 30 Apr 2016 20:46:00 CST</pubDate>
    </item>
    <item>
      <title>微信开发之获取OAuth2.0网页授权认证和获取用户信息进行关联</title>
      <link>https://itindex.net/detail/54537-%E5%BE%AE%E4%BF%A1-%E5%BC%80%E5%8F%91-oauth2</link>
      <description>&lt;div&gt;
  &lt;p&gt;   &lt;br /&gt;        最近有做了关于微信公众号和自己网站用户进行用户关联授权登录的一个功能，主要是用户关注该公众号，点击会员中心，则会弹出需要关联授权的网页授权：OAuth2.0网页授权，然后用户同意获取用户信息，进行用户和网站的关联，然后用户则可以使用微信进行登录。&lt;/p&gt;
  &lt;p&gt;        本次做的是一个在Java的Action层处理各个返回参数获取数据。&lt;/p&gt;
  &lt;p&gt;       一、 使用到的工具：&lt;/p&gt;
  &lt;p&gt;            1、ngrok，将你自己的本机映射到公网，这样保证可以随时测试开发；&lt;/p&gt;
  &lt;p&gt;                   1、下载ngrok，网址：   &lt;a href="http://www.tunnel.mobi/"&gt;http://www.tunnel.mobi/&lt;/a&gt;&lt;/p&gt;
  &lt;p&gt;                   2、将文件放到Tomcat目录下，在cmd中运行ngrok -config ngrok.cfg -subdomain xinzhi 8080&lt;/p&gt;
  &lt;p&gt;                   3、ngrok工具为在慕课网@LAOBI 看到的&lt;/p&gt;
  &lt;p&gt;            2、微信公众号测试账号，随时测试，首先保证在测试账号下没有问题后在进行公众号的移植。&lt;/p&gt;
  &lt;p&gt;       二、使用到在Java中发送一个Http请求，然后返回JSON参数，获得JSON参数，然后进行处理。&lt;/p&gt;
  &lt;p&gt;           首先，获取将公众号测试号放到properties文件中，以便我们进行调用或者更换,如：url请用https&lt;/p&gt;
  &lt;p&gt;    &lt;/p&gt;
  &lt;pre&gt;AppID = wxf00**c3dd2ebfa0
AppSecret = 3cb220755f****506dc35391aa5c03ec
url = https://xinzhi.tunnel.mobi&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;p&gt;           这里url为我们映射到外网的地址，一会需要用到。然后需要两个工具类，该工具类作用是在Java的Action中发送http请求后获取到去返回值&lt;/p&gt;
  &lt;p&gt;            这里使用的是@   &lt;a href="http://my.csdn.net/lyq8479" target="_blank"&gt;柳峰&lt;/a&gt;，关于服务器请求的代码   &lt;a href="http://blog.csdn.net/lyq8479/article/details/9841371"&gt;http://blog.csdn.net/lyq8479/article/details/9841371&lt;/a&gt;，启用自己有相应的改动，以适应本项目的需求：&lt;/p&gt;
  &lt;p&gt;           WeixinUtil.java 和 MyX509TrustManager.java&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;package com.zhtx.common.util;


import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * 公众平台通用接口工具类
 * 
 * @author xinz
 * @date 2015-10-14
 */
public class WeixinUtil {
	private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);

	/**
	 * 发起https请求并获取结果
	 * 
	 * @param requestUrl 请求地址
	 * @param requestMethod 请求方式（GET、POST）
	 * @param outputStr 提交的数据
	 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
	 */
	public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
		StringBuffer buffer = new StringBuffer();
		try {
			// 创建SSLContext对象，并使用我们指定的信任管理器初始化
			TrustManager[] tm = { new MyX509TrustManager() };
			SSLContext sslContext = SSLContext.getInstance(&amp;quot;SSL&amp;quot;, &amp;quot;SunJSSE&amp;quot;);
			sslContext.init(null, tm, new java.security.SecureRandom());
			// 从上述SSLContext对象中得到SSLSocketFactory对象
			SSLSocketFactory ssf = sslContext.getSocketFactory();

			URL url = new URL(requestUrl);
			HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
			httpUrlConn.setSSLSocketFactory(ssf);

			httpUrlConn.setDoOutput(true);
			httpUrlConn.setDoInput(true);
			httpUrlConn.setUseCaches(false);
			// 设置请求方式（GET/POST）
			httpUrlConn.setRequestMethod(requestMethod);

			if (&amp;quot;GET&amp;quot;.equalsIgnoreCase(requestMethod))
				httpUrlConn.connect();

			// 当有数据需要提交时
			if (null != outputStr) {
				OutputStream outputStream = httpUrlConn.getOutputStream();
				// 注意编码格式，防止中文乱码
				outputStream.write(outputStr.getBytes(&amp;quot;UTF-8&amp;quot;));
				outputStream.close();
			}

			// 将返回的输入流转换成字符串
			InputStream inputStream = httpUrlConn.getInputStream();
			InputStreamReader inputStreamReader = new InputStreamReader(inputStream, &amp;quot;utf-8&amp;quot;);
			BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

			String str = null;
			while ((str = bufferedReader.readLine()) != null) {
				buffer.append(str);
			}
			bufferedReader.close();
			inputStreamReader.close();
			// 释放资源
			inputStream.close();
			inputStream = null;
			httpUrlConn.disconnect();
			
		} catch (ConnectException ce) {
			log.error(&amp;quot;Weixin server connection timed out.&amp;quot;);
		} catch (Exception e) {
			log.error(&amp;quot;https request error:{}&amp;quot;, e);
		}
		return buffer.toString();
	}
}
&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;p&gt; 对于https请求，我们需要一个证书信任管理器，这个管理器类需要自己定义，但需要实现X509TrustManager接口，代码如下:&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;package com.zhtx.common.util;


import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

/**
 * 证书信任管理器（用于https请求）
 * 
 * @author xinz
 * @date 2015-10-14
 */
public class MyX509TrustManager implements X509TrustManager {

	public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
	}

	public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
	}

	public X509Certificate[] getAcceptedIssuers() {
		return null;
	}
}
&lt;/pre&gt;
  &lt;p&gt; 微信返回参数的一个POJO类：&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;  private String  openid;  //用户的唯一标识 
  private String  nickname;//用户昵称 
  private Integer sex;// 用户的性别，值为1时是男性，值为2时是女性，值为0时是未知 
  private String  province;//用户个人资料填写的省份 
  private String  city;//普通用户个人资料填写的城市 
  private String  country;// 国家，如中国为CN 
  private String  headimgurl;  // 用户头像，最后一个数值代表正方形头像大小（有0、46、64、96、132数值可选，0代表640*640正方形头像），用户没有头像时该项为空。若用户更换头像，原有头像URL将失效。 
  private String  privilege;// 用户特权信息，json 数组，如微信沃卡用户为（chinaunicom） 
  private String  unionid;// 只有在用户将公众号绑定到微信开放平台帐号后，才会出现该字段。详见：获取用户个人信息（UnionID机制） 
  private String access_token;&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;p&gt; 授权凭证验证的类：&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;private String errcode;
private String errmsg;&lt;/pre&gt;
  &lt;p&gt; 通过code换取网页授权access_token&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;	private String access_token;
	private String expires_in;
	private String refresh_token;
	private String openid;
	private String scope;
	private String unionid;&lt;/pre&gt;
  &lt;p&gt; 关于微信头像的，获取的是一个http的url，则需要将图片下载到服务器存储，然后获得相对路径：&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;/**
	 * 使用url或者http存入文件
	 * @Title: fileUpload
	 * @param @param fileUrl  文件url，可以是http
	 * @param @param path     文件存储路径
	 * @return void
	 * @throws xinz
	 */
	public static void fileUpload (String fileUrl,String path){
		 //读取文件
		  String s1 = fileUrl;   
		  java.io.InputStream is = null; //定义一个输入流。
		  BufferedInputStream bis = null;//定义一个带缓冲的输入流 。 
		//写到本地 
		  BufferedOutputStream bos = null; //定义一个带缓冲的输出流。
		  try{ 
			java.net.URL url = new java.net.URL(s1);//创建一个URL对象。
		  	is = url.openStream();//打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。
		  	bis = new java.io.BufferedInputStream(is);     
		    File file = new File(path);   
			if(!file.exists()){ //测试此抽象路径名表示的文件或目录是否存在。  
				file.createNewFile();   //创建此抽象路径名表示的文件或目录。
			}   
		  bos = new BufferedOutputStream(new FileOutputStream(file));;     
		  byte[] b = new byte[1024]; //创建字节数组。
		  while(bis.read(b)!=-1){//输入流中的数据如果还有下一行(!=-1)将继续循环
			  bos.write(b);//将字节数组写入输出流。    
		  } 
		  }catch(Exception   e){     
			  System.out.println(e.toString());       
		  }finally{     
			  try{     
				  bos.flush();//刷新此缓冲的输出流。 
				  bis.close(); //关闭此输入流 。 
			  }catch(Exception   e){     
				  System.out.println(e.toString());       
			  }     
		  }  
	}&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;p&gt;现在是基础工作都做完了，现在开发代码的开发，在微信开发文档中    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html"&gt;http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html&lt;/a&gt; 有提到每一个步骤，然后我们按照这个步骤开发：&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;ul&gt;
   &lt;li&gt;    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html#.E7.AC.AC.E4.B8.80.E6.AD.A5.EF.BC.9A.E7.94.A8.E6.88.B7.E5.90.8C.E6.84.8F.E6.8E.88.E6.9D.83.EF.BC.8C.E8.8E.B7.E5.8F.96code"&gt;1 第一步：用户同意授权，获取code&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html#.E7.AC.AC.E4.BA.8C.E6.AD.A5.EF.BC.9A.E9.80.9A.E8.BF.87code.E6.8D.A2.E5.8F.96.E7.BD.91.E9.A1.B5.E6.8E.88.E6.9D.83access_token"&gt;2 第二步：通过code换取网页授权access_token&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html#.E7.AC.AC.E4.B8.89.E6.AD.A5.EF.BC.9A.E5.88.B7.E6.96.B0access_token.EF.BC.88.E5.A6.82.E6.9E.9C.E9.9C.80.E8.A6.81.EF.BC.89"&gt;3 第三步：刷新access_token（如果需要）&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html#.E7.AC.AC.E5.9B.9B.E6.AD.A5.EF.BC.9A.E6.8B.89.E5.8F.96.E7.94.A8.E6.88.B7.E4.BF.A1.E6.81.AF.28.E9.9C.80scope.E4.B8.BA_snsapi_userinfo.29"&gt;4 第四步：拉取用户信息(需scope为 snsapi_userinfo)&lt;/a&gt;&lt;/li&gt;
   &lt;li&gt;    &lt;a href="http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html#.E9.99.84.EF.BC.9A.E6.A3.80.E9.AA.8C.E6.8E.88.E6.9D.83.E5.87.AD.E8.AF.81.EF.BC.88access_token.EF.BC.89.E6.98.AF.E5.90.A6.E6.9C.89.E6.95.88"&gt;5 附：检验授权凭证（access_token）是否有效&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  &lt;p&gt;   &lt;strong&gt;第一步：用户同意授权，获取code&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;  这里的url就是前面所准备在properties中的url了。&lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;/**
	 * 微信用户授权
	 * @Title: wechatOauth
	 * @param @param request
	 * @param @param response
	 * @param @param model
	 * @param @return
	 * @return String
	 * @throws xinz
	 */
	@RequestMapping(&amp;quot;wechatOauth&amp;quot;)
	public String wechatOauth(HttpServletRequest request,HttpServletResponse response,Model model)  {
		/**
		 *  1 第一步：用户同意授权，获取code
		 */
		//首先拿到微信公众号的AppID、AppSecret等参数
		String AppID = ZhtxHelper.getApplicationResourcesProp(&amp;quot;sendSms&amp;quot;,&amp;quot;AppID&amp;quot;);
		String urlOpen = ZhtxHelper.getApplicationResourcesProp(&amp;quot;sendSms&amp;quot;,&amp;quot;url&amp;quot;);
		//如果用户授权成功则跳转到此url
		String loginUrl = &amp;quot;&amp;quot;+urlOpen+&amp;quot;/zhtx-wap/weixin/getAccessToken&amp;quot;;
		//用户授权，获取code
		String url = &amp;quot;https://open.weixin.qq.com/connect/oauth2/authorize?&amp;quot;
					+ &amp;quot;appid=&amp;quot;+AppID+&amp;quot;&amp;quot;
					+ &amp;quot;&amp;amp;redirect_uri=&amp;quot;+loginUrl+&amp;quot;&amp;quot;
					+ &amp;quot;&amp;amp;response_type=code&amp;quot;
					+ &amp;quot;&amp;amp;scope=snsapi_userinfo&amp;quot;
					+ &amp;quot;&amp;amp;state=123#wechat_redirect&amp;quot;;
		//forward redirect
		return &amp;quot;redirect:&amp;quot;+url+&amp;quot;&amp;quot;; 
	}&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;h3&gt;   &lt;strong&gt;第二步：通过code换取网页授权access_token&lt;/strong&gt;&lt;/h3&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;/**
	 * 通过code换取网页授权access_token
	 * @Title: getAccessToken
	 * @param @param request
	 * @param @param response
	 * @param @param model
	 * @param @return
	 * @return String
	 * @throws xinz
	 */
	@RequestMapping(&amp;quot;getAccessToken&amp;quot;)
	public String getAccessToken(HttpServletRequest request,HttpServletResponse response,Model model) {
		//获取到返回的参数
		try {
			//首先拿到微信公众号的AppID、AppSecret等参数
			String AppID = ZhtxHelper.getApplicationResourcesProp(&amp;quot;sendSms&amp;quot;,&amp;quot;AppID&amp;quot;);
			String AppSecret = ZhtxHelper.getApplicationResourcesProp(&amp;quot;sendSms&amp;quot;,&amp;quot;AppSecret&amp;quot;);
			String code = request.getParameter(&amp;quot;code&amp;quot;);
			String url = null;
			if(code!=null){
				/**
				 *  2 第二步：通过code换取网页授权access_token
				 */
				//用户授权，获取code
				url = &amp;quot;https://api.weixin.qq.com/sns/oauth2/access_token?&amp;quot;
						+ &amp;quot;appid=&amp;quot;+AppID+&amp;quot;&amp;quot;
						+ &amp;quot;&amp;amp;secret=&amp;quot;+AppSecret+&amp;quot;&amp;quot;
						+ &amp;quot;&amp;amp;code=&amp;quot;+code+&amp;quot;&amp;quot;
						+ &amp;quot;&amp;amp;grant_type=authorization_code&amp;quot;;
				String requestMethod = &amp;quot;GET&amp;quot;;
				String outputStr = &amp;quot;&amp;quot;;
				String httpRequest = WeixinUtil.httpRequest(url, requestMethod, outputStr);
				
				System.out.println(&amp;quot;通过code换取网页授权access_token=&amp;quot;+httpRequest);
				
				AccessTokenModel accTok = JSON.parseObject(httpRequest, AccessTokenModel.class);
				/**
				 *  4 第四步：拉取用户信息(需scope为 snsapi_userinfo)
				 */	                                                                      
				//用户授权，获取code
				String urlUser = &amp;quot;https://api.weixin.qq.com/sns/userinfo?&amp;quot;
						+ &amp;quot;access_token=&amp;quot;+accTok.getAccess_token()+&amp;quot;&amp;quot;
						+ &amp;quot;&amp;amp;openid=&amp;quot;+accTok.getOpenid()+&amp;quot;&amp;quot;
						+ &amp;quot;&amp;amp;lang=zh_CN&amp;quot;;
				
				String httpUser = WeixinUtil.httpRequest(urlUser, requestMethod, outputStr);
				System.out.println(&amp;quot;拉取用户信息==&amp;quot;+httpUser);
				
				WechatUser wechatUser = JSON.parseObject(httpUser, WechatUser.class);
				wechatUser.setAccess_token(accTok.getAccess_token());
				/**
				 *  5 附：检验授权凭证（access_token）是否有效
				 */
				WechatMsg checkAccessToken = checkAccessToken(wechatUser.getAccess_token(), wechatUser.getOpenid());
				if(checkAccessToken.getErrcode().equals(&amp;quot;0&amp;quot;)){
					CurrentSession.setAttribute(&amp;quot;wechatUser&amp;quot;, wechatUser);
					WechatUser wechatU = new WechatUser();
					wechatU.setOpenid(wechatUser.getOpenid());
					List&amp;lt;WechatUser&amp;gt; findWechatUser = wechatUserService.findWechatUser(wechatU);
					if(findWechatUser.size()&amp;gt;0){
						UserRegister userRegister = userService.findUserByOpenid(wechatUser.getOpenid());
						CurrentSession.setAttribute(&amp;quot;user&amp;quot;, userRegister);
						return &amp;quot;redirect:/user/userCenter&amp;quot;;
					}else{
						
						return &amp;quot;/jsp/wechat/wechatregister&amp;quot;; 
					}
					
				}else{
					//如果access_token失效，则再次进行调用，并存储access_token值，access_token有效期为2个小时
					this.wechatOauth(request, response, model); 
				}
			}
		} catch (Exception e) {
			System.out.println(&amp;quot;===拉取用户出错===&amp;quot;);
			e.printStackTrace();
		}
		//forward redirect
		return &amp;quot;/jsp/wechat/wechatregister&amp;quot;; 
	}&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;h3&gt;   &lt;strong&gt;第四步：拉取用户，和自己网站用户绑定&lt;/strong&gt;&lt;/h3&gt;
  &lt;pre&gt;/**
	 * 微信关联用户
	 * @Title: saveWechatUser
	 * @param @param mobilePhone
	 * @param @param password
	 * @param @param validataCode
	 * @param @return
	 * @return String
	 * @throws xinz
	 */
	@RequestMapping(&amp;quot;saveWechatUser&amp;quot;)
	public String saveWechatUser(HttpServletResponse response,String mobilePhone,String password,String validataCode){
		//使用手机号来判断该手机是否在注册
		UserRegister userRegister = userService.findUserByPhone(mobilePhone);
		WechatUser wechatUser = (WechatUser)CurrentSession.getAttribute(&amp;quot;wechatUser&amp;quot;);
		WechatUser wechatU = new WechatUser();
		wechatU.setOpenid(wechatUser.getOpenid());
		List&amp;lt;WechatUser&amp;gt; findWechatUser = wechatUserService.findWechatUser(wechatU);
		if(findWechatUser.size()&amp;gt;0 &amp;amp;&amp;amp; userRegister.getOpenid()!=null){
			CurrentSession.setAttribute(&amp;quot;user&amp;quot;, userRegister);
			return &amp;quot;redirect:/user/userCenter&amp;quot;;
		}else{
			//如果没有注册，开始注册
			if(userRegister==null){
				Result&amp;lt;UserRegister&amp;gt; saveUserInfoApp = userRegisterService.saveUserInfoApp(mobilePhone, password, validataCode,wechatUser);
				if(saveUserInfoApp.getState()==1){
					//进行微信和用户的关联
					wechatUserService.saveWechatUser(wechatUser);
					CurrentSession.setAttribute(&amp;quot;user&amp;quot;, userRegister);
					return &amp;quot;redirect:/user/userCenter&amp;quot;;
				}
			}else if(userRegister.getOpenid()==null || userRegister.getOpenid().equals(&amp;quot;&amp;quot;)){
			//否则，查询出用户信息，放入session中，关联微信，跳转到用户中心	
				UserRegister userReg = new UserRegister();
				userReg.setId(userRegister.getId());
				//存入微信openid
				userReg.setOpenid(wechatUser.getOpenid());
				userService.upUser(userReg);
				UserInfo user = new UserInfo();
				//存入微信头像
				//图片类型
				String dateStr =DateUtil.format(DateUtil.getCurrentDate(), &amp;quot;yyyyMMdd&amp;quot;)  + &amp;quot;/&amp;quot;;
				//图片类型
				String imgType = &amp;quot;JPG&amp;quot;;
				//微信头像名称
				String app2DBarNameAndType = UuidUtil.getUUID()+&amp;quot;.&amp;quot;+imgType;
				//微信头像路径
				String path =   ZhtxHelper.getApplicationResourcesProp(&amp;quot;application&amp;quot;,&amp;quot;app.img.projectpath&amp;quot;)+ SysConstant.GOODS2DBARPATH + dateStr;
				File file1 = new File(path);
				file1.mkdirs();
				//图片全路径
				String imgUrl = SysConstant.GOODS2DBARPATH + dateStr+app2DBarNameAndType;
				FileUtil.fileUpload(wechatUser.getHeadimgurl(), path);
				user.setRegisterId(userRegister.getId());
				user.setImageUrl(imgUrl);
				userInfoService.updateUserInfo(user);
				//存入微信用户
				wechatUserService.saveWechatUser(wechatUser);
				
				UserRegister userW = userService.findUserByPhone(mobilePhone);
				CurrentSession.setAttribute(&amp;quot;user&amp;quot;, userW);
				return &amp;quot;redirect:/user/userCenter&amp;quot;;
			}else{
				CurrentSession.setAttribute(&amp;quot;user&amp;quot;, userRegister);
				return &amp;quot;redirect:/user/userCenter&amp;quot;;
			}
		}
		return &amp;quot;redirect:/user/userCenter&amp;quot;;
	}&lt;/pre&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;h3&gt;   &lt;strong&gt;附：检验授权凭证（access_token）是否有效&lt;/strong&gt;&lt;/h3&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;pre&gt;/**
	 * 检验授权凭证（access_token）是否有效
	 * @Title: checkAccessToken
	 * @param @param access_token 网页授权接口调用凭证,注意：此access_token与基础支持的access_token不同 
	 * @param @param openid 用户的唯一标识 
	 * @return WechatMsg   返回消息实体
	 * @throws xinz
	 */
	public static WechatMsg checkAccessToken(String access_token,String openid){
		 String requestMethod = &amp;quot;GET&amp;quot;;
		 String outputStr = &amp;quot;&amp;quot;;	
		 String url = &amp;quot;https://api.weixin.qq.com/sns/auth?&amp;quot;
		 		+ &amp;quot;access_token=&amp;quot;+access_token+&amp;quot;&amp;quot;
		 		+ &amp;quot;&amp;amp;openid=&amp;quot;+openid+&amp;quot;&amp;quot;;
		 String httpmsg = WeixinUtil.httpRequest(url, requestMethod, outputStr);
		 System.out.println(&amp;quot;拉取用户信息返回消息==&amp;quot;+httpmsg);
			
		 WechatMsg msg = JSON.parseObject(httpmsg, WechatMsg.class);
	    
		 return msg;
	}&lt;/pre&gt;
  &lt;p&gt; 然后在网页端，则是需要编写H5页面，进行自己网站和微信用户的关联，我这里是使用手机号，用户输入手机号，进行判断，如果注册过就直接关联，如果用户没有注册则进行注册后关联，完成后跳转到会员中心。&lt;/p&gt;
  &lt;p&gt;    &lt;br /&gt;   &lt;img alt="" src="http://dl2.iteye.com/upload/attachment/0112/3723/4f7d7713-b3f9-34cd-bc18-6832655382a8.png"&gt;&lt;/img&gt;   &lt;br /&gt; &lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
  &lt;p&gt; &lt;/p&gt;
&lt;/div&gt;
          
           &lt;br /&gt; &lt;br /&gt;
          
             &lt;a href="http://playxinz.iteye.com/blog/2249634#comments"&gt;已有   &lt;strong&gt;0&lt;/strong&gt; 人发表留言，猛击-&amp;gt;&amp;gt;  &lt;strong&gt;这里&lt;/strong&gt;&amp;lt;&amp;lt;-参与讨论&lt;/a&gt;
          
           &lt;br /&gt; &lt;br /&gt; &lt;br /&gt;
ITeye推荐
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="http://www.iteye.com/clicks/433" target="_blank"&gt;—软件人才免语言低担保 赴美带薪读研！— &lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;br /&gt; &lt;br /&gt; &lt;br /&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 />
      <guid isPermaLink="true">https://itindex.net/detail/54537-%E5%BE%AE%E4%BF%A1-%E5%BC%80%E5%8F%91-oauth2</guid>
      <pubDate>Fri, 16 Oct 2015 14:36:32 CST</pubDate>
    </item>
    <item>
      <title>【译】Hybrid移动应用：用网页技术提供Native体验</title>
      <link>https://itindex.net/detail/52872-hybrid-%E7%A7%BB%E5%8A%A8%E5%BA%94%E7%94%A8-%E7%BD%91%E9%A1%B5</link>
      <description>&lt;p&gt;原文：  &lt;a href="http://www.smashingmagazine.com/2014/10/21/providing-a-native-experience-with-web-technologies/"&gt;http://www.smashingmagazine.com/2014/10/21/providing-a-native-experience-with-web-technologies/&lt;/a&gt;  &lt;br /&gt;
翻译：  &lt;a href="http://weibo.com/4eversai"&gt;叮当当咚当当小胖妞呀&lt;/a&gt;   &lt;a href="http://weibo.com/back2black"&gt;杀手爱elva&lt;/a&gt;   &lt;a href="http://weibo.com/blueed"&gt;Ivan_z3&lt;/a&gt; 肖弦&lt;/p&gt;
 &lt;p&gt;根据最近的一篇  &lt;a href="http://www.visionmobile.com/product/developer-economics-q3-2014/"&gt;报告&lt;/a&gt;显示，HTML是移动应用开发人员使用最多的语言，开发人员对于选择哪种网页技术考虑的最主要因素，是代码的跨平台便携性和开发的低成本性。我们常常听说，hybrid app使用起来非常慢，而且设计也很糟糕，让我们看看是否有可能又有原生应用的形，又有我们习惯使用的感。&lt;/p&gt;
 &lt;p&gt;这篇文章会提供很多关于如何构建良好的hybrid移动应用的线索、代码片段和经验。我将会大致介绍一下hybrid移动应用的开发，包括它的优点和缺点。然后，我会分享一下过去两年我在开发Hojoki和CatchApp时积累的经验，这两个项目都运行在主流的移动平台，并且是由HTML、CSS和Javascript建成的。最后，我们会介绍一下打包代码到原生app的一些比较好的工具。  &lt;br /&gt;
&lt;/p&gt;
 &lt;h2&gt;什么是混合模式移动应用&lt;/h2&gt;
 &lt;p&gt;移动app可以大致被分为三种，native、hybrid和web app。如果使用native app，你可以使用设备和操作系统的所有能力，同时，平台的性能负荷最小。然而，构建web app可以让你的代码跨平台，使得开发时间和成本大大减少。而hybrid app把这两者的优点都结合起来，使用一套共同代码，在许多不同的平台上部署类似原生的app。&lt;/p&gt;
 &lt;p&gt;有两种构建hybrid app的方法：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Webview app：HTML,CSS和Javascript基础代码在一个内部的浏览器（叫做WebView）中运行，这个浏览器打包在一个原生的app中，一些原生的API可以通过这个包被Javascript获得，比如   &lt;a href="http://phonegap.com"&gt;Adobe PhoneGap&lt;/a&gt;和   &lt;a href="https://trigger.io"&gt;Trigger.io&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;被编译的hybrid app：用一种语言编写代码（如C#或者Javascript），对于每一种支持的平台都把代码编译进原生代码中，这样做的结果是，每一个平台都有一个原生的app，但是在开发过程中少了一些自由空间。可以看一下这些例子，   &lt;a href="https://xamarin.com/"&gt;Xamarin&lt;/a&gt;，   &lt;a href="http://www.appcelerator.com/titanium/"&gt;Appcelerator Titanium&lt;/a&gt;，   &lt;a href="http://www.embarcadero.com/products/rad-studio/firemonkey"&gt;Embarcadero FireMonkey&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这两种方法都被广泛使用，存在即合理，  &lt;strong&gt;不过今天我们只关注WebView app&lt;/strong&gt;，因为WebView app可以让开发人员平衡他们现有的网页技术。我们来看一下hybrid app相对于native app和web app的各种优点和缺点。&lt;/p&gt;
 &lt;h3&gt;优点&lt;/h3&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;li&gt;一些设备和操作系统特征的访问&lt;/li&gt;
  &lt;li&gt;高级的离线特性&lt;/li&gt;
  &lt;li&gt;可见度上升，因为app可以原生发布（通过app store），也可以发布给移动端浏览器（通过搜索引擎）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;缺点&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;某些特定app的性能问题（那些依赖于复杂的原生功能或者繁重的过渡动画的app，如3D游戏）&lt;/li&gt;
  &lt;li&gt;为了模拟native app的UI和感官所增加的时间和精力&lt;/li&gt;
  &lt;li&gt;并不完全支持所有的设备和操作系统&lt;/li&gt;
  &lt;li&gt;如果app的体验并不够原生化，有被Apple拒绝的风险（比如说一个简单的网站）&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这些缺点比较显著，不能忽略，它们告诉我们，并不是所有的app都适合混合模式，你需要小心的预计你的目标用户、他们对平台的选择和对app的需求。对于许多app来说，好处都是大于坏处的，比如内容驱动的app。&lt;/p&gt;
 &lt;p&gt;Hojoki和CatchApp都是内容驱动的app，所以我们一开始觉得它们非常适合混合模式的开发方式。我们之前提到的好处中的前三点对于我们构建Hojoki的移动app帮助很大，不过也仅仅是4周的时间而已。显而易见，Hojoki的第一个版本缺失了很多重要的东西，接下来的时间里，我们都把精力扑在提升性能、对每一个平台制作自定义的UI和利用不同设备的高级特性上。那个时候积累的那些经验对于让app形似并神似native app很重要，下面我会尽可能多的分享一下我的经验。&lt;/p&gt;
 &lt;p&gt;那么，   &lt;strong&gt;怎么能让你的app形似并神似一个原生的app呢？&lt;/strong&gt;对于一个移动网页开发人员来说，能够使用设备和操作系统的能力，能够打包他们的app，这些都听上去很棒。然后，如果要用户相信这是一个native app，那么它就必须长得像并且表现的像。如何完成这一点对于混合模式移动开发人员来说仍然是最大的挑战之一。&lt;/p&gt;
 &lt;h2&gt;让你的用户宾至如归&lt;/h2&gt;
 &lt;p&gt;虽然我们只写一套基础代码，但这并不意味着多种不同平台上的感官都要完全一样，你们用户根本不在乎什么潜在的跨平台技术，他们只想要app根据他们的期望来展现，他们想要“宾至如归”。你的第一步应该是为每一个平台做设计概览：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;“   &lt;a href="https://developer.apple.com/library/ios/navigation/"&gt;IOS设计资源&lt;/a&gt;”, IOS开发人员库&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://developer.android.com/design/index.html"&gt;Android设计&lt;/a&gt;”, Android开发人员&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="https://dev.windowsphone.com/design"&gt;设计&lt;/a&gt;”, Windows开发中心&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;虽然这些概览并不能完美适应全部的app，但是它们仍然提供了一套非常全面和标准的界面和体验，每一种平台的用户都会希望得到这样的体验。&lt;/p&gt;
 &lt;h3&gt;DIY VS. UI框架&lt;/h3&gt;
 &lt;p&gt;如果你一个人实施这些组件、样式和动画，这是个很大的挑战，现在有各种各样的UI框架来帮助你，从商业（  &lt;a href="http://www.telerik.com/kendo-ui1"&gt;Kendo UI&lt;/a&gt;）的到开放（  &lt;a href="http://ionicframework.com"&gt;lonic&lt;/a&gt;）的，从共同UI（  &lt;a href="http://jquerymobile.com"&gt;JQuery Mobile&lt;/a&gt;和  &lt;a href="http://onsen.io"&gt;Onsen UI&lt;/a&gt;）到许多有平台针对性的UI（  &lt;a href="http://www.sencha.com/products/touch/"&gt;Sencha Touch&lt;/a&gt;和  &lt;a href="http://chocolatechip-ui.com"&gt;ChocolateChip-UI&lt;/a&gt;）。有些能够很好的提供精确到像素的布局，有些则相对粗糙，这些各式各样的框架能够很方便的让使用者定义一个web app。然而，就我的观念而言，框架最主要的缺点关乎性能，因为大多数UI框架都尽量“海纳百川”，要根据自己的情况在设备上试试demo后再决定是否要使用。&lt;/p&gt;
 &lt;p&gt;在制作Hojoki的时候，我们尝试自己用CSS3和极少的Javascript来创建所有的组件，这样做的好处是能够帮助我们控制性能，减少负荷。当然，我们也会使用一些别人使用过的较小的库来解决一下复杂的任务。&lt;/p&gt;
 &lt;h3&gt;自定义UI组件&lt;/h3&gt;
 &lt;p&gt;自定义UI组件也有很多很好的使用例子，你需要根据你的目标用户来决定使用平台的UI还是自定义UI，如果你想要单干，你需要对UX设计有很深的理解，因为之前的那些概览都是专家们为了迎合他们平台用户的需求而制作的。&lt;/p&gt;
 &lt;p&gt;不管你是决定坚持使用平台的UI概览还是自己做自定义的组件，你都必须知道，有一些特定的设计样式是用户每天使用并热爱的。通常我们如何把一个app介绍给用户呢？通过幻灯片讲演或者教学覆盖。用户如何导航？如果标签栏或者  &lt;a href="https://github.com/jakiestfu/Snap.js"&gt;侧边栏&lt;/a&gt;。用户如何快速加载或者刷新数据？下拉刷新。（在接下来的文章中会讲到类似原生的滚动）&lt;/p&gt;
 &lt;h3&gt;移动端UI设计的资源&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;“   &lt;a href="http://sixrevisions.com/user-interface/mobile-ui-design-patterns-inspiration/"&gt;移动端UI设计样式：10几个灵感迸发的网站&lt;/a&gt;”，Jacob Gube，Six Revisions&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://c2prods.com/2013/cloning-the-ui-of-ios-7-with-html-css-and-javascript/"&gt;用HTML、CSS和Javascript克隆IOS 7的UI&lt;/a&gt;“，Côme Courteault&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://de.slideshare.net/yaelsahar/tapping-into-mobile-ui-with-html5"&gt;用HTML5点进移动端UI&lt;/a&gt;”（幻灯片），Luke Melia and Yael Sahar, Slideshare&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;设计一个看起来原生的标题栏&lt;/h2&gt;
 &lt;p&gt;在UI中，标题栏是一个很重要的部分，包括它的标题、导航元素、尤其是前进和后退按钮。对于我来说，许多流行框架在提供HTML和CSS解决方案方面，相比一些原生的app是失败的，而为每个平台用最小的DOM和最少行的css代码来仿照这个UI部分其实相当的简单.&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;html   &lt;br /&gt;
&amp;lt;header&amp;gt;   &lt;br /&gt;
&amp;lt;button class=&amp;quot;back&amp;quot;&amp;gt;Feed&amp;lt;/button&amp;gt;   &lt;br /&gt;
&amp;lt;h1&amp;gt;Details&amp;lt;/h1&amp;gt;   &lt;br /&gt;
&amp;lt;!-- more actions (e.g. a search button on the right side) --&amp;gt;   &lt;br /&gt;
&amp;lt;/header&amp;gt;&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;在JSFiddle中查看“  &lt;a href="http://ued.ctrip.com/(http://jsfiddle.net/prud/dnebx02p/)"&gt;IOS、Android和Windows Phone中看起来原生的标题栏&lt;/a&gt;”的完整代码，下面是我的成果：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/02-html-header-new-opt.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;用html5和css做成的看起来原生的标题栏&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;在不同的平台上使用相同的DOM更为便利，因为代码整洁而且易于维护，我发现这样做对于许多IOS和Android上的UI组件都适用(包括标题栏、标签栏、定制的导航菜单、设置页面、浮层，还有很多其他的东西)。然而，想要更多的支持Windows Phone变得更加困难，因为它带来了许多非常不一样的设计模块。&lt;/p&gt;
 &lt;h2&gt;支持高分辨率屏幕&lt;/h2&gt;
 &lt;p&gt;现如今，高分辨率的智能手机和平板构成了巨大的移动设备市场，在  &lt;a href="http://david-smith.org/iosversionstats/#retina"&gt;ios设备中占有率超过80%&lt;/a&gt;，在  &lt;a href="http://developer.android.com/about/dashboards/index.html#Screens"&gt;android设备中占有率超过70%&lt;/a&gt;。为了让你设备上的图片展现得更清晰，你通常不得不将其尺寸放大到超过它原本大小的两倍，因此现在响应式网站设计中，如何针对所有不同的分辨率提供适合尺寸的图片，成为了现在热议的话题之一。现在有非常多的途径解决，每一种的优点和缺点都与带宽、代码易维护性和浏览器的兼容性有关，现在让我们来快速的回顾一下当下最流行的方法，顺序不分先后：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;服务端的调整和传输&lt;/li&gt;
  &lt;li&gt;客户端通过javascript的检测和替换&lt;/li&gt;
  &lt;li&gt;html5的picture元素&lt;/li&gt;
  &lt;li&gt;html5 的srcset属性&lt;/li&gt;
  &lt;li&gt;css image-set属性&lt;/li&gt;
  &lt;li&gt;css media queries&lt;/li&gt;
  &lt;li&gt;可伸缩矢量图形(SVG)&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;一直以来，针对响应式图片都没有一个完美的方法，这主要还是取决于图片的类型和它们在app上的展现使用方式。如果是静态图片(比如logo和教程图片)，我尽量使用SVG，它们能不费吹灰之力的完美缩放，并且  &lt;a href="http://caniuse.com/#feat=svg"&gt;只要你是Android 3+就能获得很好的浏览器支持&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;当你不能选择SVG的时候，  &lt;a href="http://responsiveimages.org/"&gt;html5的picture元素和srcset属性&lt;/a&gt;一定会成为日后前端开发人员的首选。当下，它们最大的不足就是在浏览器上的支持太局限，因此他们需要一些插件.&lt;/p&gt;
 &lt;p&gt;同时，  &lt;a href="http://www.uifuel.com/hd-retina-display-media-queries/"&gt;css背景图片和media queries&lt;/a&gt;是比较可靠的解决方案:&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;css   &lt;br /&gt;
/* Normal-resolution CSS */   &lt;br /&gt;
.logo {   &lt;br /&gt;
width: 120px;   &lt;br /&gt;
background: url(logo.png) no-repeat 0 0;   &lt;br /&gt;
}   &lt;br /&gt;
/* HD and Retina CSS */   &lt;br /&gt;
@media   &lt;br /&gt;
only screen and (-webkit-min-device-pixel-ratio: 1.25),   &lt;br /&gt;
only screen and ( min--moz-device-pixel-ratio: 1.25),   &lt;br /&gt;
only screen and ( -o-min-device-pixel-ratio: 1.25/1),   &lt;br /&gt;
only screen and ( min-device-pixel-ratio: 1.25),   &lt;br /&gt;
only screen and ( min-resolution: 200dpi),   &lt;br /&gt;
only screen and ( min-resolution: 1.25dppx) {   &lt;br /&gt;
.logo {   &lt;br /&gt;
background: url(logo@2x.png) no-repeat 0 0;   &lt;br /&gt;
background-size: 120px; /* Equal to normal logo width */   &lt;br /&gt;
}   &lt;br /&gt;
}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;但是，你的app也许已经包含了许多内容(比如新闻文章)，要调整所有的图片标签或者用css替代会让我们筋疲力竭，在这种情况下，服务端解决方案就会是最好的选择。&lt;/p&gt;
 &lt;p&gt;从去年开始，越来越多的android系统已经离XXHDPI(超高分辨率)屏幕又进了一步。不论上面提到的哪种方案更适合你的需要，你要记住的是你需要用到三倍于原图大小的图片来支持android的最新的设备。&lt;/p&gt;
 &lt;h2&gt;使用系统字体&lt;/h2&gt;
 &lt;p&gt;使用系统字体是一种让用户体感到宾至如归的一种简单但是重要的方法。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/03-native-fonts-new-opt.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;   &lt;a href="http://iosfonts.com/"&gt;ios&lt;/a&gt;、   &lt;a href="http://developer.android.com/design/style/typography.html"&gt;android&lt;/a&gt;和   &lt;a href="http://msdn.microsoft.com/library/windows/apps/hh700394.aspx#ux_font_choice"&gt;windows&lt;/a&gt;的原生字体&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;主流平台上，我比较推荐这些字体样式：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;/* iOS */   &lt;br /&gt;
font-family: &amp;apos;Helvetica Neue&amp;apos;, Helvetica, Arial, sans-serif;&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;/* Android */   &lt;br /&gt;
font-family: &amp;apos;RobotoRegular&amp;apos;, &amp;apos;Droid Sans&amp;apos;, sans-serif;   &lt;br /&gt;
&lt;/code&gt;  &lt;br /&gt;
  &lt;code&gt;/* Windows Phone */   &lt;br /&gt;
font-family: &amp;apos;Segoe UI&amp;apos;, Segoe, Tahoma, Geneva, sans-serif;   &lt;br /&gt;
&lt;/code&gt;  &lt;br /&gt;
此外，ios7提供了一些有趣的预处理，这些预处理可以自动的设置正确的字体、文字大小和行高，在普通文本中应用  &lt;code&gt;font:-apple-system-body&lt;/code&gt;,在标题中使用  &lt;code&gt;font:-apple-system-headline&lt;/code&gt;，这样不仅简化了文字的声明，而且还提升了  &lt;a href="http://support.apple.com/kb/HT5956"&gt;动态类型&lt;/a&gt;（这是Apple系统范围的字号设置）的可访问性，你可以在  &lt;a href="http://mir.aculo.us/2013/09/16/how-to-create-a-web-app-that-looks-like-a-ios7-native-app-part-1/"&gt;Thomas Fuchs的文章&lt;/a&gt;中了解到更多关于ios7的字体预处理。&lt;/p&gt;
 &lt;h2&gt;一个图标胜过千言万语&lt;/h2&gt;
 &lt;p&gt;图像学在所有主流移动平台的用户体验上是一个重要的部分。相比字体，你一定更愿意使用用户已知的icon，回顾一下我之前说过的高分辨率屏幕，要确保你的icon的大小是可调节的，将它们作为字体通过css的  &lt;code&gt;@font-face&lt;/code&gt;来实现，浏览器会有很好的兼容性支持，你甚至可以通过css改变icon的样式（比如颜色、阴影和透明度)。以下是我的推荐:&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;获取多个平台的icon字体   &lt;a href="http://ionicons.com"&gt;Ionicons&lt;/a&gt;是我们的基本设置，因为它几乎包含了所有我们需要的东西。除了他们常用的一些icon之外，还包含了ios和android的一些特殊的icon，其余的来源于   &lt;a href="http://ios7-icon-font-demo.herokuapp.com/"&gt;ios&lt;/a&gt;、Android   &lt;a href="http://www.spiderflyapps.com/downloads/android-developer-icons-the-font/"&gt;set 1&lt;/a&gt;和   &lt;a href="https://github.com/Turbo87/Android-Action-Bar-Icon-Pack-Font"&gt;set 2&lt;/a&gt;以及   &lt;a href="http://modernuiicons.com/"&gt;Windows Phone&lt;/a&gt;的特殊icon字体.&lt;/li&gt;
  &lt;li&gt;将它们用icon字体生产器结合起来用不同的icon字体让我们感觉非常混乱，还会迅速的增加文件大小，这就是我们为什么使用   &lt;a href="http://fontello.com/"&gt;Fontello&lt;/a&gt;来结合不同字体、调整代码和针对每个平台输出的原因。这样的结果就是   &lt;code&gt;&amp;lt;span class=&amp;quot;icon&amp;quot;&amp;gt;s&amp;lt;/span&amp;gt;&lt;/code&gt;在IOS、Android和Windows Phone中看起来是一个搜索图标。同时，还可以了解一些其他比较流行的方案，比如   &lt;a href="https://icomoon.io/app/"&gt;IcoMoon&lt;/a&gt;和   &lt;a href="http://fontastic.me/"&gt;Fontastic&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;在 Windows Phone上,你还可以  &lt;a href="http://msdn.microsoft.com/library/windows/apps/jj841126.aspx"&gt;脱离Native&lt;/a&gt;  &lt;code&gt;font-family: &amp;apos;Segoe UI Symbol&amp;apos;&lt;/code&gt;。&lt;/p&gt;
 &lt;h2&gt;性能优化&lt;/h2&gt;
 &lt;p&gt;性能经常被认为是hybrid移动app中一个主要的缺陷，尤其当你的app有大量的动画，包含大量的滚动列表并且需要在旧设备上运行的时候，缺点会越发的明显。然而，如果你觉得能够接受只支持一些比较新的平台版本(Android 4+,iOS 7+和Windows Phone 8+)的话，那你应该就会看到满意的效果。最终的问题就在于你在优化DOM和CSS选择器、书写高性能的Javascript、减少渲染时间和最少化重排重绘上花了多少工夫。关于移动网页性能的文章和教程一大把，以下是我最喜欢的一些:&lt;/p&gt;
 &lt;p&gt;“  &lt;a href="http://www.tricedesigns.com/2013/03/11/performance-ux-considerations-for-successful-phonegap-apps/"&gt;成功PhoneGap App的性能和UX考虑因素&lt;/a&gt;”，Andrew Trice  &lt;br /&gt;
“  &lt;a href="http://estelle.github.io/mobileperf/#slide1"&gt;移动：网页性能&lt;/a&gt;” (幻灯片)，Estelle Weyl  &lt;br /&gt;
“  &lt;a href="http://www.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/"&gt;书写快速、内存高效的Javascript&lt;/a&gt;,” Addy Osmani, Smashing Magazine  &lt;br /&gt;
“  &lt;a href="https://developers.google.com/speed/articles/reflow"&gt;浏览器重排最少化&lt;/a&gt;,” Lindsey Simon, Google Developers  &lt;br /&gt;
“  &lt;a href="http://www.smashingmagazine.com/2013/04/03/build-fast-loading-mobile-website/"&gt;如何让你的网站在移动设备上运行更快&lt;/a&gt;,” Johan Johansson, Smashing Magazine&lt;/p&gt;
 &lt;p&gt;此外，随着每天都有新的设备推出，移动硬件和渲染引擎都在以一个非常迅猛的速度提升，开发者能够做到使iPhone5系列、Android手机与Nexus4和5的纯原生App的性能上基本一致。&lt;/p&gt;
 &lt;h2&gt;提升感知速度&lt;/h2&gt;
 &lt;p&gt;构建高效的app是一回事而，让app感觉上运行很快又是另一回事儿。无论你的app是否需要一些时间来完成某项任务(比如一些复杂的计算或者客户端和服务器端的交流)，实时反馈对于提供流畅和响应式体验至关重要。一个比较常用的方法是，在用户还不需要某些功能的时候推迟加载，对用户接下来可能进行的操作作预估和预加载。Instagram有一个著名的例子，当用户忙于添加标签和分享的时候，  &lt;a href="http://www.cultofmac.com/164285/the-clever-trick-instagram-uses-to-upload-photos-so-quickly/"&gt;在后台上传图片&lt;/a&gt;。感知速度和真正的速度是不一样的，让我们合理运用它。这里有一些非常简单的例子：&lt;/p&gt;
 &lt;h3&gt;消除触屏设备上点击的延迟&lt;/h3&gt;
 &lt;p&gt;触屏设备中的一个普通Javascript点击事件，从点击的最开始到得到响应会有轻微的延迟(大约300毫秒)，浏览器的这种行为是在判断用户是单击还是双击，如果你不需要“双击以放大”特性，你可以安全的消除这300毫秒从而换取更多响应操作，我最喜欢的解决方案是  &lt;a href="https://github.com/ftlabs/fastclick"&gt;FastClick&lt;/a&gt;库，你可以把它用在除了IE的所有浏览器上：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;js   &lt;br /&gt;
if (&amp;apos;ontouchstart&amp;apos; in window) {   &lt;br /&gt;
window.addEventListener(&amp;apos;load&amp;apos;, function() {   &lt;br /&gt;
FastClick.attach(document.body);   &lt;br /&gt;
}, false);   &lt;br /&gt;
}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;IE10以上比较容易解决,你只需要添加一些css代码:&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;css   &lt;br /&gt;
html {   &lt;br /&gt;
-ms-touch-action: manipulation; /* IE 10 */   &lt;br /&gt;
touch-action: manipulation; /* IE 11+ */   &lt;br /&gt;
}&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;设计点击状态的样式&lt;/h3&gt;
 &lt;p&gt;只要用户点击一些可操作的元素（例如按钮或者链接），app检测到以后应该马上给他们一些回应。就像在台式机上表现不错的css的伪类  &lt;code&gt;:hover&lt;/code&gt;，在移动端你需要换成  &lt;code&gt;:active&lt;/code&gt;或者一些javascript解决方案。我曾经在JSFiddle上比较过三种  &lt;a href="http://jsfiddle.net/prud/x9y6cfhg/"&gt;点击状态的方案&lt;/a&gt;，你可以根据你的实际情况使用。&lt;/p&gt;
 &lt;p&gt;还有，记得在调整移动端点击状态的时候清楚默认点击高亮的样式，此外我建议禁用用户在一些活动的元素上选择，因为如果用户不小心点击按钮的时间过长，出现的选择菜单会很烦人。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;iOS and Android:&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;css   &lt;br /&gt;
button {   &lt;br /&gt;
outline: 0;   &lt;br /&gt;
-webkit-tap-highlight-color: rgba(0,0,0,0);   &lt;br /&gt;
-webkit-tap-highlight-color: transparent;   &lt;br /&gt;
-webkit-touch-callout: none;   &lt;br /&gt;
-webkit-user-select: none;   &lt;br /&gt;
-moz-user-select: none;   &lt;br /&gt;
-ms-user-select: none;   &lt;br /&gt;
user-select: none;   &lt;br /&gt;
}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;Windows Phone 8+:&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;html   &lt;br /&gt;
&amp;lt;meta name=&amp;quot;msapplication-tap-highlight&amp;quot; content=&amp;quot;no&amp;quot;&amp;gt;&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;提示加载&lt;/h3&gt;
 &lt;p&gt;你的app总是需要一些时间来完成动作，即使只有一秒，所以要考虑添加加载提示，否则用户就可能认为有时候app卡住了，或者在不应该点击的时候乱点，甚至他们可能会乱砸东西并且归责于你的app。根据我的经验，移动端浏览器运用gif动画不是一个好方法。一旦CPU上有一个加载项，gif卡住了，那么这个GIF对用户的加载提示就完全没有作用了。我更喜欢  &lt;a href="http://fgnass.github.io/spin.js/"&gt;Spin.js&lt;/a&gt;，因为可以自己配置并且运用简单，当然，还有一些其他的方法：  &lt;a href="http://www.queness.com/post/9150/9-javascript-and-animated-gif-loading-animation-solutions"&gt;javascript解决方案&lt;/a&gt;和  &lt;a href="http://tympanus.net/codrops/2012/11/14/creative-css-loading-animations/"&gt;css加载方案&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;一些跨平台的工具比如PhoneGap和Trigger.io也提供一些原生的加载状态，对于全屏动画加载的展现很棒。&lt;/p&gt;
 &lt;h2&gt;正确设置滚动&lt;/h2&gt;
 &lt;p&gt;滚动在许多app中是决定用户体验的最重要因素之一，它让人又爱又恨，因为要实现这一点取决于你应用依赖的滚动细节以及需要手机系统支持。&lt;/p&gt;
 &lt;p&gt;几乎所有的app都使用了可滚动内容和固定头部和/或底部。通常有两个CSS方法来实现：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;在   &lt;code&gt;body&lt;/code&gt;上启用滚动，在header上增加   &lt;code&gt;position: fixed&lt;/code&gt;；&lt;/li&gt;
  &lt;li&gt;在   &lt;code&gt;body&lt;/code&gt;上禁用滚动，在内容上增加   &lt;code&gt;overflow: scroll&lt;/code&gt;；&lt;/li&gt;
  &lt;li&gt;在   &lt;code&gt;body&lt;/code&gt;上禁用滚动，在内容上增加JavaScript默认滚动。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;虽然第一种方法有一些优点（例如IOS的原生滚动到顶部动作以及简洁的代码结构），我强烈推荐第二种方法：  &lt;code&gt;overflow: scroll&lt;/code&gt;。这个方法  &lt;a href="http://remysharp.com/2012/05/24/issues-with-position-fixed-scrolling-on-ios/"&gt;渲染问题更少&lt;/a&gt;（虽然还是比较多），现代平台上的浏览器支持更好（Android 4+，IOS 5+和Windows Phone 8+)，对于低版本浏览器有  &lt;a href="https://github.com/filamentgroup/Overthrow#browser-support"&gt;方便的小插件&lt;/a&gt;。另外，你可以把  &lt;code&gt;overflow: scroll&lt;/code&gt;换成自定义的滚动库（第三种选择），例如  &lt;a href="http://iscrolljs.com/"&gt;iScroll&lt;/a&gt;。虽然这些JavaScript解决方案使得特性更加灵活（例如，带动量效果的滚动位置，事件处理，可定制的效果和滚动条等），但它们通常会影响性能。当你在内容里用了许多DOM节点和/或CSS效果（例如  &lt;code&gt;box-shadow&lt;/code&gt;，  &lt;code&gt;text-shadow&lt;/code&gt;,  &lt;code&gt;opacity&lt;/code&gt;和  &lt;code&gt;rgba&lt;/code&gt;)时会很危险。&lt;/p&gt;
 &lt;p&gt;让我们来看一些基本的滚动特性。&lt;/p&gt;
 &lt;h3&gt;动量效果&lt;/h3&gt;
 &lt;p&gt;触摸友好的动量效果使得用户在大块内容区域的快速滚动显得很直观。通过一些简单的CSS就能在IOS 5+以及Android上一些版本的Chrome中激活。在IOS上，这也会使得内容不溢出顶部和底部边界。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;css   &lt;br /&gt;
overflow-y: scroll;   &lt;br /&gt;
-webkit-overflow-scrolling: touch;&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;下拉刷新&lt;/h3&gt;
 &lt;p&gt;网页上有许多实现这个效果的方法，例如  &lt;a href="http://damien.antipa.at/2012/10/16/ios-pull-to-refresh-in-mobile-safari-with-native-scrolling/"&gt;Damien Antipa&lt;/a&gt;写的一种。这个方法在IOS和Windows Phone中效果和体验很相似，Android最近发布它特有的结构（如下）。我们通过一些JavaScript和CSS keyframes在CatchApp里实现了这个效果。（我还没有总结好放到Github上，所以还在调整！）&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/04-pull-ios-opt.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;IOS中的下拉刷新。 (图片属于:    &lt;a href="http://damien.antipa.at/2012/10/16/ios-pull-to-refresh-in-mobile-safari-with-native-scrolling/"&gt;Damien Antipa&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/05-pull-wp-opt.gif"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;Android中的下拉刷新。 (图片属于:    &lt;a href="http://androidwidgetcenter.com/android-tips/how-to-refresh-gmail-on-android/"&gt;Android Widget Center&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/06-pull-android-opt.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;Windows Phone中的下拉刷新。 (图片属于:    &lt;a href="http://dwcares.com/pull-to-refresh-2/"&gt;David Washington&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
 &lt;h3&gt;滚动回到顶部&lt;/h3&gt;
 &lt;p&gt;不幸的是，在  &lt;code&gt;body&lt;/code&gt;上禁用滚动的同时会破坏IOS中允许用户通过点击状态栏快速回到页面顶部的原生特性。我写了一小段可以添加在任何元素上的脚本来使用JavaScript解决  &lt;a href="https://github.com/prud/ios-overflow-scroll-to-top"&gt;滚动到顶部的问题&lt;/a&gt;，即便内容当前处于动量效果中。把它添加到手机端网页头部或通过原生插件添加到状态栏上（例如，PhoneGap）。&lt;/p&gt;
 &lt;p&gt;许多其他滚动的特性可以通过原生  &lt;code&gt;overflow: scroll&lt;/code&gt;实现，例如关闭特定元素或只是无限滚动。如果需求更加复杂，一定考虑使用JavaScript方法。&lt;/p&gt;
 &lt;h2&gt;让点击更容易&lt;/h2&gt;
 &lt;p&gt;用户触摸时，很容易跟目标偏差几个像素，尤其是点击小按钮时（例如IOS顶部条上的按钮）。开发者可以在保证设计的情况下通过在小目标周围开启不可见触摸区域来使用户体验更好。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;html   &lt;br /&gt;
&amp;lt;button&amp;gt;   &lt;br /&gt;
&amp;lt;div class=&amp;quot;innerButton&amp;quot;&amp;gt;Click me!&amp;lt;/div&amp;gt;   &lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;你需要在按钮元素上绑定事件处理器，同时用  &lt;code&gt;div.innerButton&lt;/code&gt;定义样式。在JSFiddle上查看  &lt;a href="http://jsfiddle.net/prud/r7kqr1a3/"&gt;完整的，含有CSS&lt;/a&gt;的例子。&lt;/p&gt;
 &lt;h2&gt;使用触摸手势&lt;/h2&gt;
 &lt;p&gt;智能手机的精髓就是触摸和手势。在和触摸设备交互时，我们总是滑动，按压，缩放，拖动和长按。所以，为什么不提供相同的方式来让用户控制你的hybird应用呢？  &lt;a href="http://quojs.tapquo.com/"&gt;QuoJS&lt;/a&gt;和  &lt;a href="http://hammerjs.github.io/"&gt;Hammer.js&lt;/a&gt;是广为人知的支持所有手势类型的库。如果你想要更多选择，看一下Kevin Liew对”  &lt;a href="http://www.queness.com/post/11755/11-multi-touch-and-touch-events-javascript-libraries"&gt;11个多点触摸和触摸事件JavaScript库&lt;/a&gt;“的比较。&lt;/p&gt;
 &lt;h2&gt;不要忘了原生功能&lt;/h2&gt;
 &lt;p&gt;用网页技术构建你的应用并不意味着你不能用原生特性。事实上，所有主要的跨平台开发工具提供了提供了内置的对重要功能的接口。其中有许多API包括调用设备数据，文件系统，网络连接，地理位置定位，加速度传感器，提示（包括推送）等等。&lt;/p&gt;
 &lt;p&gt;通常，你甚至可以通过构建自定义插件来扩展开发工具。在Hojoki,我们加入了许多缺失的特性，包括读取用户对于我们app的推送提示的设置，读取用户时区，检查是否安装第三方APP并启动。让我们来看两个关于用原生插件实现的效果的例子。首先，让我们对IOS 6+里的input启动JavaScript  &lt;code&gt;focus（）&lt;/code&gt;：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;js   &lt;br /&gt;
if ([[[UIDevice currentDevice] systemVersion] floatValue] &amp;gt;= 6) {   &lt;br /&gt;
[YourWebView setKeyboardDisplayRequiresUserAction:NO];   &lt;br /&gt;
}&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;下面是在IOS上把给定字符串复制到设备剪贴板里的代码：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;[[UIPasteboard generalPasteboard] setString:@&amp;quot;Your copied string&amp;quot;];&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;总要留有出口&lt;/h2&gt;
 &lt;p&gt;网页开发者经常忽视如何处理hybird应用中的糟糕情况（例如，连接超时，错误输入，时间问题等等）。hybird应用从根本上区别于网站，主要因为它没有全局刷新按钮，在一些移动端操作系统里应用很容易在后台运行数周。如果用户死机了，他们唯一的选择是重启应用，这势必要强制退出然后重启。许多人甚至不知道怎样重启，尤其是在Android 2.x上（它深深隐藏在app设置里）和IOS 6及以下版本上（你需要双击home键，长按图标并且关闭它）。&lt;/p&gt;
 &lt;p&gt;所以，在开发中先不要觉得有刷新按钮就万事大吉而不考虑出错的情况，我们应该做的是遇到问题就立即解决。对于其他所有意外的情况，例如包含客户端-服务端的通信，准备好应对错误情况，给用户提供一个出口。可以简单得显示一个全屏的错误信息-“欧！出了些问题。请检查你的连接并再次尝试”-下面放一个大大的“重新载入”按钮。&lt;/p&gt;
 &lt;h2&gt;如何打包&lt;/h2&gt;
 &lt;p&gt;开发hybrid移动app就需要用到通常开发（移动）网站一样的工具，开发流程也一样。虽然这么说，我真正喜欢hybrid的地方是你能比较轻松地使用HTML、CSSS和JavaScript来部署移动web app。确保对原生特性实现回退，或在完全不支持该特性的情况下找到优雅的解决方案。大多数移动开发者更喜欢让用户群留在原生app上，甚至可以向使用移动网站的用户宣传这个app。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Native WebView wrapper around a HTML/CSS/JavaScript code base." src="http://www.smashingmagazine.com/wp-content/uploads/2013/05/07-hybrid-app-opt.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;em&gt;基于HTML、CSSS和JavaScript的原生WebView打包器&lt;/em&gt;&lt;/p&gt;
 &lt;p&gt;那原生部分怎样呢？移动web app（纯的HTML、CSSS和JavaScript）将加载到一个WebView中。作为一个内部浏览器引擎，WebView会按照设备的默认浏览器渲染网页的方式渲染app（可能会存在细微差别——你遇到的情况可能有所不同）。而且，原生“应用开发平台”用来暴露设备和操作系统的特性，而JavaScript能通过API调用到这些特性。这个API通常包含有调用特性的接口，比如设备摄像头、通讯录、地理位置、文件系统和原生事件（比如通过Android的硬件按钮）等特性。&lt;/p&gt;
 &lt;p&gt;有一些跨平台的开发工具提供了原生应用开发平台，简化了整个打包流程。下面深入研究部分工具。&lt;/p&gt;
 &lt;h3&gt;PHONEGAP和APACHE CORDOVA&lt;/h3&gt;
 &lt;p&gt;  &lt;a href="http://phonegap.com/"&gt;PhoneGap&lt;/a&gt;绝对是一个最流行的跨平台开发工具之一，它的名字本身经常被当作hybrid移动app开发的同义词。&lt;/p&gt;
 &lt;p&gt;关于它的名字以及与  &lt;a href="http://cordova.io/"&gt;Apache Cordova&lt;/a&gt;的关系存在的  &lt;a href="http://phonegap.com/2012/03/19/phonegap-cordova-and-what%E2%80%99s-in-a-name/"&gt;一些误解&lt;/a&gt;是可以理解的。后者是顶级Apache项目，曾用名就是PhoneGap。它提供了一套设备API，并通过运行在WebView中的HTML、CSSS和JavaScript调用原生的功能。现在，Adobe PhoneGap是Cordova的一个分支——与Chrome使用Webkit作为其引擎没有什么不同。&lt;/p&gt;
 &lt;p&gt;两者都是开源和免费的，支持所有主流平台，拥有一个开发各类插件和扩展的活跃社区。&lt;/p&gt;
 &lt;p&gt;PhoneGap对塑造hybrid贡献巨大，涌现出的很多新的工具提供了  &lt;strong&gt;附加功能&lt;/strong&gt;，使开发流程简单。这些工具带来了很多便利：通过在云端构建app，免去了在本地安装所有不同平台的SDK和工具的工作。每个工具都有不同的关注点、平台支持度和价钱：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://build.phonegap.com/"&gt;PhoneGap Build&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.telerik.com/appbuilder"&gt;Telerik AppBuilder&lt;/a&gt;（前身是Icenium）&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.appgyver.com/"&gt;AppGyver Steroids&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://appery.io/"&gt;Appery.io&lt;/a&gt;（前身是Tiggzi）&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://monaca.mobi/"&gt;Monaca&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://software.intel.com/html5/tools"&gt;Intel XDK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;SENCHA TOUCH&lt;/h3&gt;
 &lt;p&gt;  &lt;a href="http://www.sencha.com/products/touch/"&gt;Sencha Touch&lt;/a&gt;最初是一个针对移动web的UI框架，存在有几年了。过去，开发者使用Sencha构建app需同时使用其他如PhoneGap这样的服务来部署成hybrid app。现在Sencha内置了这种功能，可免费使用。支持的平台有iOS和Android（都需要通过Sencha自有的原生打包工具），BlackBerry、Windows 8等更多（通过PhoneGap Build）。&lt;/p&gt;
 &lt;h3&gt;TRIGGER.IO&lt;/h3&gt;
 &lt;p&gt;在Hojoki，我们在两年半前开始用  &lt;a href="https://trigger.io/"&gt;Trigger.io&lt;/a&gt;，因为当时想找一个比PhoneGap轻量级的替代品。虽然它只支持iOS和Android平台，但它提供了一套很不错的原生API、自定义插件和第三方集成（包括Parse消息推送服务、Flurry分析器以及部分Facebook的SDK）。通过Trigger.io的命令行工具，可以将app打包集成到  &lt;a href="http://gruntjs.com/"&gt;Grunt&lt;/a&gt;的构建过程。如果喜欢自动化，这一点很棒。&lt;/p&gt;
 &lt;p&gt;它的一个重要特性是  &lt;a href="https://trigger.io/reload/"&gt;Reload&lt;/a&gt;，该特性能使开发者推送HTML、CSSS和JavaScript的更新到运行中的app。与PhoneGap Build的  &lt;a href="http://docs.build.phonegap.com/en_US/tools_hydration.md.html"&gt;Hydration&lt;/a&gt;不同，Reload专门为开发和生产app设计。这样就使得合法绕过Apple的提交流程去提交bug解决方案和用A/B测试快速迭代就成为可能。&lt;/p&gt;
 &lt;p&gt;对很多开发者来说，一旦14天的试用期结束，Trigger.io  &lt;a href="https://trigger.io/pricing/"&gt;极高的价格&lt;/a&gt;可能就是它最大的缺点。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://www.mosync.com/"&gt;MoSync&lt;/a&gt;似乎是另一种不与PhoneGap有瓜葛的工具，但不太确定当下它的开发活跃度怎样。&lt;/p&gt;
 &lt;h3&gt;在真机上测试&lt;/h3&gt;
 &lt;p&gt;显然，用web技术构建移动app会诱使我们在web浏览器上做大部分的测试。在开发非原生特性时还算说得过去，但在发布时一定要避免。提交app之前，要在尽量多的生产商、平台和各种机型各种版本上测试。Android的机型和版本太多，在浏览器渲染、特性支不支持和生产商更改上会千差万别。虽然iOS渲染的差异好很多，但Apple生产的不同尺寸、分辨率和像素密度的设备越来越多。想了解更多请点击查看“  &lt;a href="http://www.smashingmagazine.com/2014/07/14/testing-and-responsive-web-design/"&gt;设备优先级：测试和响应式web设计&lt;/a&gt;”。&lt;/p&gt;
 &lt;p&gt;在2012年，Facebook放弃绝大部分HTML5开发转向原生开发，其中一个主要原因是“  &lt;a href="http://lists.w3.org/Archives/Public/public-coremob/2012Sep/0021.html"&gt;缺少调试工具和开发者API&lt;/a&gt;”。半年后，  &lt;a href="http://venturebeat.com/2013/04/17/linkedin-mobile-web-breakup/"&gt;LinkedIn得出同样的结论&lt;/a&gt;，声称HTML5本身准备好了，但基础工具和生态系统还没来得及支持它。从我的角度来说，情况正变得越来越好：Android 4.4+支持WebView的远程调试；各平台的开发工具越来越多：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;“   &lt;a href="https://developer.apple.com/safari/tools/"&gt;Web检测器&lt;/a&gt;”，Safari（iOS）&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="https://developer.chrome.com/devtools/docs/remote-debugging"&gt;使用Chrome在Android上远程调试&lt;/a&gt;”&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://msdn.microsoft.com/en-us/library/windows/apps/hh441472.aspx"&gt;在Visual Studio里调试商店app&lt;/a&gt;” (Windows Phone 8.1), Windows Dev Center&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://people.apache.org/~pmuellr/weinre/"&gt;Weinre&lt;/a&gt;（针对所有平台），Patrick Mueller&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://html.adobe.com/edge/inspect/"&gt;Edge Inspect&lt;/a&gt;（针对iOS和Android），Adobe&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;开始考虑硬发布&lt;/h3&gt;
 &lt;p&gt;为web浏览器构建app时，为用户部署修复程序是简单的一步，这意味着测试会失去其重要性。当通过app商店发布app，这就需要重新考虑了。把它想成上世纪90年代的软件开发：你现在就生活在硬发布的世界里。&lt;/p&gt;
 &lt;p&gt;那么，为什么这很糟糕？首先，提交过程随便就是一两周（寨见，Apple！）。其次，即使修复程序很快发布，也不能保证用户在短时间更新app。以下是我的建议：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;测试的优先级要高。&lt;/li&gt;
  &lt;li&gt;有类似“强制更新”的逻辑来放弃旧有客户端版本。&lt;/li&gt;
  &lt;li&gt;使用类似Trigger.io的   &lt;a href="https://trigger.io/reload/"&gt;Reload&lt;/a&gt;的机制来修复运行中的代码。&lt;/li&gt;
  &lt;li&gt;如果想快点，申请   &lt;a href="https://developer.apple.com/appstore/contact/appreviewteam/index.html"&gt;紧急app审核&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;发布到商店&lt;/h3&gt;
 &lt;p&gt;上面提到的工具会为每个平台生成一个版本，然后将这些版本提交到相应的商店。从这点，过程和发布一个“普通”的原生app几乎一样。在这方面，有些已讨论过的工具可能有更好的文档。尽管这样，以下是官方指南：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;“   &lt;a href="https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/Introduction/Introduction.html"&gt;App分发指南&lt;/a&gt;”，Apple&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://developer.android.com/distribute/googleplay/start.html"&gt;发布入门手册&lt;/a&gt;”和“   &lt;a href="http://developer.android.com/distribute/tools/launch-checklist.html"&gt;发布清单&lt;/a&gt;”，Android开发者&lt;/li&gt;
  &lt;li&gt;“   &lt;a href="http://msdn.microsoft.com/library/windows/apps/jj206736.aspx"&gt;Windows Phone发布&lt;/a&gt;”，Windows研发中心&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;结论&lt;/h2&gt;
 &lt;p&gt;既然开发的hybrid移动app在Apple的App商店和Google Play已经上线两年了，这里我就总结下在本文开头提到的一些优点和缺点。&lt;/p&gt;
 &lt;p&gt;对于我们这样一个资源有限、没有原生iOS和Android开发经验的创业公司，要在短短的几周内构建一个多平台的app是不可能的。选择hybrid，我们就能复用很多web app的代码，根据用户反馈迭代速度就快。我们已经成功发布了支持桌面Windows 8和微软Surface的原生app，支持Mac OS X的app也使用了基本一样的代码。移植到另一个平台的工作量很大程度上取决于给定浏览器与设备的能力和所需要的原生功能的水平高低。我们需要消息推送、app内置购买、获取用户联系方式，以及其他功能。根据你的需求，很多原生功能会使你很依赖于所选择的原生打包工具。&lt;/p&gt;
 &lt;p&gt;最后，我们来看看hybrid app是否真的能给出一个原生的感官享受。以下精选了来自app商店的用户评论。积极和消极的评论都有，其中很多消极的评论来自早期版本——各平台UI一样，性能相对较慢。&lt;/p&gt;
 &lt;blockquote&gt;  &lt;p&gt;评论略去&lt;/p&gt;&lt;/blockquote&gt;
 &lt;p&gt;的确，我们正远离特定平台的app开发而面向不断涌现的很多新技术。去年的Google I/O大会上被问到关于web的未来，  &lt;a href="http://youtu.be/9pmPa_KxsAM?t=2h56m6s"&gt;Larry Page说&lt;/a&gt;：&lt;/p&gt;
 &lt;blockquote&gt;  &lt;p&gt;在很长一段时间，我不认为作为开发者的你会考虑是否为这个平台或那个平台或其他类似平台在开发。我认为你应该在更高的层次上工作，你所写的软件能在每个平台运行起来，而且是很容易地运行起来。&lt;/p&gt;&lt;/blockquote&gt;
 &lt;p&gt;在这方面，（移动）web取得了很大的成功。使用这个平台而且仍然能在所有商店分发app是向前迈出的巨大一步。未来会发生什么敬请期待。无论发生什么，使用  &lt;a href="http://en.wikipedia.org/wiki/List_of_countries_by_number_of_Internet_users"&gt;世界上1/3人口&lt;/a&gt;（其中超过2/3来自欧洲和美国）依赖的技术大概不会是一个坏的选择。&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>前端开发 html5 web 翻译</category>
      <guid isPermaLink="true">https://itindex.net/detail/52872-hybrid-%E7%A7%BB%E5%8A%A8%E5%BA%94%E7%94%A8-%E7%BD%91%E9%A1%B5</guid>
      <pubDate>Wed, 04 Mar 2015 10:21:47 CST</pubDate>
    </item>
    <item>
      <title>Chrome采用新压缩算法 提升网页加载速度降低数据流量消耗</title>
      <link>https://itindex.net/detail/55093-chrome-%E5%8E%8B%E7%BC%A9-%E7%AE%97%E6%B3%95</link>
      <description>&lt;div&gt;
  &lt;p&gt;   &lt;strong&gt;谷歌Chrome浏览器很快就会提升网页加载速度并且降低数据流量消耗，这要归功于公司引进的Brotli压缩算法&lt;/strong&gt;。Brotli压缩算法始于去年九月。谷歌声称和使用已经达到3年时间的Zopfli算法相比，它可以将数据压缩率继续提升26%，谷歌表示，Brotli还可以帮助降低移动设备的电池使用量，达到省电目的。&lt;/p&gt;&lt;/div&gt;
 &lt;div&gt;  &lt;p&gt;据谷歌表示，Brotli是一个全新的数据格式，在包装中容纳更多数据，而解压缩速度和其他算法大致相同。如果你希望现在就用上Brotli压缩算法，那么可以下载   &lt;a href="https://www.google.com/chrome/browser/canary.html" target="_self" title="https://www.google.com/chrome/browser/canary.html"&gt;Chrome Canary版&lt;/a&gt;（谷歌用于测试新功能的浏览器）。&lt;/p&gt;  &lt;p&gt;   &lt;a href="http://static.cnbetacdn.com/article/2016/0120/1e2215e63d00208.jpg" target="_blank"&gt;    &lt;img alt="http://static.cnbetacdn.com/article/2016/0120/1e2215e63d00208.jpg" src="http://static.cnbetacdn.com/thumb/article/2016/0120/1e2215e63d00208.jpg_600x600.jpg" title="Chrome.jpg"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&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 />
      <guid isPermaLink="true">https://itindex.net/detail/55093-chrome-%E5%8E%8B%E7%BC%A9-%E7%AE%97%E6%B3%95</guid>
      <pubDate>Wed, 20 Jan 2016 22:27:29 CST</pubDate>
    </item>
    <item>
      <title>网页链接触发原生Intent</title>
      <link>https://itindex.net/detail/54866-%E7%BD%91%E9%A1%B5-%E9%93%BE%E6%8E%A5-intent</link>
      <description>&lt;p&gt;人们每天都要访问大量的手机网页, 如果把手机网页(Web)和应用(App)紧密地联系起来, 就可以增大用户的访问量, 也有其他应用场景, 如  &lt;code&gt;网页中调用支付链接&lt;/code&gt;,   &lt;code&gt;新闻中启动问诊界面&lt;/code&gt;,   &lt;code&gt;提供优质的原生功能&lt;/code&gt;等等.&lt;/p&gt;

 &lt;p&gt;如何在网页(Web)中, 通过Intent直接启动应用(App)的Activity呢? &lt;/p&gt;

 &lt;p&gt;本文主要有以下几点:   &lt;br /&gt;
(1) 如何在Web中发送原生的Intent消息.   &lt;br /&gt;
(1) 如何加载本地的HTML页面到浏览器.   &lt;br /&gt;
(2) 如何创建半透明的Activity页面.&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="&amp;#23637;&amp;#31034;" src="http://img.blog.csdn.net/20151214170412321" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h1&gt;1. 配置项目&lt;/h1&gt;

 &lt;p&gt;新建HelloWorld工程. 添加ButterKnife支持.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;compile &amp;apos;com.jakewharton:butterknife:7.0.1&amp;apos;&lt;/code&gt;&lt;/pre&gt;



 &lt;h1&gt;2. BottomSheet&lt;/h1&gt;

 &lt;p&gt;逻辑, 添加ShareIntent的监听, 即网页链接触发的Intent, 提取Link和Title信息, 底部出现或消失的动画.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;/**
 * 网页Activity
 * &amp;lt;p/&amp;gt;
 * Created by wangchenlong on 15/12/7.
 */
public class WebIntentActivity extends Activity {

    @Bind(R.id.web_intent_et_title) EditText mEtTitle;
    @Bind(R.id.web_intent_et_link) EditText mEtLink;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottom_sheet);
        ButterKnife.bind(this);

        // 获取WebIntent信息
        if (isShareIntent()) {
            ShareCompat.IntentReader intentReader = ShareCompat.IntentReader.from(this);
            mEtLink.setText(intentReader.getText());
            mEtTitle.setText(intentReader.getSubject());
        }
    }

    @Override protected void onResume() {
        super.onResume();
        // 底部出现动画
        overridePendingTransition(R.anim.bottom_in, R.anim.bottom_out);
    }

    // 判断是不是WebIntent
    private boolean isShareIntent() {
        return getIntent() != null &amp;amp;&amp;amp; Intent.ACTION_SEND.equals(getIntent().getAction());
    }

    @Override public void overridePendingTransition(int enterAnim, int exitAnim) {
        super.overridePendingTransition(enterAnim, exitAnim);
    }
}&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;动画属性, 沿Y轴变换.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;&amp;lt;set xmlns:android=&amp;quot;http://schemas.android.com/apk/res/android&amp;quot;&amp;gt;
    &amp;lt;translate
        android:duration=&amp;quot;300&amp;quot;
        android:fromYDelta=&amp;quot;100%p&amp;quot;
        android:toYDelta=&amp;quot;0%p&amp;quot;/&amp;gt;
&amp;lt;/set&amp;gt;&lt;/code&gt;&lt;/pre&gt;



 &lt;pre&gt;  &lt;code&gt;&amp;lt;set xmlns:android=&amp;quot;http://schemas.android.com/apk/res/android&amp;quot;&amp;gt;
    &amp;lt;translate
        android:duration=&amp;quot;300&amp;quot;
        android:fromYDelta=&amp;quot;0%p&amp;quot;
        android:toYDelta=&amp;quot;100%p&amp;quot;/&amp;gt;
&amp;lt;/set&amp;gt;&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;BottomSheet页面, 由两个EditText组成.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;LinearLayout
    android:id=&amp;quot;@+id/web_intent_ll_popup_window&amp;quot;
    xmlns:android=&amp;quot;http://schemas.android.com/apk/res/android&amp;quot;
    android:layout_width=&amp;quot;match_parent&amp;quot;
    android:layout_height=&amp;quot;wrap_content&amp;quot;
    android:layout_gravity=&amp;quot;bottom|center&amp;quot;
    android:background=&amp;quot;@android:color/white&amp;quot;
    android:orientation=&amp;quot;vertical&amp;quot;
    android:padding=&amp;quot;10dp&amp;quot;&amp;gt;

    &amp;lt;TextView
        android:layout_width=&amp;quot;match_parent&amp;quot;
        android:layout_height=&amp;quot;wrap_content&amp;quot;
        android:gravity=&amp;quot;center&amp;quot;
        android:text=&amp;quot;发送网页内容到应用&amp;quot;
        android:textSize=&amp;quot;20sp&amp;quot;/&amp;gt;

    &amp;lt;android.support.design.widget.TextInputLayout
        android:layout_width=&amp;quot;match_parent&amp;quot;
        android:layout_height=&amp;quot;wrap_content&amp;quot;
        android:layout_marginEnd=&amp;quot;16dp&amp;quot;
        android:layout_marginStart=&amp;quot;12dp&amp;quot;
        android:layout_marginTop=&amp;quot;8dp&amp;quot;&amp;gt;

        &amp;lt;EditText
            android:id=&amp;quot;@+id/web_intent_et_title&amp;quot;
            android:layout_width=&amp;quot;match_parent&amp;quot;
            android:layout_height=&amp;quot;wrap_content&amp;quot;
            android:hint=&amp;quot;Post Title&amp;quot;
            android:inputType=&amp;quot;textCapWords&amp;quot;/&amp;gt;

    &amp;lt;/android.support.design.widget.TextInputLayout&amp;gt;

    &amp;lt;android.support.design.widget.TextInputLayout
        android:layout_width=&amp;quot;match_parent&amp;quot;
        android:layout_height=&amp;quot;wrap_content&amp;quot;
        android:layout_marginEnd=&amp;quot;16dp&amp;quot;
        android:layout_marginStart=&amp;quot;12dp&amp;quot;
        android:layout_marginTop=&amp;quot;8dp&amp;quot;&amp;gt;

        &amp;lt;EditText
            android:id=&amp;quot;@+id/web_intent_et_link&amp;quot;
            android:layout_width=&amp;quot;match_parent&amp;quot;
            android:layout_height=&amp;quot;wrap_content&amp;quot;
            android:hint=&amp;quot;Link&amp;quot;
            android:inputType=&amp;quot;textCapWords&amp;quot;/&amp;gt;

    &amp;lt;/android.support.design.widget.TextInputLayout&amp;gt;

&amp;lt;/LinearLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;

 &lt;blockquote&gt;
    &lt;p&gt;注意    &lt;br /&gt;
  设置LinearLayout的   &lt;code&gt;android:layout_gravity=&amp;quot;bottom|center&amp;quot;&lt;/code&gt;属性,     &lt;br /&gt;
  配合样式(Styles)的   &lt;code&gt;&amp;lt;item name=&amp;quot;android:windowIsFloating&amp;quot;&amp;gt;false&amp;lt;/item&amp;gt;&lt;/code&gt;属性,    &lt;br /&gt;
  可以在底部显示页面.&lt;/p&gt;
&lt;/blockquote&gt;

 &lt;p&gt;  &lt;img alt="&amp;#25928;&amp;#26524;" src="http://img.blog.csdn.net/20151214170449043" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;声明, 添加  &lt;code&gt;SEND&lt;/code&gt;的Action,   &lt;code&gt;BROWSABLE&lt;/code&gt;的Category,   &lt;code&gt;text/plain&lt;/code&gt;的文件类型.    &lt;br /&gt;
主题设置透明主题. 启动时, 会保留上部半透明, 用于显示网页信息.&lt;/p&gt;

 &lt;pre&gt;  &lt;code&gt;        &amp;lt;activity
            android:name=&amp;quot;.WebIntentActivity&amp;quot;
            android:theme=&amp;quot;@style/Theme.Transparent&amp;quot;&amp;gt;
            &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&amp;quot;android.intent.action.SEND&amp;quot;/&amp;gt;

                &amp;lt;category android:name=&amp;quot;android.intent.category.DEFAULT&amp;quot;/&amp;gt;
                &amp;lt;category android:name=&amp;quot;android.intent.category.BROWSABLE&amp;quot;/&amp;gt;

                &amp;lt;data android:mimeType=&amp;quot;text/plain&amp;quot;/&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
        &amp;lt;/activity&amp;gt;&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;透明主题, 注意一些关键属性, 参考注释, 不一一列举.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;    &amp;lt;style name=&amp;quot;Theme.Transparent&amp;quot; parent=&amp;quot;AppTheme.NoActionBar&amp;quot;&amp;gt;
        &amp;lt;!--背景色--&amp;gt;
        &amp;lt;item name=&amp;quot;android:windowBackground&amp;quot;&amp;gt;@color/page_background&amp;lt;/item&amp;gt;
        &amp;lt;!--不使用背景缓存--&amp;gt;
        &amp;lt;item name=&amp;quot;android:colorBackgroundCacheHint&amp;quot;&amp;gt;@null&amp;lt;/item&amp;gt;
        &amp;lt;!--控制窗口位置, 非流窗口, 固定位置, 用于非全屏窗口--&amp;gt;
        &amp;lt;item name=&amp;quot;android:windowIsFloating&amp;quot;&amp;gt;false&amp;lt;/item&amp;gt;
        &amp;lt;!--窗口透明--&amp;gt;
        &amp;lt;item name=&amp;quot;android:windowIsTranslucent&amp;quot;&amp;gt;true&amp;lt;/item&amp;gt;
        &amp;lt;!--窗口无标题--&amp;gt;
        &amp;lt;item name=&amp;quot;android:windowNoTitle&amp;quot;&amp;gt;true&amp;lt;/item&amp;gt;
    &amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;背景颜色  &lt;code&gt;windowBackground&lt;/code&gt;非常重要, 不是常规颜色, 也可以设置为透明.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;&amp;lt;!--最前两位是颜色厚度, 00透明, FF全黑--&amp;gt;
&amp;lt;color name=&amp;quot;page_background&amp;quot;&amp;gt;#99323232&amp;lt;/color&amp;gt;&lt;/code&gt;&lt;/pre&gt;



 &lt;h1&gt;3. 主页面&lt;/h1&gt;

 &lt;p&gt;本地HTML文件存放在  &lt;code&gt;assets&lt;/code&gt;中, 提供在浏览器打开功能.    &lt;br /&gt;
浏览器打开Web链接非常简单, 打开本地HTML有很多难点.&lt;/p&gt;



 &lt;pre&gt;  &lt;code&gt;/**
 * 测试WebIntent的Demo
 *
 * @author C.L.Wang
 */
public class MainActivity extends AppCompatActivity {

    @SuppressWarnings(&amp;quot;unused&amp;quot;)
    private static final String TAG = &amp;quot;DEBUG-WCL: &amp;quot; + MainActivity.class.getSimpleName();

    private static final String FILE_NAME = &amp;quot;file:///android_asset/web_intent.html&amp;quot;;

    @Bind(R.id.main_wv_web) WebView mWvWeb; // WebView

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 跳转WebIntentActivity
                startActivity(new Intent(MainActivity.this, WebIntentActivity.class));
            }
        });

        mWvWeb.loadUrl(FILE_NAME);
    }

    @Override public void onBackPressed() {
        // 优先后退网页
        if (mWvWeb.canGoBack()) {
            mWvWeb.goBack();
        } else {
            finish();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        // 打开浏览器选项
        if (id == R.id.action_open_in_browser) {
            // 获取文件名, 打开assets文件使用文件名
            String[] as = FILE_NAME.split(&amp;quot;/&amp;quot;);
            openUrlInBrowser(as[as.length - 1]);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * 在浏览器中打开
     *
     * @param url 链接(本地HTML或者网络链接)
     */
    private void openUrlInBrowser(String url) {
        Uri uri;
        if (url.endsWith(&amp;quot;.html&amp;quot;)) { // 文件
            uri = Uri.fromFile(createFileFromInputStream(url));
        } else { // 链接
            if (!url.startsWith(&amp;quot;http://&amp;quot;) &amp;amp;&amp;amp; !url.startsWith(&amp;quot;https://&amp;quot;)) {
                url = &amp;quot;http://&amp;quot; + url;
            }
            uri = Uri.parse(url);
        }

        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            // 启动浏览器, 谷歌浏览器, 小米手机浏览器支持, 其他手机或浏览器不支持.
            intent.setClassName(&amp;quot;com.android.browser&amp;quot;, &amp;quot;com.android.browser.BrowserActivity&amp;quot;);
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this, &amp;quot;没有应用处理这个请求. 请安装浏览器.&amp;quot;, Toast.LENGTH_LONG).show();
            e.printStackTrace();
        }
    }

    /**
     * 存储assets内的文件
     *
     * @param url 文件名
     * @return 文件类(File)
     */
    private File createFileFromInputStream(String url) {
        try {
            // 打开Assets内的文件
            InputStream inputStream = getAssets().open(url);
            // 存储位置 /sdcard
            File file = new File(
                    Environment.getExternalStorageDirectory().getPath(), url);
            OutputStream outputStream = new FileOutputStream(file);
            byte buffer[] = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) &amp;gt; 0) {
                outputStream.write(buffer, 0, length);
            }
            outputStream.close();
            inputStream.close();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;

 &lt;blockquote&gt;
    &lt;p&gt;注意:    &lt;br /&gt;
  (1) 浏览器打开   &lt;code&gt;assets&lt;/code&gt;内文件的方式, 与   &lt;code&gt;WebView&lt;/code&gt;有所不同,     &lt;br /&gt;
  具体参考   &lt;code&gt;createFileFromInputStream&lt;/code&gt;函数.    &lt;br /&gt;
  (2) 在浏览器打开时, 需要指定包名, 而且各自浏览器的模式也不一样,     &lt;br /&gt;
  小米支持Google原生调用, 参考   &lt;code&gt;openUrlInBrowser&lt;/code&gt;函数.    &lt;br /&gt;
  (3) 回退事件的处理方式, 参考   &lt;code&gt;onBackPressed&lt;/code&gt;函数.&lt;/p&gt;
&lt;/blockquote&gt;

 &lt;p&gt;  &lt;img alt="&amp;#21160;&amp;#30011;" src="http://img.blog.csdn.net/20151214170509038" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;Github  &lt;a href="https://github.com/SpikeKing/TestWebIntent"&gt;下载地址&lt;/a&gt;&lt;/p&gt;

 &lt;p&gt;就这些了, 在浏览器的HTML5页面中, 可以添加更多和本地应用的交互.   &lt;br /&gt;
OK, Enjoy It.&lt;/p&gt;
 &lt;div&gt;
    作者：u012515223 发表于2015/12/14 17:05:44   &lt;a href="http://blog.csdn.net/caroline_wendy/article/details/50297671"&gt;原文链接&lt;/a&gt;
&lt;/div&gt;
 &lt;div&gt;
    阅读：0 评论：0   &lt;a href="http://blog.csdn.net/caroline_wendy/article/details/50297671#comments" target="_blank"&gt;查看评论&lt;/a&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 />
      <guid isPermaLink="true">https://itindex.net/detail/54866-%E7%BD%91%E9%A1%B5-%E9%93%BE%E6%8E%A5-intent</guid>
      <pubDate>Tue, 15 Dec 2015 01:05:44 CST</pubDate>
    </item>
    <item>
      <title>iOS中UIWebView与其中网页的javascript的交互</title>
      <link>https://itindex.net/detail/54814-ios-uiwebview-%E4%B8%AD%E7%BD%91</link>
      <description>&lt;div&gt;
  &lt;div&gt;
   &lt;p&gt;首发：    &lt;a href="http://427studio.net/blog/1/258"&gt;个人博客，更新&amp;amp;纠错&amp;amp;回复&lt;/a&gt;&lt;/p&gt;
   &lt;div&gt;1.本地语言调js的方式与android中的方式类似，也是向WebView控件发送要调用的js语句&lt;/div&gt;
   &lt;div&gt;2. 但js调本地语言，则不是像android那样直接调一个全局变量的方法，而是通过location.href=xx://yy这样的方式触发UIWebViewDelegate接口实现者的webView shouldStartLoadWithRequest navigationType方法，该方法应该判断目标路径（即xx://yy）的schema（即xx）是否实际是要调用swift，如果是，则按约定执行之，并返回false阻止网页路径变化，如果不是要调用swift，则返回true，让改网页继续正常加载目标url。&lt;/div&gt;
   &lt;div&gt;android和iOS对比，它们都用了伪url的技术，但android是在本地语言调js时使用了伪url（该url的schema为javascript），而iOS是js调本地语言时使用了伪url（该url是自定义的标识），这个错落很有意思。&lt;/div&gt;
   &lt;div&gt; &lt;/div&gt;
   &lt;div&gt; &lt;/div&gt;
   &lt;p&gt;    &lt;strong&gt;swift代码：&lt;/strong&gt;&lt;/p&gt;
   &lt;div&gt;
    &lt;blockquote&gt;
     &lt;p&gt;import UIKit&lt;/p&gt;
     &lt;p&gt; &lt;/p&gt;
     &lt;p&gt;class ViewController: UIViewController, UIWebViewDelegate {&lt;/p&gt;
     &lt;p&gt;    &lt;/p&gt;
     &lt;p&gt;    @IBOutlet weak var theWebView: UIWebView!&lt;/p&gt;
     &lt;p&gt;    &lt;/p&gt;
     &lt;p&gt;    override func viewDidLoad() {&lt;/p&gt;
     &lt;p&gt;              &lt;strong&gt;//加载本地html&lt;/strong&gt;&lt;/p&gt;
     &lt;p&gt;        let res = NSBundle.mainBundle().pathForResource(&amp;quot;index&amp;quot;,ofType:&amp;quot;html&amp;quot;)&lt;/p&gt;
     &lt;p&gt;        let url = NSURL(fileURLWithPath: res!);&lt;/p&gt;
     &lt;p&gt;        let request = NSURLRequest(URL: url);&lt;/p&gt;
     &lt;p&gt;        self.theWebView.loadRequest(request);&lt;/p&gt;
     &lt;p&gt;        self.theWebView.delegate = self;&lt;/p&gt;
     &lt;p&gt;    }&lt;/p&gt;
     &lt;p&gt;    &lt;/p&gt;
     &lt;p&gt;          &lt;strong&gt;//让js可以调用swift&lt;/strong&gt;&lt;/p&gt;
     &lt;p&gt;    func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -&amp;gt; Bool {&lt;/p&gt;
     &lt;p&gt;        //判断是不是js在调用swift，如果是，则处理并返回false&lt;/p&gt;
     &lt;p&gt;        if(request.URL!.scheme == &amp;quot;myschema&amp;quot;){&lt;/p&gt;
     &lt;p&gt;            let host = request.URL!.host;&lt;/p&gt;
     &lt;p&gt;            if(host == &amp;quot;go&amp;quot;){&lt;/p&gt;
     &lt;p&gt;                let query = request.URL!.query!;&lt;/p&gt;
     &lt;p&gt;                print(query);&lt;/p&gt;
     &lt;p&gt;                let split = query.componentsSeparatedByString(&amp;quot;=&amp;quot;);&lt;/p&gt;
     &lt;p&gt;                let text = split[1];&lt;/p&gt;
     &lt;p&gt;                self.theWebView.stringByEvaluatingJavaScriptFromString(&amp;quot;changeContent(\&amp;quot;&amp;quot; + text + &amp;quot;\&amp;quot;)&amp;quot;);&lt;/p&gt;
     &lt;p&gt;            }&lt;/p&gt;
     &lt;p&gt;            return false;&lt;/p&gt;
     &lt;p&gt;        }else{&lt;/p&gt;
     &lt;p&gt;            return true;&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;strong&gt;//swift调用js&lt;/strong&gt;&lt;/p&gt;
     &lt;p&gt;    @IBAction func btnClicked(sender: AnyObject) { &lt;/p&gt;
     &lt;p&gt;        self.theWebView.stringByEvaluatingJavaScriptFromString(&amp;quot;changeContent(\&amp;quot;123\&amp;quot;)&amp;quot;);&lt;/p&gt;
     &lt;p&gt;    }&lt;/p&gt;
     &lt;p&gt;}&lt;/p&gt;
&lt;/blockquote&gt;
    &lt;p&gt; &lt;/p&gt;
    &lt;p&gt;     &lt;strong&gt;网页代码：&lt;/strong&gt;&lt;/p&gt;
    &lt;blockquote&gt;
     &lt;p&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/p&gt;
     &lt;p&gt;&amp;lt;html&amp;gt;&lt;/p&gt;
     &lt;p&gt;&amp;lt;head&amp;gt;&lt;/p&gt;
     &lt;p&gt;    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;&lt;/p&gt;
     &lt;p&gt;&amp;lt;/head&amp;gt;&lt;/p&gt;
     &lt;p&gt;&amp;lt;body&amp;gt;&lt;/p&gt;
     &lt;p&gt;    &amp;lt;span id=&amp;quot;theSpan&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;/p&gt;
     &lt;p&gt;    &amp;lt;button onclick=&amp;quot;doIt()&amp;quot;&amp;gt;js调用swift&amp;lt;/button&amp;gt;&lt;/p&gt;
     &lt;p&gt;    &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;t&amp;quot;&amp;gt;&lt;/p&gt;
     &lt;p&gt;    &amp;lt;script&amp;gt;&lt;/p&gt;
     &lt;p&gt;              &lt;strong&gt;//让swift可以调用js&lt;/strong&gt;&lt;/p&gt;
     &lt;p&gt;        function changeContent(str){&lt;/p&gt;
     &lt;p&gt;            document.getElementById(&amp;apos;theSpan&amp;apos;).innerHTML = str;&lt;/p&gt;
     &lt;p&gt;        }&lt;/p&gt;
     &lt;p&gt;              &lt;strong&gt;//js调用swift&lt;/strong&gt;&lt;/p&gt;
     &lt;p&gt;        function doIt(){&lt;/p&gt;
     &lt;p&gt;            document.location.href = &amp;quot;myschema://go?a=&amp;quot; + document.getElementById(&amp;quot;t&amp;quot;).value;&lt;/p&gt;
     &lt;p&gt;        }&lt;/p&gt;
     &lt;p&gt;    &amp;lt;/script&amp;gt;&lt;/p&gt;
     &lt;p&gt;&amp;lt;/body&amp;gt;&lt;/p&gt;
     &lt;p&gt; &lt;/p&gt;
     &lt;p&gt;&amp;lt;/html&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
  &lt;div&gt;长期欢迎项目合作机会介绍，项目收入10%用于酬谢介绍人。新浪微博：   &lt;a href="http://weibo.com/zidafone"&gt;@冷镜&lt;/a&gt;，QQ：   &lt;a&gt;908789432&lt;/a&gt;。&lt;/div&gt;
&lt;/div&gt;
          
           &lt;br /&gt; &lt;br /&gt;
          
             &lt;a href="http://zidafone.iteye.com/blog/2263042#comments"&gt;已有   &lt;strong&gt;0&lt;/strong&gt; 人发表留言，猛击-&amp;gt;&amp;gt;  &lt;strong&gt;这里&lt;/strong&gt;&amp;lt;&amp;lt;-参与讨论&lt;/a&gt;
          
           &lt;br /&gt; &lt;br /&gt; &lt;br /&gt;
ITeye推荐
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="http://www.iteye.com/clicks/433" target="_blank"&gt;—软件人才免语言低担保 赴美带薪读研！— &lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;br /&gt; &lt;br /&gt; &lt;br /&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 />
      <guid isPermaLink="true">https://itindex.net/detail/54814-ios-uiwebview-%E4%B8%AD%E7%BD%91</guid>
      <pubDate>Thu, 10 Dec 2015 01:38:55 CST</pubDate>
    </item>
    <item>
      <title>懒人必备的移动端定宽网页适配方案</title>
      <link>https://itindex.net/detail/54801-%E7%A7%BB%E5%8A%A8-%E7%BD%91%E9%A1%B5-%E9%85%8D%E6%96%B9</link>
      <description>&lt;blockquote&gt;  &lt;p&gt;本文最初发布于我的个人博客：   &lt;a href="http://jerryzou.com/posts/design-for-all-mobile-resolution/"&gt;咀嚼之味&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
 &lt;p&gt;如今移动设备的分辨率纷繁复杂。以前仅仅是安卓机拥有各种各样的适配问题，如今 iPhone 也拥有了三种主流的分辨率，而未来的 iPhone 7 可能又会玩出什么新花样。如何以不变应万变，用简简单单的几行代码就能支持种类繁多的屏幕分辨率呢？今天就给大家介绍一种懒人必备的移动端定宽网页适配方法。&lt;/p&gt;
 &lt;p&gt;首先看看下面这行代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, user-scalabel=no&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;有过移动端开发经验的同学是不是对上面这句代码非常熟悉？它可能最常见的响应式设计的   &lt;code&gt;viewport&lt;/code&gt; 设置之一，而我今天介绍的这种方法也是利用了 meta 标签设置   &lt;code&gt;viewport&lt;/code&gt; 来支持大部分的移动端屏幕分辨率。&lt;/p&gt;
 &lt;h2&gt;目标&lt;/h2&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;仅仅通过配置     &lt;code&gt;&amp;lt;meta name=&amp;quot;viewport&amp;quot;&amp;gt;&lt;/code&gt; 使得移动端网站只需要按照固定的宽度设计并实现，就能在任何主流的移动设备上都能看到符合设计稿的页面，包括 Android 4+、iPhone 4+。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;h2&gt;测试设备&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;p&gt;三星 Note II (Android 4.1.2) - 真机&lt;/p&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;p&gt;三星 Note III (Android 4.4.4 - API 19) - Genymotion 虚拟机&lt;/p&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;p&gt;iPhone 6 (iOS 9.1) - 真机&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;iPhone&lt;/h2&gt;
 &lt;p&gt;iPhone 的适配比较简单，只需要设置   &lt;code&gt;width&lt;/code&gt; 即可。比如：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;!-- for iPhone --&amp;gt;
&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=320, user-scalable=no&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这样你的页面在所有的 iPhone 上，无论是 宽 375 像素的 iPhone 6 还是宽 414 像素的 iPhone 6 plus，都能显示出定宽 320 像素的页面。&lt;/p&gt;
 &lt;h2&gt;Android&lt;/h2&gt;
 &lt;p&gt;Android 上的适配被戏称为移动端的 IE，确实存在着很多兼容性问题。Android 以 4.4 版本为一个分水岭，首先说一说相对好处理的 Android 4.4+&lt;/p&gt;
 &lt;h3&gt;Android 4.4+&lt;/h3&gt;
 &lt;p&gt;为了兼容性考虑，Android 4.4 以上抛弃了   &lt;code&gt;target-densitydpi&lt;/code&gt; 属性，它只会在 Android 设备上生效。如果对这个被废弃的属性感兴趣，可以看看下面这两个链接：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;a href="http://stackoverflow.com/questions/11592015/support-for-target-densitydpi-is-removed-from-webkit"&gt;Support for target-densitydpi is removed from WebKit&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=88047"&gt;Bug 88047 - Remove support for target-densitydpi in the viewport meta tag&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;我们可以像在 iPhone 上那样设置   &lt;code&gt;width=320&lt;/code&gt; 以达到我们想要的 320px 定宽的页面设计。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;!-- for Android 4.4+ --&amp;gt;
&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=320, user-scalable=no&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;Android 4.0 ~ 4.3&lt;/h3&gt;
 &lt;p&gt;作为 Android 相对较老的版本，它对 meta 中的 width 属性支持得比较糟糕。以三星 Note II 为例，它的 device-width 是 360px。如果设置 viewport 中的 width (以下简称   &lt;code&gt;vWidth&lt;/code&gt; ) 为小于等于 360 的值，则不会有任何作用；而设置   &lt;code&gt;vWidth&lt;/code&gt; 为大于 360 的值，也不会使画面产生缩放，而是出现了横向滚动条。&lt;/p&gt;
 &lt;p&gt;想要对 Android 4.0 ~ 4.3 进行支持，还是不得不借助于  &lt;strong&gt;页面缩放&lt;/strong&gt;，以及那个被废除的属性：  &lt;code&gt;target-densitydpi&lt;/code&gt;。&lt;/p&gt;
 &lt;h3&gt;target-densitydpi&lt;/h3&gt;
 &lt;p&gt;target-densitydpi 一共有四种取值：low-dpi (0.75), medium-dpi (1.0), high-dpi (1.5), device-dpi。在 Android 4.0+ 的设备中，device-dpi 一般都是 2.0。我使用手头上的三星 Note II 设备 (Android 4.1.2) 进行了一系列实验，得到了下面这张表格：&lt;/p&gt;
 &lt;table&gt;
  &lt;tr&gt;
   &lt;th&gt;target-densitydpi&lt;/th&gt;
   &lt;th&gt;viewport: width&lt;/th&gt;
   &lt;th&gt;body width&lt;/th&gt;
   &lt;th&gt;屏幕可视范围宽度&lt;/th&gt;
&lt;/tr&gt;

  &lt;tr&gt;
   &lt;td&gt;low-dpi (0.75)&lt;/td&gt;
   &lt;td&gt;vWidth &amp;lt;= 320&lt;/td&gt;
   &lt;td&gt;270&lt;/td&gt;
   &lt;td&gt;270&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;vWidth &amp;gt; 320&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;270&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;medium-dpi (1.0)&lt;/td&gt;
   &lt;td&gt;vWidth &amp;lt;= 360&lt;/td&gt;
   &lt;td&gt;360&lt;/td&gt;
   &lt;td&gt;360&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;vWidth &amp;gt; 360&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;360&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;high-dpi (1.5)&lt;/td&gt;
   &lt;td&gt;vWidth &amp;lt;= 320&lt;/td&gt;
   &lt;td&gt;540&lt;/td&gt;
   &lt;td&gt;540&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;320 &amp;lt; vWidth &amp;lt;= 540&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;vWidth &amp;gt; 540&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;540&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;device-dpi (2.0)**&lt;/td&gt;
   &lt;td&gt;vWidth &amp;lt;= 320&lt;/td&gt;
   &lt;td&gt;720&lt;/td&gt;
   &lt;td&gt;720&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;320 &amp;lt; vWidth &amp;lt;= 720&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt; &lt;/td&gt;
   &lt;td&gt;vWidth &amp;gt; 720&lt;/td&gt;
   &lt;td&gt;vWidth*&lt;/td&gt;
   &lt;td&gt;720&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;vWidth&lt;/strong&gt;*：指的是与 viewport 中设置的 width 的值相同。&lt;/p&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;device-dpi (2.0&lt;/strong&gt;)**：在 Android 4.0+ 的设备中，device-dpi 一般都是 2.0。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;首先可以看到   &lt;strong&gt;320px&lt;/strong&gt; 是个特别诡异的临界值，低于这个临界值后就会发生超出我们预期的事情。综合考虑下来，还是采用   &lt;code&gt;target-densitydpi = device-dpi&lt;/code&gt; 这一取值。如果你想要以 320px 作为页面的宽度的话，我建议你针对安卓 4.4 以下的版本设置   &lt;code&gt;width=321&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;如果 body 的宽度超过屏幕可视范围的宽度，就会出现水平的滚动条。这并不是我们期望的结果，所以我们还要用到缩放属性   &lt;code&gt;initial-scale&lt;/code&gt;。计算公式如下：&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;Scale = deviceWidth / vWidth&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;这样的计算式不得不使用 JS 来实现，最终我们就能得到适配   &lt;strong&gt;Android 4.0 ~ 4.3&lt;/strong&gt; 定宽的代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) &amp;lt; 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector(&amp;apos;meta[name=&amp;quot;viewport&amp;quot;]&amp;apos;).setAttribute(&amp;apos;content&amp;apos;, &amp;apos;width=&amp;apos; + TARGET_WIDTH + &amp;apos;, initial-scale = &amp;apos; + scale + &amp;apos;, target-densitydpi=device-dpi&amp;apos;);
    }
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;其中，  &lt;code&gt;TARGET_WIDTH&lt;/code&gt; 就是你所期望的宽度，注意这段代码仅在   &lt;strong&gt;320-720px&lt;/strong&gt; 之间有效哦。&lt;/p&gt;
 &lt;h2&gt;缩放中的坑&lt;/h2&gt;
 &lt;p&gt;如果是 iPhone 或者 Android 4.4+ 的机器，在使用 scale 相关的属性时要非常谨慎，包括   &lt;code&gt;initial-scale&lt;/code&gt;,   &lt;code&gt;maximum-scale&lt;/code&gt; 和   &lt;code&gt;minimum-scale&lt;/code&gt;。  &lt;br /&gt;要么保证   &lt;strong&gt;Scale = deviceWidth / vWidth&lt;/strong&gt;，要么就尽量不用。来看一个例子：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="Android 4.4+ &amp;#21644; iPhone &amp;#22312;&amp;#32553;&amp;#25918;&amp;#26102;&amp;#30340;&amp;#34892;&amp;#20026;&amp;#19981;&amp;#19968;&amp;#33268;" src="http://segmentfault.com/img/bVrqtP" title="Android 4.4+ &amp;#21644; iPhone &amp;#22312;&amp;#32553;&amp;#25918;&amp;#26102;&amp;#30340;&amp;#34892;&amp;#20026;&amp;#19981;&amp;#19968;&amp;#33268;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在缩放比不能保证的情况下，即时设置同样的   &lt;code&gt;width&lt;/code&gt; 和   &lt;code&gt;initial-scale&lt;/code&gt; 后，两者的表现也是不一致。具体两种机型采用的策略如何我还没有探索出来，有兴趣的同学可以研究看看。最省事的办法就是在 iPhone 和 Android 4.4+ 上不设置 scale 相关的属性。&lt;/p&gt;
 &lt;h2&gt;总结&lt;/h2&gt;
 &lt;p&gt;结合上面所有的分析，你可以通过下面这段 JS 代码来对所有 iPhone 和 Android 4+ 的手机屏幕进行适配：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) &amp;lt; 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector(&amp;apos;meta[name=&amp;quot;viewport&amp;quot;]&amp;apos;).setAttribute(&amp;apos;content&amp;apos;, &amp;apos;width=&amp;apos; + TARGET_WIDTH + &amp;apos;, initial-scale = &amp;apos; + scale + &amp;apos;, target-densitydpi=device-dpi&amp;apos;);
    }
} else {
    document.querySelector(&amp;apos;meta[name=&amp;quot;viewport&amp;quot;]&amp;apos;).setAttribute(&amp;apos;content&amp;apos;, &amp;apos;width=&amp;apos; + TARGET_WIDTH);
}&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果你不希望你的页面被用户手动缩放，你还可以加上   &lt;code&gt;user-scalable=no&lt;/code&gt;。不过需要注意的是，这个属性在部分安卓机型上是无效的哦。&lt;/p&gt;
 &lt;h2&gt;其他参考资料&lt;/h2&gt;
 &lt;ol&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=88047"&gt;Supporting Different Screens in Web Apps - Android Developers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;p&gt;    &lt;a href="http://www.petelepage.com/blog/2013/02/viewport-target-densitydpi-support-is-being-deprecated/"&gt;Viewport target-densitydpi support is being deprecated&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;附录 - 测试页面&lt;/h2&gt;
 &lt;p&gt;有兴趣的同学可以拿这个测试页面来测测自己的手机，别忘了改   &lt;code&gt;viewport&lt;/code&gt; 哦。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=250, initial-scale=1.5, user-scalable=no&amp;quot;&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        body {
            margin: 0;
        }

        div {
            background: #000;
            color: #fff;
            font-size: 30px;
            text-align: center;
        }

        .block {
            height: 50px;
            border-bottom: 4px solid #ccc;
        }

        #first  { width: 100px; }
        #second { width: 200px; }
        #third  { width: 300px; }
        #fourth { width: 320px; }
        #log { font-size: 16px; }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&amp;quot;first&amp;quot; class=&amp;quot;block&amp;quot;&amp;gt;100px&amp;lt;/div&amp;gt;
    &amp;lt;div id=&amp;quot;second&amp;quot; class=&amp;quot;block&amp;quot;&amp;gt;200px&amp;lt;/div&amp;gt;
    &amp;lt;div id=&amp;quot;third&amp;quot; class=&amp;quot;block&amp;quot;&amp;gt;300px&amp;lt;/div&amp;gt;
    &amp;lt;div id=&amp;quot;fourth&amp;quot; class=&amp;quot;block&amp;quot;&amp;gt;320px&amp;lt;/div&amp;gt;
    &amp;lt;div id=&amp;quot;log&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
        function log(content) {
            var logContainer = document.getElementById(&amp;apos;log&amp;apos;);
            var p = document.createElement(&amp;apos;p&amp;apos;);
            p.textContent = content;
            logContainer.appendChild(p);
        }

        log(&amp;apos;body width:&amp;apos; + document.body.clientWidth)
        log(document.querySelector(&amp;apos;[name=&amp;quot;viewport&amp;quot;]&amp;apos;).content)
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&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 viewport android</category>
      <guid isPermaLink="true">https://itindex.net/detail/54801-%E7%A7%BB%E5%8A%A8-%E7%BD%91%E9%A1%B5-%E9%85%8D%E6%96%B9</guid>
      <pubDate>Tue, 08 Dec 2015 13:49:40 CST</pubDate>
    </item>
    <item>
      <title>Android应用安全开发之浅谈网页打开APP</title>
      <link>https://itindex.net/detail/55497-android-%E5%BA%94%E7%94%A8-%E5%AE%89%E5%85%A8</link>
      <description>&lt;p&gt;  &lt;strong&gt;Author:伊樵，呆狐，舟海@阿里移动安全&lt;/strong&gt;&lt;/p&gt;
 &lt;h1&gt;0x00 网页打开APP简介&lt;/h1&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;Android有一个特性，可以通过点击网页内的某个链接打开APP，或者在其他APP中通过点击某个链接打开另外一个APP（AppLink），一些用户量比较大的APP，已经通过发布其AppLink SDK，开发者需要申请相应的资格，配置相关内容才能使用。这些都是通过用户自定义的URI scheme实现的，不过背后还是Android的Intent机制。Google的官方文档  &lt;a href="https://developer.chrome.com/multidevice/android/intents"&gt;《Android Intents with Chrome》&lt;/a&gt;一文，介绍了在Android Chrome浏览器中网页打开APP的两种方法，一种是用户自定义的URI scheme（Custom URI scheme），另一种是“intent:”语法（Intent-based URI）。&lt;/p&gt;
 &lt;p&gt;&lt;/p&gt;
 &lt;p&gt;第一种用户自定义的URI scheme形式如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p1" src="http://static.wooyun.org//drops/20160422/2016042209202214629140.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;第二种的Intent-based URI的语法形式如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p2" src="http://static.wooyun.org//drops/20160422/2016042209202650760228.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;因为第二种形式大体是第一种形式的特例，所以很多文章又将第二种形式叫Intent Scheme URL，但是在Google的官方文档并没有这样的说法。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意：&lt;/strong&gt;使用Custom URI scheme给APP传递数据，只能使用相关参数来传递数据，不能想当然的使用scheme://host#intent;参数;end的形式来构造传给APP的intent数据。详见3.1节的说明。&lt;/p&gt;
 &lt;p&gt;此外，还必须在APP的Androidmanifest文件中配置相关的选项才能产生网页打开APP的效果，具体在下面讲。&lt;/p&gt;
 &lt;h1&gt;0x01 Custom Scheme URI打开APP&lt;/h1&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h3&gt;1.1 基本用法&lt;/h3&gt;
 &lt;p&gt;需求：使用网页打开一个APP，并通过URL的参数给APP传递一些数据。 &lt;/p&gt;
 &lt;p&gt;如自定义的Scheme为：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p3" src="http://static.wooyun.org//drops/20160422/2016042209202847271319.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意：&lt;/strong&gt;uri要用UTF-8编码和URI编码。&lt;/p&gt;
 &lt;p&gt;网页端的写法如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p4" src="http://static.wooyun.org//drops/20160422/2016042209202951459413.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;APP端接收来自网页信息的Activity，要在Androidmanifest.xml文件中Activity的intent-filter中声明相应action、category和data的scheme等。 &lt;/p&gt;
 &lt;p&gt;如在MainActivity中接收从网页来的信息，其在AndroidManifest.xml中的内容如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p5" src="http://static.wooyun.org//drops/20160422/2016042209203180208513.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在MainActivity中接收intent并且获取相应参数的代码：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p6" src="http://static.wooyun.org//drops/20160422/2016042209203319298614.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;另外还有以下几个API来获取相关信息： &lt;/p&gt;
 &lt;pre&gt;getIntent().getScheme(); //获得Scheme名称 
getIntent().getDataString(); //获得Uri全部路径 
getIntent().getHost(); //获得host
&lt;/pre&gt;
 &lt;h3&gt;1.2 风险示例&lt;/h3&gt;
 &lt;p&gt;常见的用法是在APP获取到来自网页的数据后，重新生成一个intent，然后发送给别的组件使用这些数据。比如使用Webview相关的Activity来加载一个来自网页的url，如果此url来自url scheme中的参数，如：  &lt;code&gt;jaq://jaq.alibaba.com?load_url=http://www.taobao.com&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;如果在APP中，没有检查获取到的load_url的值，攻击者可以构造钓鱼网站，诱导用户点击加载，就可以盗取用户信息。&lt;/p&gt;
 &lt;p&gt;接2.1的示例，新建一个WebviewActivity组件，从intent里面获取load_url，然后使用Webview加载url：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p7" src="http://static.wooyun.org//drops/20160422/2016042209203478610714.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;修改MainActivity组件，从网页端的URL中获取load_url参数的值，生成新的intent，并传给WebviewActivity：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p8" src="http://static.wooyun.org//drops/20160422/2016042209203674488812.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;网页端：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p9" src="http://static.wooyun.org//drops/20160422/2016042209203928263910.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;钓鱼页面：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p10" src="http://static.wooyun.org//drops/20160422/2016042209204215545109.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;点击“打开钓鱼网站”，进入APP，并且APP加载了钓鱼网站：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p11" src="http://static.wooyun.org//drops/20160422/20160422092044681191114.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;本例建议：&lt;/strong&gt;  &lt;br /&gt;
在Webview加载load_url时，结合APP的自身业务采用白名单机制过滤网页端传过来的数据，黑名单容易被绕过。&lt;/p&gt;
 &lt;h3&gt;1.3 阿里聚安全对开发者建议&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;APP中任何接收外部输入数据的地方都是潜在的攻击点，过滤检查来自网页的参数。&lt;/li&gt;
  &lt;li&gt;不要通过网页传输敏感信息，有的网站为了引导已经登录的用户到APP上使用，会使用脚本动态的生成URL Scheme的参数，其中包括了用户名、密码或者登录态token等敏感信息，让用户打开APP直接就登录了。恶意应用也可以注册相同的URL Sechme来截取这些敏感信息。Android系统会让用户选择使用哪个应用打开链接，但是如果用户不注意，就会使用恶意应用打开，导致敏感信息泄露或者其他风险。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h1&gt;0x02 Intent-based URI打开APP&lt;/h1&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h3&gt;2.1基本用法&lt;/h3&gt;
 &lt;p&gt;Intent-based URI语法：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p12" src="http://static.wooyun.org//drops/20160422/20160422092045385601211.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;注意：&lt;/strong&gt;第二个Intent的第一个字母一定要大写，不然不会成功调用APP。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;如何正确快速的构造网页端的intent？&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;可以先建个Android demo app，按正常的方法构造自己想打开某个组件的Intent对象，然后使用Intent的toUri()方法，会得到Intent对象的Uri字符串表示，并且已经用UTF-8和Uri编码好，直接复制放到网页端即可，切记前面要加上“intent:”。 &lt;/p&gt;
 &lt;p&gt;如：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p13" src="http://static.wooyun.org//drops/20160422/20160422092047772571310.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;结果：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p14" src="http://static.wooyun.org//drops/20160422/2016042209205169242148.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;S.load_url是跟的是intent对象的putExtra()方法中的数据。其他类型的数据可以一个个试。&lt;/p&gt;
 &lt;p&gt;如果在demo中的Intent对象不能传递给目标APP的Activity或其他组件，则其Uri形式放在网页端也不可能打开APP的，这样写个demo容易排查错误。&lt;/p&gt;
 &lt;p&gt;APP端中的Androidmanifest.xml的声明写法同2.1节中的APP端写法完全一样。对于接收到的uri形式的intent，一般使用Intent的parseUri()方法来解析产生新的intent对象，如果处理不当会产生Intent Scheme URL攻击。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;为何不能用scheme://host#intent;参数;end的形式来构造传给APP的intent数据？&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;这种形式的intent不会直接被Android正确解析为intent，整个scheme字符串数据可以使用Intent的getDataSting()方法获取到。 &lt;/p&gt;
 &lt;p&gt;如对于：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p15" src="http://static.wooyun.org//drops/20160422/2016042209205383294158.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在APP中获取数据：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p16" src="http://static.wooyun.org//drops/20160422/2016042209205421707167.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;结果是：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p17" src="http://static.wooyun.org//drops/20160422/2016042209205810120179.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;由上图可知Android系统自动为Custom URI scheme添加了默认的intent。 &lt;/p&gt;
 &lt;p&gt;要想正确的解析，还需使用Intent的parseUri()方法对getDataString()获取到的数据进行解析，如：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p18" src="http://static.wooyun.org//drops/20160422/2016042209210067503188.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;2.2 风险示例&lt;/h3&gt;
 &lt;p&gt;关于Intent-based URI的风险我觉得  &lt;a href="http://blog.csdn.net/l173864930/article/details/36951805"&gt;《Android Intent Scheme URLs攻击》&lt;/a&gt;和  &lt;a href="http://drops.wooyun.org/papers/2893"&gt;《Intent Scheme URL attack》&lt;/a&gt;这两篇文章写的非常好，基本把该说的都都说了，我就不多说了，大家看这两篇文章吧。&lt;/p&gt;
 &lt;h3&gt;2.3 阿里聚安全对开发者建议&lt;/h3&gt;
 &lt;p&gt;上面两篇文章中都给出了安全使用Intent Scheme URL的方法：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="p19" src="http://static.wooyun.org//drops/20160422/2016042209210170821196.jpg"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;除了以上的做法，还是不要信任来自网页端的任何intent，为了安全起见，使用网页传过来的intent时，还是要进行过滤和检查。&lt;/p&gt;
 &lt;h1&gt;0x03 参考&lt;/h1&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;ol&gt;
  &lt;li&gt;   &lt;a href="https://developer.chrome.com/multidevice/android/intents"&gt;Android Intents with Chrome&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://drops.wooyun.org/papers/2893"&gt;Intent scheme URL attack&lt;/a&gt; &lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.jssec.org/dl/android_securecoding_en.pdf"&gt;Android Appliaction Secure Design/Secure Coding Guidebook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://developer.android.com/intl/zh-cn/training/app-links/index.html"&gt;Handling App Links&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0718/3200.html"&gt;Android M App Links: 实现, 缺陷以及解决办法&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://blog.csdn.net/l173864930/article/details/36951805"&gt;Android Intent Scheme URLs攻击&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>移动安全</category>
      <guid isPermaLink="true">https://itindex.net/detail/55497-android-%E5%BA%94%E7%94%A8-%E5%AE%89%E5%85%A8</guid>
      <pubDate>Fri, 22 Apr 2016 18:05:40 CST</pubDate>
    </item>
    <item>
      <title>巧用网页分析“反击”钓鱼网站</title>
      <link>https://itindex.net/detail/53935-%E7%BD%91%E9%A1%B5-%E5%88%86%E6%9E%90-%E5%8F%8D%E5%87%BB</link>
      <description>&lt;p&gt;  &lt;strong&gt;接触网络一段时间的用户都会多多少少遇到一些钓鱼网站，而作为开发者的我们遇到的钓鱼网站更是数不胜数，有时稍不留神就会被钓鱼网站将自己的重要信息钓走，对于钓鱼网站我们也是咬牙切齿，当我们发现钓鱼网站后，我们总是气愤后关闭离开，或者有些正义感的朋友会选择举报该网站。这一次我来讲个更绝的，通过分析钓鱼网站获得其传输方法，发送垃圾数据反击作者。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;先说说今天的经历，今天早上去了一趟科技市场，回来发现自己两台不同的手机多了两条相同的发自95588的信息，大致的内容是我的工行密保被冻结了，登陆wap-icbce.pw去解冻，咋看下去好像确实有点蹊跷，登陆上去还真是有模有样:&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/1437380471875.png!small" title="&amp;#38035;&amp;#40060;&amp;#32593;&amp;#31449;&amp;#20027;&amp;#39029;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我乱输入了一个卡号和密码，会显示这样的一个界面  &lt;br /&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/1437380506625.png!small" title="QQ&amp;#25130;&amp;#22270;20150720153300.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;无论你输入什么，他都会显示这样一个界面，后来我又去工行确认了一下，人工服务告诉我这确实是一个钓鱼网站。我作为一个一身正气的人怎么能置之不理呢？首先我先举报了这个网站，发现这还没解气，我尝试使用电脑登陆这个网站，发现这个网站为了防止电脑的防护软件还不让电脑来登陆了。&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/14373805191015.png!small" title="QQ&amp;#25130;&amp;#22270;20150720153556.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;没关系，使用强大的chrome我成功的在电脑上打开了这个网站（方法不具体说明了，详细参阅百度百科上的chrome开启手机网站的方法）&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/1437380531785.png!small" title="QQ&amp;#25130;&amp;#22270;20150720153712.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;现在我们可以开始分析网站的源代码了，该钓鱼网站使用.net技术进行编写，感觉网站除了设计方面剩下都是粗制滥造，验证码也是放着其实并没有进行验证，网站的首页是一个识别是否是手机登陆的小图片，如果是手机登陆点击会使用submit字段随便传回一段字符，猜测在该网页的服务器端判断这submit字符应该仅仅使用是否为null来进行判断。  &lt;br /&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/14373805684904.png!small" title="QQ&amp;#25130;&amp;#22270;20150720154101.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/14373805692183.png!small" title="QQ&amp;#25130;&amp;#22270;20150720154134.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;进入这个登录界面，我们能看到里边table字段中清晰的写着用户名密码以及验证码的name，而且获取方式是使用post获取，字符编码使用gb2312，网站分析基本到此结束，现在好戏开场了，首先我使用java编写一段发送数据至asp的java访问代码：&lt;/p&gt;
 &lt;pre&gt;  public static String doPost(String reqUrl, Map parameters, String recvEncoding) {
   HttpURLConnection conn = null;
   String responseContent = null;
   try {
     StringBuffer params = new StringBuffer();
     for (Iterator iter = parameters.entrySet().iterator(); iter.hasNext();) {
       Entry element = (Entry) iter.next();
       params.append(element.getKey().toString());
       params.append(&amp;quot;=&amp;quot;);
       params.append(URLEncoder.encode(element.getValue().toString(), recvEncoding));
       params.append(&amp;quot;&amp;amp;&amp;quot;);
     }
     if (params.length() &amp;gt; 0) {
       params = params.deleteCharAt(params.length() - 1);
     }
     URL url = new URL(reqUrl);
     HttpURLConnection url_con = (HttpURLConnection) url.openConnection();
     url_con.setRequestMethod(&amp;quot;POST&amp;quot;);
     url_con.setConnectTimeout(5000);//（单位：毫秒）jdk
     url_con.setDoOutput(true);
     byte[] b = params.toString().getBytes();
     url_con.getOutputStream().write(b, 0, b.length);
     url_con.getOutputStream().flush();
     url_con.getOutputStream().close();
     InputStream in = url_con.getInputStream();
     BufferedReader rd = new BufferedReader(new InputStreamReader(in, recvEncoding));
     String tempLine = rd.readLine();
     StringBuffer tempStr = new StringBuffer();
     String crlf = System.getProperty(&amp;quot;line.separator&amp;quot;);
     while (tempLine != null) {
       tempStr.append(tempLine);
       tempStr.append(crlf);
       tempLine = rd.readLine();
     }
     responseContent = tempStr.toString();
     rd.close();
     in.close();
   } catch (IOException e) {
     e.printStackTrace();
   } finally {
     if (conn != null) {
       conn.disconnect();
     }
   }
   return responseContent;
 }&lt;/pre&gt;
 &lt;p&gt;测试没问题后，既然要冲击网站，我们先要随机一些银行卡用户名和密码。&lt;/p&gt;
 &lt;p&gt;先定义一些需要用到的参数以及要攻击网站的action以及发送的字符编码：&lt;/p&gt;
 &lt;pre&gt;Map m = new HashMap();
String url = &amp;quot;http://wap-icbce.pw/add_1.asp&amp;quot;;
String code = &amp;quot;gb2312&amp;quot;;&lt;/pre&gt;
 &lt;p&gt;首先银行卡前缀一般是固定的，前十位不随机生成：&lt;/p&gt;
 &lt;pre&gt;String randomcarnum10=&amp;quot;6222022102&amp;quot;;&lt;/pre&gt;
 &lt;p&gt;后9位就进行一个随机生成的工作：&lt;/p&gt;
 &lt;pre&gt;int randomnum;
randomnum = (int) (Math.random()*1000000000);
randomcarnum10+=randomnum;&lt;/pre&gt;
 &lt;p&gt;按照大部分人的习惯，密码都是前面几个字母+后边几个数字，我们随机前后字母的数量随机生成：&lt;/p&gt;
 &lt;pre&gt;int loopentime,loopnumtime;
loopentime=(int) (Math.random()*10);//随机的英文密码长度
loopnumtime=(int) (Math.random()*10);//随机的中文密码长度
String randompsw = &amp;quot;&amp;quot;;
String chars = &amp;quot;abcdefghijklmnopqrstuvwxyz&amp;quot;;
for(int i=0;i
randompsw+=chars.charAt((int)(Math.random() * 26));
for(int i=0;i
randompsw+=(int)(Math.random() * 10);&lt;/pre&gt;
 &lt;p&gt;为了以防万一，验证码也随机生成提交：&lt;/p&gt;
 &lt;pre&gt;String randomidcode;
randomidcode=(int)(Math.random()*10000)+&amp;quot;&amp;quot;;&lt;/pre&gt;
 &lt;p&gt;最后将数据直接发送到钓鱼网站服务器端：&lt;/p&gt;
 &lt;pre&gt;m.put(&amp;quot;logonCardNum&amp;quot;, randomcarnum);
m.put(&amp;quot;netType&amp;quot;, randompsw);
m.put(&amp;quot;randomId&amp;quot;, randomidcode);
String rus = doPost(url, m, code);
//
System.out.println(rus);&lt;/pre&gt;
 &lt;p&gt;让我们看看成果吧，在程序代码的最后我在控制台中输出了钓鱼网站返回的html文本内容&lt;/p&gt;
 &lt;p&gt;  &lt;img src="http://image.3001.net/images/20150720/14373805923915.png!small" title="QQ&amp;#25130;&amp;#22270;20150720160344.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;对比一下钓鱼网站出现的等待跳转界面的html文本内容，基本能确定应该是已经上传数据成功了！写个循环，让程序跑个2小时&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;这次网站攻击采用了简单的网页关键信息获取方法，以及编写简单的java代码，达到反击钓鱼网站的目的，写这篇文章也是希望能够抛砖引玉，大神轻喷，我有什么建议私信给我，我会很乐于接受多学习多改进。&lt;/strong&gt;  &lt;br /&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;*本文作者：bawanag，转载须注明来自FreeBuf黑客与极客（FreeBuf.COM）&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>WEB安全</category>
      <guid isPermaLink="true">https://itindex.net/detail/53935-%E7%BD%91%E9%A1%B5-%E5%88%86%E6%9E%90-%E5%8F%8D%E5%87%BB</guid>
      <pubDate>Mon, 20 Jul 2015 21:00:15 CST</pubDate>
    </item>
    <item>
      <title>影响网页渲染的关键！</title>
      <link>https://itindex.net/detail/53793-%E7%BD%91%E9%A1%B5-%E6%B8%B2%E6%9F%93</link>
      <description>&lt;div&gt;
  &lt;p&gt;原文链接：   &lt;a href="http://www.gbtags.com/gb/share/5599.htm"&gt;http://www.gbtags.com/gb/share/5599.htm&lt;/a&gt;&lt;/p&gt;
  &lt;p&gt;经常有站长、开发者、运维疑惑：为什么我们的后台服务器很快，但是用户要看网页里面的内容却需要很长时间？我们在上一篇文章   &lt;a href="http://news.yesky.com/prnews/420/58034920.shtml" target="_blank"&gt;《怪兽大作战: 解析网站打开慢的原因》&lt;/a&gt;中简单介绍了影响网站打开速度的几个指标，感兴趣的同学可以再读一下。今天我们主要讲一下，是哪些因素拖慢了我们的首屏加载时间，也就是用户看到网页中内容时所等待的时间。&lt;/p&gt;
  &lt;p&gt;   &lt;img alt="ruby-prof" src="http://code.oneapm.com/images/render/1.png"&gt;&lt;/img&gt;&lt;/p&gt;
  &lt;p&gt;用过   &lt;a href="http://www.gbtags.com/gb/share/&amp;" target="_blank"&gt;OneAPM&lt;/a&gt;的读者对这幅图肯定不陌生，一般来讲，如果服务器很快，机房所在线路很快，那么影响用户看到网页内容的主要时间，就是最后两个时间阶段：DOM处理以及网页渲染，在这两个阶段中，浏览器需要解析网页中的各种资源并进行渲染，最终形成用户页面。这个过程是否流畅，直接影响到用户需要等待的时间，从更深层次而言，直接会影响最终的用户体验，现在大家也普遍接受一个观点“延迟就是故障”，所以你需要重视网站的加载速度。&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;打造轻量级的资源路径--关键渲染路&lt;/h2&gt;
  &lt;p&gt;网页加载速度中最重要的概念是关键渲染路径。如果能理解好这个概念，的确可以让用户更快看到网页中的内容。&lt;/p&gt;
  &lt;p&gt;轻量级资源和路径，可以缩短复杂网页的构建和渲染时间，甚至比简单网页还要快！ 由于大多数网页都包含许多不同的组成部分，仅仅移除部分资源并不能保证更快的加载速度。 如果你曾经想过：“为了提高网页的加载速度，我还能做什么？”或者“新浪、QQ、网易是如何做到在一秒钟内加载那么多网页内容的？”那么关键渲染路径这个概念正是你需要了解的。&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;什么是关键渲染路径？&lt;/h2&gt;
  &lt;p&gt;清楚起见，让我们先定义一些概念：&lt;/p&gt;
  &lt;blockquote&gt;
   &lt;p&gt;关键：绝对需要     &lt;br /&gt;渲染：显示或者展示（在我们的情境中，网页经过渲染才能呈现给用户）     &lt;br /&gt;路径：使我们的网页展示在浏览器中的一系列事件     &lt;br /&gt;首屏：是用户滚动页面之前就能看见的部分。&lt;/p&gt;
&lt;/blockquote&gt;
  &lt;p&gt;因此，换言之，渲染路径就是一系列使你的网页呈现在浏览器中的事件。而关键渲染路径是呈现网页首屏所需的那些事件。因为几乎所有网站在渲染网页时都包含了不必要的步骤，而减少这些不必要的路径，能使你的网页加载速度提高几秒钟，这也是提高网页速度的最快方法。&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;路径&lt;/h2&gt;
  &lt;p&gt;为了显示一张网页，浏览器必须获取网页所需的所有资源。一个简单的例子：一个网页需要一张图片，一个CSS文件，一个JavaScript文件。&lt;/p&gt;
  &lt;p&gt;我们来看看这张网页在展示之前经历的路径：&lt;/p&gt;
  &lt;ol&gt;
   &lt;li&gt;浏览器下载html文件&lt;/li&gt;
   &lt;li&gt;浏览器读取html文件，发现里面涉及一个CSS文件，一个JavaScript文件和一张图片&lt;/li&gt;
   &lt;li&gt;浏览器开始下载这张图片&lt;/li&gt;
   &lt;li&gt;浏览器发现不获取CSS和JavaScript文件就无法显示网页&lt;/li&gt;
   &lt;li&gt;浏览器下载CSS文件并读取之，确保除此之外没有别的文件需要被访问&lt;/li&gt;
   &lt;li&gt;浏览器发现不获取JavaScript文件还是无法显示网页&lt;/li&gt;
   &lt;li&gt;浏览器下载JavaScript文件并读取之，确保除此之外没有别的文件需要被访问&lt;/li&gt;
   &lt;li&gt;浏览器发现现在可以显示网页了&lt;/li&gt;
&lt;/ol&gt;
  &lt;p&gt;上面的路径是简单网页的加载过程。现在，试想一下你的网页加载路径会怎么样？你很可能会有几个交互按钮，数个CSS和JavaScript文件，很多图片和小插件，甚至可能还有音频或者视频文件。这意味着，你的渲染路径很可能会像一个大迷宫。大多数网站的渲染路径都极其复杂，因为浏览器在显示网页之前需要加载的文件太多。这就是你可以超过他人的地方。如果你让自己的网页加载得比竞争者的快，你就能获得访问者的青睐（百度就喜欢这样的开发者），例如新浪微博的路径就是这样的：&lt;/p&gt;
  &lt;p&gt;   &lt;img alt="ruby-prof" src="http://code.oneapm.com/images/render/2.png"&gt;&lt;/img&gt;&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;渲染过程&lt;/h2&gt;
  &lt;p&gt;在展示网页所需的众多资源中，存在一些资源会阻塞网页的渲染过程。最常见的两种资源就是CSS文件和JavaScript文件。不管你需要多少个这样的文件，浏览器必须逐一下载并分析这些文件，然后才能给用户展示内容。让我们来看一个最常见不过的场景：&lt;/p&gt;
  &lt;p&gt;WordPress博客使用主题。几乎每一个WordPress主题都包含多个CSS文件。&lt;/p&gt;
  &lt;p&gt;许多主题包含六七个CSS文件。所有的CSS文件都可以合并到一个文件中，但是当你添加主题时，会包含多个CSS文件。因此，在你的博客显示哪怕一个字之前，浏览器都不得不经过六七次的与服务器交互，把这些文件一个个地下载下来，并分析读取，之后才能开始显示。&lt;/p&gt;
  &lt;p&gt;在加载的过程中，访问者都只能看到一篇空白的屏幕。因为只有当关键步骤完成以后，才会有东西显示。&lt;/p&gt;
  &lt;p&gt;但是，即便下载完这些CSS文件，你的博客还是不能完成渲染。因为WordPress主题还需要几个JavaScript文件。&lt;/p&gt;
  &lt;p&gt;因此，渲染一页典型的WordPress博客网页，需要浏览器与服务器交互大约20次，才能将主要的CSS和JavaScript文件下载完毕。但是，等等，现在你还需要交互按钮，小插件……噢，不，针对每一个这样的部件，你还要下载几个CSS，JavaScript文件。&lt;/p&gt;
  &lt;p&gt;你可能要下载几十个文件，才能让自己的博客展示在用户面前。真是麻烦！（去查查你的网页都要加载什么文件，可以使用OneAPM 的SessionTrace 功能看看网页加载资源在用户那里的速度）&lt;/p&gt;
  &lt;p&gt;但是事情不仅限于WordPress，本文只是拿它举个例子而已。通常创建网页的初始视图都很多资源，因此会产生多个请求。&lt;/p&gt;
  &lt;p&gt;   &lt;img alt="ruby-prof" src="http://code.oneapm.com/images/render/3.png"&gt;&lt;/img&gt;&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;关键&lt;/h2&gt;
  &lt;p&gt;目前我只是描绘了一张非常朦胧的蓝图。好消息是：你可以为你的网页请求一    &lt;br /&gt;百万个资源，其中包括12000张图像，200个JavaScript文件，而这些都可能在一秒钟内加载完成。&lt;/p&gt;
  &lt;p&gt;这是如何实现的呢？&lt;/p&gt;
  &lt;p&gt;只要理解对你的网站而言，显示首屏的内容所需的关键步骤，就能实现。&lt;/p&gt;
  &lt;p&gt;最优化渲染路径，实际上只要聚焦三件事情：&lt;/p&gt;
  &lt;blockquote&gt;
   &lt;p&gt;最小化关键资源的数量     &lt;br /&gt;最小化关键字节数     &lt;br /&gt;最小化关键路径的长度&lt;/p&gt;
&lt;/blockquote&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;理解页面加载速度的测量办法&lt;/h2&gt;
  &lt;p&gt;当百度谈论页面加载速度时，他们并不是指加载一个网页的总时间。他们说的是用户看到首屏所需的时间，以及用户可以开始与页面内容进行交互所需的时间。&lt;/p&gt;
  &lt;p&gt;百度之所以开始采用页面加载速度作为影响要素，是基于他们用户的满意度。当用户使用百度搜索时，他们要是被带到加载时间很长的页面，无疑是很糟糕的经历。&lt;/p&gt;
  &lt;p&gt;人们向百度抱怨，他们说：“为什么将我带到一个加载如此缓慢的页面？”显然，人们感知到了速度的差别。&lt;/p&gt;
  &lt;p&gt;如果一个用户要盯着一个空白的网页10秒之久等待它加载内容，这无疑是很差的体验。百度不想在他们的结果中出现这样的页面。如果那个页面能在1秒内显示内容，这就是极好的用户体验，这才是百度想要的结果。&lt;/p&gt;
  &lt;p&gt;我们讨论网页速度时最关注的就是将初首屏的内容尽早地显示给用户。 通过OneAPM SessionTrace 功能可以查看各个资源的加载速度，方便调整加载资源的策略，例如&lt;/p&gt;
  &lt;p&gt;   &lt;img alt="ruby-prof" src="http://code.oneapm.com/images/render/4.png"&gt;&lt;/img&gt;&lt;/p&gt;
  &lt;div&gt; &lt;/div&gt;
  &lt;h2&gt;后续：&lt;/h2&gt;
  &lt;p&gt;其实，优化网页渲染路还有很多小技巧、插件、方法等待，未来我们将在后续的文章中一一和大家分享。&lt;/p&gt;
  &lt;p&gt;本文系   &lt;a href="http://www.gbtags.com/gb/share/&amp;" target="_blank"&gt;OneAPM&lt;/a&gt;工程师编译整理。OneAPM是中国基础软件领域的新兴领军企业，能帮助企业用户和开发者轻松实现：缓慢的程序代码和SQL语句的实时抓取。想阅读更多技术文章，请访问OneAPM   &lt;a href="http://www.gbtags.com/gb/share/&amp;" target="_blank"&gt;官方技术博客&lt;/a&gt;。&lt;/p&gt;
  &lt;p&gt;原文链接：   &lt;a href="http://www.gbtags.com/gb/share/5599.htm"&gt;http://www.gbtags.com/gb/share/5599.htm&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
          
           &lt;br /&gt; &lt;br /&gt;
          
             &lt;a href="http://kvhur.iteye.com/blog/2222827#comments"&gt;已有   &lt;strong&gt;0&lt;/strong&gt; 人发表留言，猛击-&amp;gt;&amp;gt;  &lt;strong&gt;这里&lt;/strong&gt;&amp;lt;&amp;lt;-参与讨论&lt;/a&gt;
          
           &lt;br /&gt; &lt;br /&gt; &lt;br /&gt;
ITeye推荐
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;   &lt;a href="http://www.iteye.com/clicks/433" target="_blank"&gt;—软件人才免语言低担保 赴美带薪读研！— &lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
 &lt;br /&gt; &lt;br /&gt; &lt;br /&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 />
      <guid isPermaLink="true">https://itindex.net/detail/53793-%E7%BD%91%E9%A1%B5-%E6%B8%B2%E6%9F%93</guid>
      <pubDate>Mon, 29 Jun 2015 14:33:54 CST</pubDate>
    </item>
    <item>
      <title>小米路由器劫持特定网页插入应用推广广告</title>
      <link>https://itindex.net/detail/53722-%E5%B0%8F%E7%B1%B3-%E8%B7%AF%E7%94%B1%E5%99%A8-%E5%8A%AB%E6%8C%81</link>
      <description>陈少举 写道 &amp;quot;2015年06月19日，V2EX社区有用户报告称，访问豆瓣电影时，网页上会出现小米路由手机App的广告信息。虽然小米路由器劫持HTTP404错误页面、HTTP403错误页面，已经遭人诟病，不过事后小米官方推出了关闭HTTP错误劫持功能的开关。 通过进一步分析小米路由器的页面劫持广告原理，发现存在一个具体的劫持清单，若用户访问的网址存在于此清单中，则会劫持页面并插入广告代码，而此代码存储于&amp;amp;nbsp;api.miwifi.com&amp;amp;nbsp;，全程明文通信，并且没有任何加密。 由于目前小米路由器没有加密认证机制确保传输的广告内容不被更改，若小米路由器的广告代码访问域名&amp;amp;nbsp;api.miwifi.com 被网络运营商或恶意劫持，则可能可以推送任意内容，包括恶意内容给小米路由器的用户。 此类智能路由器作为用户的入口，可能存在较多隐患，加上厂商对于用户所传输的数据进行精确掌控与收集，以及进行大数据的处理与分析，未来可能会对用户的隐私产生难以预料的影响。&amp;quot; &lt;img border="0" height="1" src="http://solidot.org.feedsportal.com/c/33236/f/556826/s/47617c79/sc/3/mf.gif" width="1"&gt;&lt;/img&gt; &lt;br /&gt; &lt;br /&gt; &lt;br /&gt; &lt;a href="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/1/rc.htm" rel="nofollow"&gt;  &lt;img border="0" src="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/1/rc.img"&gt;&lt;/img&gt;&lt;/a&gt; &lt;br /&gt; &lt;a href="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/2/rc.htm" rel="nofollow"&gt;  &lt;img border="0" src="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/2/rc.img"&gt;&lt;/img&gt;&lt;/a&gt; &lt;br /&gt; &lt;a href="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/3/rc.htm" rel="nofollow"&gt;  &lt;img border="0" src="http://rc.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/rc/3/rc.img"&gt;&lt;/img&gt;&lt;/a&gt; &lt;br /&gt; &lt;br /&gt; &lt;a href="http://da.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/a2.htm"&gt;  &lt;img border="0" src="http://da.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/a2.img"&gt;&lt;/img&gt;&lt;/a&gt; &lt;img border="0" height="1" src="http://pi.feedsportal.com/r/228766855570/u/49/f/556826/c/33236/s/47617c79/a2t.img" width="1"&gt;&lt;/img&gt; &lt;div&gt;
  &lt;a href="http://feeds.feedburner.com/~ff/solidot?a=0jQmt6EQS6w:MKRYIym5b-c:yIl2AUoC8zA"&gt;   &lt;img border="0" src="http://feeds.feedburner.com/~ff/solidot?d=yIl2AUoC8zA"&gt;&lt;/img&gt;&lt;/a&gt;   &lt;a href="http://feeds.feedburner.com/~ff/solidot?a=0jQmt6EQS6w:MKRYIym5b-c:7Q72WNTAKBA"&gt;   &lt;img border="0" src="http://feeds.feedburner.com/~ff/solidot?d=7Q72WNTAKBA"&gt;&lt;/img&gt;&lt;/a&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 />
      <guid isPermaLink="true">https://itindex.net/detail/53722-%E5%B0%8F%E7%B1%B3-%E8%B7%AF%E7%94%B1%E5%99%A8-%E5%8A%AB%E6%8C%81</guid>
      <pubDate>Fri, 19 Jun 2015 15:41:26 CST</pubDate>
    </item>
    <item>
      <title>网页大图片应用技巧浅析</title>
      <link>https://itindex.net/detail/52914-%E7%BD%91%E9%A1%B5-%E5%9B%BE%E7%89%87-%E5%BA%94%E7%94%A8</link>
      <description>&lt;p&gt;用图像创造场景感，增强用户的真实体验，近些年来，图片作为背景填充整个屏幕的设计越来越广泛，曾经只有时尚潮流网站或者摄影类站点才会特别依赖富有冲击力的图像，但是现在几乎任何类型的网站都有可能采用这种大图片作为背景，特别是企业品牌宣传、产品介绍等等。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310191850651.jpg" target="_blank"&gt;   &lt;img alt="&amp;#22270;1" height="373" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310191850651-590x373.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&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;1 .提炼关键词寻找图片&lt;/p&gt;
 &lt;p&gt;首先需要提炼产品特性关键词，通过关键词的形式寻找合适的图片。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192229828.png" target="_blank"&gt;   &lt;img alt="QQ&amp;#25130;&amp;#22270;20150106144641" height="338" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192229828-590x338.png" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上图是google云盘官网banner背景为一张在飞机向外看的图片，不难发现用这种比拟手法来表现云盘的口号“随时随地使用”，在任何时候、任何地点都可以使用文件。换位思考，假如我们来设计google云盘官网，首先提炼出关键词：随时随地、自动同步、超大空间等等，用最能突出特点和有代表性的关键词来寻找合适的图片。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192556455.png" target="_blank"&gt;   &lt;img alt="{7101B016-4F1A-4D88-B17C-2AD8109BBF35}" height="318" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192556455-590x318.png" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上图是IBM旗下的MobileFirst产品，其主要为用户移动应用从创建到维护推出的一体化解决方案为广大用户提供更便利服务，那么根据项目提炼出关键词：手机、用户，该网站用了一张人拿着手机进行使用的图片，目的是为了突出项目的业务特点。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192252367.jpg" target="_blank"&gt;   &lt;img alt="apple" height="314" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192252367-590x314.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192259569.png" target="_blank"&gt;   &lt;img alt="QQ&amp;#25130;&amp;#22270;20150310173705" height="345" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192259569-590x345.png" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上图分别为苹果官网、研究无线电技术企业——高通的网站中运用的图片同样也是通过产品特点提炼的关键词来完成图片的选用。&lt;/p&gt;
 &lt;p&gt;2. 图片品牌化&lt;/p&gt;
 &lt;p&gt;什么是图片品牌化？通过关键词寻找到的图片进行深度加工处理，使之在项目中运用到的图片都是一整套的、风格统一的，比如可以在图片的色调、色彩饱和度与特色风格上做调整等等，下面举几个案例。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192310137.jpg" target="_blank"&gt;   &lt;img alt="google-Drive" height="373" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192310137-590x373.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;Google Drive官网摘取出来的图片，这些图片不难发现无论在色调、色彩饱和度上是一致的，特点是低饱和度、颜色深沉和图片噪点，颜色上新颖有特色、风格上具有明显的品牌统一性。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192327495.jpg" target="_blank"&gt;   &lt;img alt="SAP" height="373" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192327495-590x373.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;上图为SAP公司官网的系列图片，作为全球最大的B2B企业为各行各业、不同规模的其他企业提供解决方案，正如它运用不同行业场景的图片一样：色彩丰富、但色调又不过于鲜艳，它们所有用到的图片都有这些特点。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192344269.jpg" target="_blank"&gt;   &lt;img alt="qualcomm" height="242" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150310192344269-590x242.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&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;a href="http://isux.tencent.com/wp-content/uploads/2015/03/20150311174635827.jpg" target="_blank"&gt;   &lt;img alt="&amp;#37197;&amp;#22270;2" height="253" src="http://isux.tencent.com/wp-content/uploads/2015/03/20150311174635827-590x253.jpg" width="590"&gt;&lt;/img&gt;&lt;/a&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;/p&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>视觉设计 图片应用 技巧 视觉</category>
      <guid isPermaLink="true">https://itindex.net/detail/52914-%E7%BD%91%E9%A1%B5-%E5%9B%BE%E7%89%87-%E5%BA%94%E7%94%A8</guid>
      <pubDate>Wed, 11 Mar 2015 18:17:39 CST</pubDate>
    </item>
  </channel>
</rss>

