[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002Fweb\u002F2":3},{"items":4,"total":122},[5,20,30,41,49,60,68,77,86,96,105,114],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":14,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":19},174,"article","notes-for-2nd-frontend-exp-conf","第二届前端体验大会杂记","2017-12-26 09:37","web",[13],"会议","\n前端体验大会于我来说并非一个新鲜事物了。即便是担任讲师嘉宾这件事情，也不是第一次，因此也并没有什么太激动的地方。参会完之后本来想着，我本不是一个张扬的人，就让它归于尘土吧。但细细回想起来，本次大会带给我的，还是有挺多前所未有的感觉，因此从一个不起眼的角度来记录一下，也挺好。\n\n## 评审很煎熬\n\n本次大会公司有给予赞助，考虑到品牌露出更自然，我也就联系了一下主办方，申请了一个演讲主题。虽然明知道近段时间忙得跟狗一样，但还是对自己恶吼了一句“你他妈不是就喜欢被事情压着跑么？”\n\n主题还没准备完，就要进入评审阶段，于是两次翘班出去咖啡馆跟主办方过PPT。不得不说，翘班去咖啡馆的感觉还是挺爽的。但是评审PPT就没那么爽了。\n\n熟悉的人都知道，彪叔是一个三句话就要上升到人生哲学高度的人，因此听他把PPT的评审意见讲完并不是一件很轻松的事情。虽然自认为在多年的会议中已经练就了非常好的理解和归纳能力，但是要完全理会彪叔的意见还是很有挑战性。因此两次评审其实多少都有点煎熬，总是为自己的PPT感觉到有点焦虑，担心是否和主办方的主题契合。\n\n\u003C!-- more -->\n\n![评审](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F01.jpg)\n\n## 硬广告\n\n早上早早到了会场，大屏幕正播放着赞助商的广告。大家的广告都很文艺，有的还拍成了微电影。切到我们的广告时，稍微有点惊——我们的广告是硬广，一股小尴尬的情绪涌上来。\n\n下午上场前只好自嘲一番，“富途的广告一向很硬，背后是硬实习撑腰”。好在，公司的同事都很给力，让我说这句话的时候并不觉得很脸红。\n\n演讲结尾的故事很有趣，我放了一个招聘广告的二维码。还没来得及说明这是另一个硬广，大家就已经举起手机纷纷扫描了，我能咋办呢？\n\n![硬广告](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F02.jpg)\n\n![赞助礼品](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F03.jpg)\n\n## 感受生活状态\n\n这个标题有点装了，但也确实是现场的感受。开场前能感受到每个人在为会议而奔忙。看到了摄影师一瞬间放空的状态，看到了主持人妹子认真而紧张的状态，看到了设备同学自信而又略显担心的状态。\n\n我在现场卖梗，在猫和老鼠的笑话后，看到大家很放松的状态。在演讲完成后问大家我讲得好吗，看到了大家热情的状态。\n\n看到我们公司的妹子们中奖后略显高兴又不好意思流露出来的状态。\n\n中场休息的时候，到楼下看到了擦车哥的宝宝，楼上是爸爸认真工作，楼下是妈妈认真带小孩，有一种互相并不打扰但又相依相守的状态。\n\n罗磊的演讲让我看到一个技术人对待技术和生活时的状态。\n\n结尾的煽情让我看到主办方、主持人真实的心理状态，并不是春晚那样套路话一套溜完然后难忘今宵。\n\n……\n\n总而言之，这场会让我真切地感受到了在技术交流背后的人，他们都有各自的心情、感受，有各自想表达的情绪、想说的话，支撑这些的是每个人都在努力的生活。从一场技术会议上见到技术人背后的生活，不是一件容易的事情。\n\n\n![主持人1](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F04.jpg)\n\n![主持人2](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F05.jpg)\n\n![摄影师](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F06.jpg)\n\n![擦车哥的宝宝](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F07.jpg)\n\n## 主办方的焦虑\n\n这场会议从反馈来看，还是非常成功的，属于少有的评价一边倒的会议。因为参加过评审，以前自己也主办过一些活动，所以我完成能想象主办方在背后付出了多少。\n\n一场会议能够给人留下良好的印象，并不是一件偶然的事情，它的背后是主办方一个点一个点地去确认和推动的过程。\n\n那么，即便是印象如此好的一场会议，能够给参会者带来的除了奖品，还有什么？再放大一点说，能够对行业的用户体验有切实的推动作用吗？\n\n这是主办方和参会者诉求不一样的地方，也正是他们所焦虑的事情。\n\n但我想，既然有一群人初心不改，能够继续在这个层次去考虑问题，那么明年的效果必然也不会差。所谓取乎其上，得乎其中。\n\n## 结\n\n多大的事都会有结束的时候，会议也是。周日休整一天，周一又开始忙忙碌碌的工作。每个人有自己的轨迹，在人海中也看不出你是会议的主持人还是策划，是观众或者是嘉宾。\n\n还好，互联网的时代，除了相忘于江湖，我们还有可以沟通的渠道。\n\n最后，一个未解之谜忍了好几天：为什么现场投票了最佳讲师，却不给我颁奖？\n\n![最佳讲师](\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F08.jpg)\n",null,0,"2026-08-28 04:37:17","published","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F01.jpg",{"id":21,"type":7,"slug":22,"title":23,"date":24,"category":11,"tags":25,"body_markdown":27,"permalink":28,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":29},172,"migrate-blog-to-amp","轻松迁移博客到AMP","2017-08-29 20:18",[26],"AMP","\nAMP 全称 Accelerated Mobile Pages ，是由 Google 提出的一种移动端页面的规范。相比普通 HTML 而言，最大的特点是对页面中可用元素进行了严格的限制，以确保高性能。此外，Google 还对 AMP 页面提供了高速缓存，如果从 Google 搜索中打开 AMP 页面，速度非常快，几乎是秒开。\n\n前几天将我的博客完全迁移到了 AMP，在此也做一个记录。\n\n## 作为单独页面存在的 AMP\n\n在很长一段时间内，我都以为 AMP 只适用于移动端某些特殊场景，至少文章页应该是适用的，于是自然而然就想到了博客应该是非常适合 AMP 的场景。因为我的博客使用的是著名的静态博客程序 [Hexo](https:\u002F\u002Fhexo.io)，所以也就很自然地想到了，会不会有人已经写好了 Hexo 的 AMP 插件？搜了一下，果然没有失望，找到了一个名为 [hexo-generator-amp](https:\u002F\u002Fgithub.com\u002Ftea3\u002Fhexo-generator-amp)的插件。\n\n这个插件不会修改已有的文章页面，而是会为文章页面再生成一个合法的 AMP 页面。这两个页面可以互相引用，表示一个是普通页面，一个是 AMP 页面。这种方式也是 Google 搜索等平台认可的 AMP 生成方式。\n\n按照插件的文档，使用起来还是比较简单的，具体的方式就不写了，直接参考文档即可。有几个值得注意的点：\n\n1. 一定要修改文章页的模板，添加`link[rel=amphtml]`链接，要不然搜索无法找到 AMP 页面\n2. AMP 页面会自动在`head`中添加`canonical`链接回原有的文章页\n\n这个插件支持自己修改模板。鉴于我对它的样式并不是很满意，没有修改的欲望，于是也就没有去改它。如果你需要修改 AMP 模板的话，可以在网址中添加`#development=1`，然后在控制台中查看 AMP 验证结果。\n\n\u003C!-- more -->\n\n最后效果：\n\n![Google搜索显示AMP](\u002Fassets\u002Fmigrate-blog-to-amp\u002F1-google-search.jpeg)\n\n从 Google 搜索的时候能看到明显的“AMP”和闪电标记\n\n![AMP使用Google域名缓存](\u002Fassets\u002Fmigrate-blog-to-amp\u002F1-google-cache.jpeg)\n\n打开后就能看到这个插件提供的默认模板长什么样。\n\n另外可以看到，从搜索中直接点击时，访问的域名是 Google 的，这便是经常被说到的，Google 对 AMP 页面提供了高速缓存。按照 Google 的说法，这种状态下，页面的逻辑仍然会运行，此时页面既是在 Google 那里的，也是在网站作者的控制下的。\n\n![hexo-generator-amp默认样式](\u002Fassets\u002Fmigrate-blog-to-amp\u002F1-footer.jpeg)\n\n这是插件默认模板的 footer，可以说是相当大，而且个人不太喜欢。但不管怎么说，我的博客有 AMP 页面了，并且能在 Google 搜索结果中被标识出来，点击时还能秒开，这确实是一种非常不错的使用体验。\n\n## 全站 AMP\n\n后来我看到了[《澄清对AMP的十个误解》](\u002Farticle\u002Fweb\u002F2017\u002F10-misconceptions-about-amp.html)这篇文章，才知道原来 AMP 既不是移动端的专属应用，也不是能力非常受限的技术。因此萌生了将博客整站都改成 AMP 的想法。\n\n首先，既然全站都要改成 AMP 了，那么为文章页再单独生成一个 AMP 页面就显得不必要了，因此我去掉了上面说的 hexo-generator-amp 插件，而使用完全手工修改的方式来改造。\n\n### 1. 照葫芦画瓢\n\n前面说过，当在地址栏中加上`#development=1`时，浏览器控制台就会出现 AMP 的验证信息。于是我想着那就先打开这个验证信息，然后跟着错误一个一个改吧，于是也就直接访问了`http:\u002F\u002Flocalhost:4000\u002F#development=1`，结果控制台空空如也。通过[查询文档](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Ftutorials\u002Fcreate\u002Fbasic_markup)，才知道原来要打开 AMP 验证，至少还是得做一点前置工作的，最起码你得让浏览器知道“我是打算变成 AMP 的，请按 AMP 的标准来要求我”。于是按照文档一一照做：\n\n- Doctype 是必须要有：无需改动\n- 包含一个顶层的`\u003Chtml ⚡>`或者`\u003Chtml amp>`：打开 Hexo 模板，给`html`加上闪电属性`⚡`\n- 包含`\u003Chead>`和`\u003Cbody>`：无需改动\n- `\u003Chead>`的第一个子节点是`\u003Cmeta charset=\"utf-8\">`：无需改动\n- `\u003Chead>`的第二个子节点是`\u003Cscript async src=\"https:\u002F\u002Fcdn.ampproject.org\u002Fv0.js\">\u003C\u002Fscript>`：照做\n- 在`\u003Chead>`中通过`\u003Clink rel=\"canonical\" href=\"$SOME_URL\">`指向非 AMP 版本的页面，如果只有 AMP 版本则指向自身：照做，指向自身\n    ```html\n    link(rel=\"canonical\",href=url_for(page.path))\n    ```\n- 在`\u003Chead>`中包含`\u003Cmeta name=\"viewport\" content=\"width=device-width,minimum-scale=1,initial-scale=1\">`：无需改动\n- 包含一段 AMP [必须有的代码](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Freference\u002Fspec\u002Famp-boilerplate.html)：照做，注意这里不要对代码进行格式化，按原样一行复制下来就好，否则会验证不通过\n\n做完回头看一下，其实改动并不是很多。然后再次刷新浏览器，就能看到验证信息了：\n\n![AMP验证截图](\u002Fassets\u002Fmigrate-blog-to-amp\u002F2-getting-started.png)\n\n在图上，我们可以看到大概有这样几个问题：\n\n- 使用`link[rel-stylesheet]`加载了一个 CDN 域名上的字体\n- `script` 标签不允许（3次）\n- `img`的`src`属性缺失\n- `img`不允许出现，可能需要`amp-img`\n\n对这些问题一一进行解释和解决。\n\n### 2. 样式表\n\n第一个问题，它说我加载了一个 CDN 域名上的字体，这是怎么回事呢？同样，首先[查看文档](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Freference\u002Fspec#custom-fonts)，发现文档上说，只允许加载以下几个域名的自定义字体：\n\n- https:\u002F\u002Ffast.fonts.net\n- https:\u002F\u002Ffonts.googleapis.com\n- https:\u002F\u002Fmaxcdn.bootstrapcdn.com\n\n但下面也说，在自定义 CSS 中，可以使用`@font-face`来引用字体，这种方式不受域名限制。\n\n我的博客主题确实用到了自定义字体，而且确实是在我自己的 CSS 中定义的，而我的字体托管域名并不是上面那几个域名，所以肯定是无法加载了。于是我将字体的定义从`CSS`中移到了`\u003Chead>`中：\n\n```html\n\u003Cstyle>\n@font-face {\n    font-family: 'sourcesanspro';\n    src: url('\u002F\u002Ftoobug.s.f2er.info\u002Ffont\u002Fsourcesanspro.woff2') format('woff2'),\n         url('\u002F\u002Ftoobug.s.f2er.info\u002Ffont\u002Fsourcesanspro.woff') format('woff');\n    font-weight: normal;\n    font-style: normal;\n}\n\u003C\u002Fstyle>\n```\n\n满心希望地刷新了一下，结果发现这个错误依然存在。在花了三十分钟百思不得其解之后，我终于想到了去翻一下 [AMP 关于样式的说明](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Fguides\u002Fresponsive\u002Fstyle_pages)，文档中明确说：“AMP pages can’t include external stylesheets, with the exception of custom fonts”，即 AMP 页面中不允许加载外部样式，除了自定义字体。\n\n再明确一下，AMP 中不允许使用 CSS 样式表加载样式。唯一可以用 CSS 来加载的只有字体，而用来加载字体的 CSS 必须是上面的三个网址之一。\n\n那么解决方式就简单了，使用`\u003Cstyle amp-custom>`将样式文件内联进来即可，具体到 jade 模板中，只要`include`一下就好：\n\n```\nstyle(amp-custom)\n    include ..\u002F..\u002Fsource\u002Fcss\u002Fapollo.css\n```\n\n### 3. 脚本\n\nAMP 页面中不允许以我们熟悉的方式引入脚本，或者也可以先简单理解为不允许使用脚本。而我的页面上使用了 3 个脚本：\n\n1. 用于将 HTTP 访问跳转到 HTTPS 的脚本\n2. 用于图片 lazyload 的脚本\n3. Google Analytics 统计脚本\n\n第 1 个有一定历史原因，因为最早将博客托管在 Gitlab.com 上，不支持自动 HTTPS 跳转，所以只能使用脚本。现在使用了自己的服务器，可以直接使用301跳转，并支持 HSTS，所以这个脚本直接去掉即可。\n\n第 2 个是自己写的一个简单的图片 lazyload 的脚本，即构建时将`img`的`src`属性换成`data-src`，然后在图片滚动到当前视野中时再加载。因为 AMP 并不支持`img`，且`amp-img`有 lazyload 的特性，所以直接去掉。（关于图片的问题下文详述。）\n\n第 3 个，GA 统计的脚本，AMP 有官方的组件可以支持，通过[查看文档](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Freference\u002Fcomponents\u002Famp-analytics)，只要先引入`amp-analytics`组件脚本，然后将 GA 的代码替换掉就可以解决：\n\n```html\n\u003C!-- 在head区 AMP 脚本之前引入 -->\n\u003Cscript async custom-element=\"amp-analytics\" src=\"https:\u002F\u002Fcdn.ampproject.org\u002Fv0\u002Famp-analytics-0.1.js\">\u003C\u002Fscript>\n\n\u003C!-- 将统计脚本替换成如下代码 -->\n\u003Camp-analytics type=\"googleanalytics\" id=\"UA-XXXXXXXX\">\n    \u003Cscript type=\"application\u002Fjson\">\n    {\n        \"vars\": {\n            \"account\": \"UA-XXXXXXXX\"\n        },\n        \"triggers\": {\n            \"trackPageview\": {\n                \"on\": \"visible\",\n                \"request\": \"pageview\"\n            }\n        }\n    }\n    \u003C\u002Fscript>\n\u003C\u002Famp-analytics>\n```\n\n至此，脚本的问题解决了。\n\n### 4. 图片\n\nAMP 有一个很大的特点，就是强调页面的静态布局。\n\n举个例子，当浏览器加载一张图片时，如果图片没有被显式指定宽高，此时图片的占位大小是不确定的，因此浏览器会先对后面的内容进行排版，等图片加载完之后再回来重新计算图片占的位置，此时就会造成页面布局的变化。\n\n而 AMP 强调页面布局应该是确定的，因此不允许像上面这样的页面布局变化存在。针对图片，AMP 中需要使用`\u003Camp-img>`元素来替代`\u003Cimg>`，并且强制要求一定要指定图片的布局方式和宽高。\n\n首先第一张要处理的图是博客顶部的 Logo，直接在模板中将它修改成`\u003Camp-img>`：\n\n```\na.logo-link(href=url_for())\n    amp-img(layout=\"fixed\",width=\"60\",height=\"60\",src=theme.logo)\n```\n\n这里我使用了`layout=\"fixed\"`，表示这张图片的大小是固定的。\n\n接下来要处理的文章正文中的图片，我利用了 Hexo 主题的勾子（Script）特性。按官方文档的说法，只要在与`source`同级的目录创建一个`scripts`目录，里面的脚本就会被执行。但是我是将`scripts`目录放到了主题的根目录下，同样会被执行。\n\n```javascript\nvar imageSize = require('image-size');\nvar path = require('path');\n\nhexo.extend.filter.register('after_render:html', (source) => {\n    return source.replace(\u002F\u003Cimg src=\"(.+?)\"\u002Fg, function(str, src){\n        var imagePath = path.join(process.cwd(), 'source' , src);\n        var size = imageSize(imagePath);\n        if(!size){\n            size = {\n                width: 800,\n                height: 500\n            };\n        }\n        return `\u003Camp-img layout=\"responsive\" width=\"${size.width}\" height=\"${size.height}\" src=\"\u002F\u002Ftoobug.s.f2er.info${src}\"`;\n    });\n});\n```\n\n这段代码注册了一个勾子，在渲染 HTML 之后执行，所以可以对 HTML 中的`\u003Cimg>`进行替换。\n\n因为`\u003Camp-img>`要求必须指定宽高，因此使用了`image-size`这个 npm 模块来获取图片宽高。最后将`\u003Cimg>`替换成`\u003Camp-img>`即可。因为是文章正文中的图片，`layout`指定了`responsive`，这样就可以在不同宽度下自适应。最后我在替换的时候顺手加了上 CDN 的前缀。\n\n至此，首页就已经改造完成了，刷新一下，看到错误信息已经没有了。\n\n### 5. 评论框\n\n进入文章详情页，仍然会有一个使用了脚本的提示，这是因为引入了 disqus 评论组件。\n\n```javascript\nvar disqus_shortname = '#{theme.disqus}';\nvar disqus_identifier = '#{page.path}';\nvar disqus_title = '#{page.title}';\nvar disqus_url = '#{config.url}\u002F#{page.path}';\n(function() {\n    var dsq = document.createElement('script'); dsq.type = 'text\u002Fjavascript'; dsq.async = true;\n    dsq.src = '\u002F\u002F' + disqus_shortname + '.disqus.com\u002Fembed.js';\n    (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);\n})();\n```\n\n首先动用搜索，看看 disqus 官方是否向 GA 那样有提供官方组件。结论是……没有。但是，找到一篇官方的 AMP 页面下[使用指南](https:\u002F\u002Fgithub.com\u002Fdisqus\u002Fdisqus-install-examples\u002Ftree\u002Fmaster\u002Fgoogle-amp)。\n\n事实上，这篇指南写得并不是十分清楚，并且照做的话会有一些错误信息，也不能正常显示和发布评论。在尝试好几次并做了反复修改之后，才终于弄懂它的含义。它的大致原理是使用`amp-iframe`组件，将评论放在一个独立的 iframe 中去。这是利用了 AMP 的规则，虽然 AMP 页面不允许有脚本存在，但是可以通过`amp-iframe`来包含页面，将脚本放到这个单独的页面中即可。\n\n第一步，我们要准备一下这个被嵌入的页面，它将包含主要的 disqus 相关的代码：\n\n```html\n\u003C!-- 用于显示评论框和评论列表的容器 -->\n\u003Cdiv id=\"disqus_thread\">\u003C\u002Fdiv>\n\u003Cscript>\n\u002F\u002F 监听disqus组件传递的消息\n\u002F\u002F 其中有一个是`resize`，是disqus用于告诉当前页面\n\u002F\u002F “我加载完了，我的尺寸是XXX”\nwindow.addEventListener('message', receiveMessage, false);\nfunction receiveMessage(event)\n{\n    if (event.data) {\n        var msg;\n        try {\n            msg = JSON.parse(event.data);\n        } catch (err) {\n            \u002F\u002F Do nothing\n        }\n        if (!msg)\n            return false;\n\n        if (msg.name === 'resize') {\n            \u002F\u002F 向 AMP 页面发送消息，要求重新设置 amp-iframe的高度\n            window.parent.postMessage({\n              sentinel: 'amp',\n              type: 'embed-size',\n              height: msg.data.height\n            }, '*');\n        }\n    }\n}\n\u003C\u002Fscript>\n\u003Cscript>\n    \u002F\u002F 用于解析url参数\n    function getQueryVariable(variable) {\n        var query = window.location.search.substring(1);\n        var vars = query.split(\"&\");\n        for (var i=0;i\u003Cvars.length;i++) {\n            var pair = vars[i].split(\"=\");\n            if(pair[0] == variable){return pair[1];}\n        }\n        return(false);\n    }\n    \u002F\u002F 通过url参数获取当前 AMP 页面对应的文章相关参数\n    \u002F\u002F 并赋值给page变量，稍后disqus将读取page变量\n    var disqus_config = function () {\n        this.page.title = decodeURIComponent(getQueryVariable(\"title\"));\n        this.page.url = decodeURIComponent(getQueryVariable(\"url\"));\n        this.page.identifier = decodeURIComponent(getQueryVariable(\"identifier\"));\n    };\n\n    \u002F\u002F 引入disqus脚本\n    (function() {  \u002F\u002F DON'T EDIT BELOW THIS LINE\n        var d = document, s = d.createElement('script');\n\n        s.src = '\u002F\u002Ftoobug.disqus.com\u002Fembed.js';\n\n        s.setAttribute('data-timestamp', +new Date());\n        (d.head || d.body).appendChild(s);\n    })();\n\u003C\u002Fscript>\n```\n\n我已经在代码中标上了注释，这里面有几个关键点需要理解：\n\n1. 这个页面将会被`amp-iframe`引用，因此 AMP 页面是父页面，本页面是指上面的这段代码所在页面\n2. disqus 的脚本在加载后，会将一个新的 iframe 写到容器`#disqus_thread`中\n3. disqus 会从新的 iframe 中发消息，告知本页面自己的状态，其中一种消息是尺寸\n4. disqus 需要知道 AMP 页面对应的 url、标题等信息，我们通过本页面的`location.search`获取\n5. `amp-iframe` 可以接受本页面的消息，动态设置高度\n\n第二步，需要将这个页面放到一个单独的域名上，不能和 AMP 页面同一个域名。刚好我使用了一个 CDN ，有独立的域名，因此下面就直接用 CDN 的域名进行引入。\n\n第三步，使用`amp-iframe`将这个页面引入，并且在 url 中传入文章的相关参数：\n\n```html\n\u003Camp-iframe\n    width=\"600\"\n    height=\"140\"\n    layout=\"responsive\"\n    sandbox=\"allow-scripts allow-same-origin allow-modals allow-popups allow-forms\"\n    resizable\n    src=\"https:\u002F\u002Ftoobug.s.f2er.info\u002Famp\u002Fdisqus\u002Ftoobug.html?title=#{page.title}&url=#{config.url}\u002F#{page.path}&identifier=#{page.path}\">\n    \u003Cdiv\n        overflow\n        tabindex=0\n        role=button\n        aria-label=\"Disqus Comments\"\n    >Disqus Comments\u003C\u002Fdiv>\n\u003C\u002Famp-iframe>\n```\n\n值得注意的点：\n\n1. `layout`写`responsive`以便自适应宽度\n2. `sandbox`需要写明权限，否则可能导致评论无法操作\n3. 要有`resizable`属性，这样会接受 iframe 中页面的消息，重新计算高度\n4. 要有`div[overflow]`子元素，否则会有报错“Overflow element must be defined for resizable frames”\n\n这样就完成了 disqus 评论的改造。\n\n![amp-iframe加载中](\u002Fassets\u002Fmigrate-blog-to-amp\u002F3-comments-1.jpeg)\n\n图：amp-iframe加载中\n\n![disqus加载中](\u002Fassets\u002Fmigrate-blog-to-amp\u002F3-comments-2.jpeg)\n\n图：disqus加载中\n\n![disqus评论](\u002Fassets\u002Fmigrate-blog-to-amp\u002F3-comments-3.jpeg)\n\n图：disqus评论正常显示\n\n回顾一下完整的原理：\n\n- 使用`amp-iframe`来包含评论的逻辑\n- 接受 disqus 的消息，如果是高度变更了，那么向 AMP 页面发送一个消息，要求重新计算高度\n- 通过`amp-iframe`的`src`参数来指定`disqus`相关的参数\n\n> 改造完之后发现一个疑似 AMP 的 bug：在移动端 Chrome 上访问时，评论框显示不完整，也就是说，高度调整并没有完成。按照文档中的说法，说这个高度调整不一定是立即完成，AMP 会判断需要调整的时候再做调整。估计是这个判断什么时候进行调整有 bug 。如果选中文字往下拖到底，则有一定机率高度会变正常。目前尚未找到更好的解决方案。\n\n## 结\n\n以上就是本博客的改造过程。将博客改造成全站 AMP 并不是很困难。一方面是因为基本上没有什么逻辑，另一方面以文章为主的站点非常符合 AMP 的定位。\n\n如果你的站点逻辑非常多，或者并不是以文章、资讯为主的站点，可能还需要再考量一下是否有必要。\n\n最后再附一张改造之后的 Google AMP 缓存访问的图：\n\n![Google AMP 缓存](\u002Fassets\u002Fmigrate-blog-to-amp\u002F4-google-cache-final.jpeg)\n\n比`hexo-generator-amp`生成的样式顺眼多啦。\n","\u002Farticle\u002Fmigrate-blog-to-amp.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fmigrate-blog-to-amp\u002F1-google-search.jpeg",{"id":31,"type":7,"slug":32,"title":33,"date":34,"category":11,"tags":35,"body_markdown":39,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":40},173,"mini-app-of-xiaomi","小米也有小程序？热闹了","2017-08-29 09:17",[36,37,38],"小程序","微信","小米","上上周五（18号）支付宝小程序在经历了大半年的传闻之后，终于揭开了神秘面纱，开始公测。和之前传闻的一样，支付宝小程序的核心能力的 API 与微信小程序几乎完全一样。\n\n至此，支付宝勉强算挤进“小程序俱乐部”，而小程序也不再是微信小程序的代名词。\n\n## 小程序俱乐部\n\n在之前的推文[【闲聊】大前端揭秘](\u002Farticle\u002Fweb\u002F2017\u002Fsecrets-of-wild-frontend.html)中，有表述过 native 终端和 web 端融合的趋势。大体上来看，主要的方向包括 native 动态化（React-Native 为代表）、web native 化（以 AMP、PWA 为代表）以及“小程序”。\n\n长期以来，在移动端被诟病的有两件事情：\n\n- web 不管是能力、性能还是体验都太差\n- native 体积大，发版更新困难\n\n在大前端融合的大背景下，实现一种既可以充分利用 web 快速轻量的特性，又能够达成 native 的性能和体验的技术方案可谓是众望所归。正是在这种愿景下，诞生了小程序。\n\n从这个角度来看的话，小程序的诞生可能还不能简单归于微信的高瞻远瞩，而更可能是行业发展到了一个路口，大家都发现要走一些新的路了，只是微信提前选定了小程序这样一条路，然后花了一年半的时间把它一脚一脚踩出来。\n\n那么支付宝要做小程序也算是水到渠成了。既有行业大背景，又有前言的领路人。总之小程序俱乐部现在不止是微信一个人玩了。\n\n\u003C!-- more -->\n\n## 小米 MIUI 也要加入\n\n其实这才是本文的重点，MIUI 也要做小程序。\n\n如果这篇文章提前两个月写好发出来，那么还可以说是 MIUI 要做，事实上到现在这个时间点，小米已经放出了一部分 MIUI 小程序，官方的名称似乎是叫作秒开应用，只是还没有正式对开发者开放。\n\n从技术上来说，微信、支付宝、MIUI 三者的小程序落地方案各不相同，微信主要靠自研框架以 web 技术落地为主，支付宝以 React 来落地（React-web还是React-Native并不确定），MIUI 则是完全落地到 native UI 上。\n\n![MIUI应用市场搜索秒开](\u002Fassets\u002Fweb\u002F2017\u002Fmini-app-of-xiaomi\u002F01.jpg)\n\n图：MIUI应用市场搜索秒开\n\n## 小程序的宿主\n\n小程序的体验如何，很大程度上会取决于它的宿主。一个好的宿主可以给小程序带来非常大的优势。\n\n对于做技术的人来说，可能最容易理解的首先是生命周期上的优势。简单点说就是你的小程序什么时候被启动，什么时候被暂停，什么时候被杀掉，什么时候能跑在后台。\n\n如果你做过 native 开发的话，会发现这些问题本身也是做一个 App 需要关注的问题。作为一个 App ，自身都可能随时被杀掉，却还要关注自己的子程序是否被杀掉，其实有点勉为其难。事实上在使用微信小程序的过程中也时常会碰到小程序被意外杀掉的情况，这很有可能是一个无法彻底解决的难点。\n\n而如果小程序的宿主变成了操作系统，生命周期的管理就变得非常自然了，和 native App 完全一样。\n\n同样的问题也出现在权限或者能力上。举一个简单的例子，一个微信小程序如果要修改锁屏界面，那么首先需要微信有修改锁屏界面的能力和权限，同时还需要微信赋予小程序能力和权限。这并不是一件容易的事情，甚至有一些事情是不可实现的，例如修改解锁方式。但如果小程序的宿主是操作系统，那么事情又会简单很多。\n\n除了上面两者之外，宿主还能直接决定的一件事情，就是入口。\n\n目前微信已经在各个功能中为小程序留下了入口，以便为它引流。但是这仍然不能解决一件非常尴尬的事情：入口太深。想想打开一个常用的小程序，需要先打开微信，然后点发现tab，然后点小程序，然后在列表中点开，这个过程是无比繁琐的。所以即便是只有 Android 才支持将小程序放到桌面，微信也义无反顾地进行了支持，因为除此之外确实很难有一个更方便的入口。\n\n而如果小程序的宿主变成了操作系统，就是另一番风景了。别说桌面图标了，就是状态栏、锁屏、设置甚至通讯录都能轻易成为小程序的入口。更别说操作系统还可以支持更多场景化的入口：连上公司 Wifi 自动打开打卡小程序，回到家自动打开空调控制小程序，拍完照打开美图小程序等等。\n\n总结下来，会发现 MIUI 做小程序在技术上是有巨大的优势的，无论是技术实现上还是入口引流上都能做得更好，用户用起来也会觉得更自然。\n\n## 超级 App 与系统之争\n\n留意科技新闻的同学可能前一阵有注意到一条新闻：苹果手机在华销量下滑，最大的原因居然是微信。手机和微信一个是平台，一个是 App，这两个看似是合作关系，怎么会有竞争性呢？\n\n事实上微信早已经不是一般意义上的 App。从功能上来说，它含有通讯、阅读、社交、支付等主要功能，而这些刚好对应 iMessage \u002F iBooks \u002F Apply Pay等功能。或者换一个说法，苹果所提供的独特服务，在中国有很大部分被微信所承载，对用户来说，使用苹果手机或者别的手机，只要是能用微信，那么就能得到几乎完全一样的体验。这样就极大的削弱了苹果手机的差异性，从而客观上减弱了市场竞争力。\n\n具体到小程序这件事上，也有相似的问题。如果我们假设用户会更信赖微信，而不是更信赖操作系统，假设用户使用手机的主要界面就是微信，那么从微信中打开小程序和从操作系统中打开小程序哪个更自然也许是一件不好评判的事情。或者换一个说法，当微信变成操作系统上的另一个操作系统时，它们就有了竞争关系。\n\n事实上，现在已经有了这个趋势。例如微信小程序一直推崇的二维码入口即是如此。当我们看到一个二维码，第一反应会是打开微信扫一扫，而不是找系统相机或者系统浏览器来扫描。这样二维码就成了微信这一层的专属入口。当这样的入口越来越多时，操作系统的入口优势也许就不存在了。\n\n回到技术上，我们说操作系统相比微信有天然的优势。但是当微信的体量足够大的时候，操作系统同样必须给予它足够的权限，并保证它的运行是顺畅的，否则用户会认为手机有问题，从而责怪操作系统厂商。以 Android 系统最典型的后台运行为例，现在没有哪个厂商是敢主动杀掉微信的进程的，既然如此，微信也就有一定信心能保证小程序的运行。从这个角度来说，超级 App 作为小程序的宿主也并不一定有非常大的劣势。\n\n除了这些之外，超级 App 作为小程序宿主相比操作系统还会有一个优势，就是市场推广。让一个用户安装微信比让一个用户安装 MIUI 的代价可能要小100倍。\n\n结论：微信小程序相比 MIUI 小程序在技术和入口上的劣势不一定非常大，反而在市场推广上有巨大优势。\n\n## 目的\n\n微信推出小程序，目的显然不在于建立技术规范或者引领技术潮流，否则不会对支付宝几乎全盘照抄这件事情如此淡定。事实上小程序的推出是有非常重的产品发展痕迹的。一方面，公众号取得巨大的成功，另一方面，公众号的用户体验却相当差。在前述技术背景下，小程序就这么被微信弄出来了。\n\n它其实承载的是微信原来对服务号的期望，即微信提供信息呈现、交互、支付等功能，商家在微信中做好服务。所以也可以说小程序其实是为 O2O 而生的。这也是小程序对线上流量如此克制，却大力发展线下流量的原因。当这一套东西跑顺之后，微信实际上就拥有了电商、电商服务、支付业务的话语权，这正是微信商业变现的愿景。\n\nMIUI 推出小程序的目的却有点让人摸不着头脑。从目前的产品形态来看，它跟微信打的算盘显然是不同的。那么猜想它的出发点可能有如下几种：\n\n发现小程序的风口，先行投入建立技术规范，掌握话语权\n为 MIUI 添加增值服务，使它不止成为一个操作系统，而是同时具备商业能力\n\n不管是哪一种，都和微信的初衷是不一样的。目的的不同会导致产品和运营的方式有巨大的不同。\n\n## MIUI 小程序的不确定性\n\n因为 MIUI 的推广跟微信相比有天然的劣势，因此产品策略上的选择就显得非常重要了。一种玩法是采用去中心化的策略，小米提供小程序技术框架，然后推动其它操作产家按这个框架自己实现。另一种则是中心化的策略，小米也和微信一样，提供统一的市场、入口并制定开发工具、审核标准。\n\n这两种玩法各有利弊。前者可能可以推得更广一些，但是各个操作系统厂家不一定都有动力去做落地。即使是有落地意向，也可能在具体落地的时候采用一些不同的技术方案。后者则可以让小米从这个小程序中获取更多的商业利益，但同时也限制了它的生存空间。\n\n具体小米会怎么样去经营这样一个小程序，拭目以待。\n\n## 有端就有兼容性问题\n\n对前端而言，小程序俱乐部一下子出现了三个玩家，可能意味着社区会再次分裂，并且这一次，没有 W3C 组织来一统天下。可以预见的是，小程序的兼容性问题会逐渐成为前端领域一个新的问题。然而从另一方面来说，这也正是前端工程师核心价值中的一部分：在不同的端中提供良好的一致的用户体验。不管怎么样，未来已经变得更好玩了。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fmini-app-of-xiaomi\u002F01.jpg",{"id":42,"type":7,"slug":43,"title":44,"date":45,"category":11,"tags":46,"body_markdown":47,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":48},167,"10-misconceptions-about-amp","【译】澄清对AMP的十个误解","2017-08-23 10:30",[26],"\n## 1. AMP是一个新的渲染引擎\u002F编程语言\n\nAMP 是一套开源的 web 组件格式和类库。与其它类库或者框架相比，AMP 最大的区别在于，它采用了白名单策略，来约定你可以做什么。\n\n为什么要限制一些东西的使用呢？这是因为一些看起来很无辜的小代码很容易拖慢网站的性能。而且一段时间后回来排查这些性能问题会是一件非常困难的事情。这就像你在德国的 Autobahn 快速路上开车，却只在右道行走，不知道最左边的道路才是更快的。AMP 就是这样一种技术，强迫你走到最左边的快速道路，并且保证你前方的道路是没有障碍的。\n\nAMP 带来的并不只有限制，它还提供了很多的自定义标签，这些标签都有各自内置的功能。当你使用这些自定义标签，并遵守一些其它的规则，那么 AMP 将通过一些手段保证你的网站速度是非常快的。这些手段主要包括强制静态布局、高效率资源加载和一些其它的优化。\n\nAMP 有一份文档，规定了什么样的标签是兼容的，什么样的标签是不兼容的。它还发布了一个内置的验证工具，可以让你看到当前页面的是否符合 AMP 文档的要求。需要强调的是，从技术上来说，即使不遵守所有的规定，AMP 页面也能运行得很好，只是你的页面无法通过 AMP 验证（从性能上来说，不遵守 AMP 规定到一定程度的时候，AMP做的性能改进也会全部失效，另外如果有一些东西是要求与 AMP 页面协作的，那么你的页面可能无法正常显示 ）。但同时，这也意味着 AMP 所强调的一些特性全部没有了。\n\n\u003C!-- more -->\n\n### 还有更多解释吗？\n\n真的没有了。AMP 只是一套 web 组件生态系统而已。但是，因为可以很容易通过编程的方式来确定一个页面是合法的 AMP，就可以做更多炫酷的事情了。比如：\n\n- 合法的 AMP 可以使用免费、高速的缓存（例如[Google AMP Cache](https:\u002F\u002Fdevelopers.google.com\u002Famp\u002Fcache\u002F)）\n- 基本可以确认合法的 AMP 页面速度很快，且对用户友好\n- AMP 页面是“自包含”（self-contained）的（译注：指页面是完整、独立的），所以可以被嵌入第三方平台\n\n这也允许第三方平台做一些很炫酷的事情：\n\n- 出现在 Google 搜索的 Top Stories 轮播上\n- 从 Pinterest 上链接到 AMP 页面\n- 在 PWA 中使用 AMP 页面\n\n## 2. AMP是Google的项目\n\nAMP 最早是由出版行业和 Google 在2015年提出来的（当然，一些促使 AMP 诞生的体验问题，比如移动端 web 页面加载慢等，属于明显的行业内共性问题）。从一开始，它就是由出版行业、广告行业、技术提供者和平台提供方一起携手开发的，除了 Google 以外，参与者还包括 Twitter、Linkedin 和 Pinterest 。AMP 从提出来的第一天起就是一个通过 Github 进行开放协作的开源项目。到现在为止，AMP 接受了来自超过 200 名贡献者的 Pull Request，这些贡献者绝大部分不是 Google 的员工。\n\nGoogle 确实有一支团队在全职为 AMP 项目工作，AMP 项目的大部分贡献也来自这个团队，但这个团队也是通过和其它人一样的[Intent to implement](https:\u002F\u002Fgithub.com\u002Fampproject\u002Famphtml\u002Fblob\u002Fmaster\u002FCONTRIBUTING.md#contributing-features)流程来工作。Google 团队也会将它们的[周会纪要](https:\u002F\u002Fgithub.com\u002Fampproject\u002Famphtml\u002Fissues?utf8=%E2%9C%93&q=label%3A%22Meeting%20Notes%22%20)以及其它的文档发布出来，尽量保证外部贡献者都可以参与进来。\n\n> 10月17日更新：针对这一点，外界有一些疑问和评论。上面这两段话仍然有效，但是我补充一个更精简的结论：AMP 项目当前的核心贡献者都是 Google 员工，所以 AMP 可以称作是 Google 领导（Google-led）的项目。但是它是被当作一个独立的开源项目来看待的，我们正在邀请开发者和社区参与进来一起贡献，让他们也变成核心贡献者，使 AMP 项目更加独立。\n\n## 3. AMP需要Chrome才能运行\n\n绝对不是这样！AMP 是一个跨平台、跨浏览器的类库，支持所有流行的移动浏览器和桌面浏览器的[最新两个版本](https:\u002F\u002Fwww.ampproject.org\u002Flearn\u002Fbrowsers\u002F):\n\n![AMP可以运行的浏览器](\u002Fassets\u002Fweb\u002F2017\u002F10-misconceptions-about-amp\u002F1.png)\n\n## 4. AMP 限制了我的布局和设计\n\n你肯定会被 AMP 能做的事情惊讶到。AMP 确实限制了一些标签和对性能影响很大的 CSS 属性的使用，但是整体来看，在为站点编写样式时，受到的[限制非常小](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Fguides\u002Fresponsive\u002Fstyle_pages)。想写一个疯狂的 5 层 flexbox 嵌套布局？那就写吧。想基于伪元素写一个疯狂的 UI ？也 OK。\n\n下面是一个我写的[AMP发展计划页面](https:\u002F\u002Fpaulbakaus.com\u002Ftutorials\u002Fcss\u002Fflexbox-freebie-auto-growing-list-for-amp-roadmap\u002F)：\n\n\u003Ciframe height='265' scrolling='no' title='Flexy Steppy List' src='\u002F\u002Fcodepen.io\u002Fpbakaus\u002Fembed\u002FezOQYa\u002F?height=265&theme-id=0&default-tab=result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen \u003Ca href='https:\u002F\u002Fcodepen.io\u002Fpbakaus\u002Fpen\u002FezOQYa\u002F'>Flexy Steppy List\u003C\u002Fa> by Paul Bakaus (\u003Ca href='https:\u002F\u002Fcodepen.io\u002Fpbakaus'>@pbakaus\u003C\u002Fa>) on \u003Ca href='https:\u002F\u002Fcodepen.io'>CodePen\u003C\u002Fa>.\n\u003C\u002Fiframe>\n\n## 5. AMP只适合轻量级页面\n\n有几分道理，但也有误导性。这主要取决于你如何理解“轻量”。严格来说，AMP 的目标是静态内容。但我们所说的静态内容同样可以包含具有艺术气息的动画、侧边栏、灯箱广告、手风琴导航、轮播等等。你可以查看[AMPByExample](https:\u002F\u002Fampbyexample.com\u002F)中的一些[高级例子](https:\u002F\u002Fampbyexample.com\u002F#advanced)。\n\n## 6. AMP只适用于移动端\n\n诚然，AMP（Accelerated Mobile Pages）中的“Mobile”无助于澄清这个问题，但是这个说法还是跟事实完全不符。\n\nAMP 是一个非常强大的跨平台解决方案，它希望出版行业和开发者将工程资源从细碎的多平台兼容支持中解放出来，将焦点放到创建伟大的新产品特性上，而这些产品特性可以被任何设备上的所有用户轻易访问到。\n\nAMP本身是在[响应式设计](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Fguides\u002Fresponsive_amp)的概念支持下被创造出来的。目前有与 AMP 集成的平台大部分是聚焦移动端的，但是在桌面端，你也可以从 AMP 中获取得很多好处。\n\n想知道如何使用 AMP 来处理不同分辨率和不同设备的话，可以看我的另一篇文章[“AMP中的'mobile'”](https:\u002F\u002Fpaulbakaus.com\u002F2016\u002F07\u002F01\u002Fabout-that-mobile-in-accelerated-mobile-pages\u002F)。\n\n## 7. 我现有的网站上无法使用AMP\n\n我们已经澄清过第 4 点，并没有什么特别的理由让你现在的网站无法使用 AMP，因为当你读完第一个问题后，就知道了 AMP 只是一个 web 组件类库而已。事实上，[AMP项目主页](https:\u002F\u002Fwww.ampproject.org\u002F)就是完全使用的 AMP：\n\n![AMP项目主页使用的AMP](\u002Fassets\u002Fweb\u002F2017\u002F10-misconceptions-about-amp\u002F2.png)\n\n当然，和其它类库一样，[AMP并不适合每一个人](https:\u002F\u002Fpaulbakaus.com\u002F2016\u002F02\u002F26\u002Flife-after-amp\u002F)。在动手前想一想在[AMP的强制限制](https:\u002F\u002Fwww.ampproject.org\u002Flearn\u002Fhow-amp-works\u002F)（同时也带来好处）下，你的网站是否能正常运行。如果答案是肯定的，那么就切换到 AMP 吧。有一个基本的原则，如果你的网站没有静态内容，并且页面并不是最深层次的页面（译注：原文 leaf pages，leaf 指树状结构中的叶子节点，对应到网站一般指最深层次的页面，例如文章页），例如入口页，也就是用户从搜索中点进来的页面，那么 AMP 可能不适合你。\n\n## 8. 如果我自己做优化，那AMP就没什么用\n\nAMP 的优化是“无脑优化”，即使你身边没有web开发大师，它也能帮助你。我们对将网站性能优化到极致这件事情感到自信和骄傲。事实上，因为 AMP 是一个通用库，它可能会漏掉一些针对你的网站特殊场景下的优化策略，这意味着你自己的手工优化工作很可能会带来更好的性能。\n\n但到今天为止，浏览器和一些大的平台例如 Google 搜索，仍然没有办法来确认你的网站是非常快速且对用户友好的。所以如果你选择自己做优化工作，你可能能得到一个非常快的网站，但是没有办法让其它人确信。而 AMP 的验证使得它对于第三方平台非常有吸引力。\n\n## 9. AMP只对出版发行行业有好处\n\n没错，如果你将你的新闻站点变成 AMP，就有机会出现在 Google 的 Top Stories 轮播上，并且 Google 会在移动端[搜索结果](https:\u002F\u002Fsearch.googleblog.com\u002F2016\u002F09\u002Fsearch-results-are-officially-ampd.html)中使用一个内联的查看器来加速 AMP 页面。但是[eBay也创建了AMP页面](http:\u002F\u002Fwww.ebaytechblog.com\u002F2016\u002F06\u002F30\u002Fbrowse-ebay-with-style-and-speed\u002F)（[示例](http:\u002F\u002Fm.ebay.com\u002Fsch\u002Famp\u002F16GB-iPhone-5s-Smartphones\u002F9355\u002Fbn_341667\u002Fi.html)），尽管它们并不是新闻网站。\n\n![AMP可以运行的浏览器](\u002Fassets\u002Fweb\u002F2017\u002F10-misconceptions-about-amp\u002F3.png)\n\n为什么 eBay 要选择AMP？[它们自己是这么说的](http:\u002F\u002Fwww.ebaytechblog.com\u002F2016\u002F06\u002F30\u002Fbrowse-ebay-with-style-and-speed\u002F)：\n\n> AMP 的好处之一在于，它是一套构建移动端 web 页面的最佳实践的集合。我们之前已经在遵守这些最佳实践中的一部分，但是这些不同的做法分散在各个团队中，而且每个团队都有自己的偏好。AMP 的出现让我们可以更好地整理加固这个最佳实践的清单，并将它们作为常规开发周期中的一部分。\n\n## 10. 我得在AMP和PWA中做出选择\n\nAMP 和 PWA 是互补的技术，它们的使用场景完全不一样。如果将它们结合在一起使用，你就能使用它们创建出我认为目前最完美的内容站点：\n\n1. 用户发现了你的内容的链接，点进来了\n2. 内容被瞬间加载完毕，并且看起来很舒服\n3. 阅读完之后，用户被邀请阅读更多内容，或者邀请用户使用一个更好体验的版本，它们由快速导航、通知推送、离线支持等技术支持\n4. 当用户接受了你的邀请后，他们将被引导到一个可以安装到桌面的版本，这个版本的使用体验就像 App 一样\n\n听起来非常棒对吧？你需要做的只是下面这些（或许有稍许变化）：\n\n- 最深层面的页面（有内容的页面，而不是概览页面）使用 AMP 发布，以获得瞬间加载的体验\n- 当用户浏览你的内容的时候，在这些AMP页面中使用[&lt;amp-install-serviceworker&gt;](https:\u002F\u002Fwww.ampproject.org\u002Fdocs\u002Freference\u002Fextended\u002Famp-install-serviceworker.html)初始化缓存和 PWA 应用外壳（PWA app shell）\n- 当用户点击网站上的其它链接的时候（例如，在类似 App 的体验中，点击底部的按钮），[Service Worker](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FService_Worker_API\u002FUsing_Service_Workers)接管请求，然后加载[PWA应用外壳](https:\u002F\u002Fdevelopers.google.com\u002Fweb\u002Fupdates\u002F2015\u002F11\u002Fapp-shell?hl=en)\n- 最后，已经加载好的PWA应用外壳可以将 AMP 作为数据源嵌入到页面，这将使得你只需要为相同内容创建一个后台（既可以作为单独的AMP浏览，也可以作为 PWA 的数据源）\n\n如果同时谈到 AMP 和 PWA 的话，还有更多话题可以说，所以请期待在这个主题上的后续深度文章吧。\n\n## 总结\n\n现在你有答案了。针对 10 个误解，我们给了 10 个澄清的答案，希望能给你一个对 AMP 更大更清晰的印象，也让你想清楚 AMP 对你来说是否适合。还有问题吗？[联系我吧](https:\u002F\u002Ftwitter.com\u002Fpbakaus)！\n\n原文链接\u003Chttps:\u002F\u002Fpaulbakaus.com\u002F2016\u002F10\u002F13\u002Fdebunked-10-misconceptions-about-amp\u002F>\n\n作者：Paul Bakaus (@pbakaus)\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002F10-misconceptions-about-amp\u002F1.png",{"id":50,"type":7,"slug":51,"title":52,"date":53,"category":11,"tags":54,"body_markdown":58,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},175,"passing-data-between-promise-callbacks","【译】在Promise回调之间传值的方法","2017-08-22 13:00:00",[55,56,57],"JavaScript","Promise","异步","\n在基于 Promise 编写的代码中，经常会有很多回调函数，它们都有各自的变量作用域。那么如果我们需要在这些回调函数之间共享数据，要怎么办呢？本文总结了一些方法。\n\n## 1. 问题\n\n下面的代码演示了使用 Promise 回调时经常碰到的一类问题：变量`connection`（A）在一个作用域中存在，但是需要被另一个作用域访问（B和C）：\n\n```javascript\ndb.open()\n.then(connection => { \u002F\u002F (A)\n    return connection.select({ name: 'Jane' });\n})\n.then(result => {\n    \u002F\u002F Process result\n    \u002F\u002F Use `connection` to make more queries (B)\n})\n···\n.catch(error => {\n    \u002F\u002F handle errors\n})\n.finally(() => {\n    connection.close(); \u002F\u002F (C)\n});\n```\n\n在这段代码中，我们使用了 ES 规范中的`Promise.prototype.finally()`。它提供了和`try`语句的`finally`分支类似的功能。\n\n\u003C!-- more -->\n\n## 2. 解决方法：副作用\n\n第一种解决方法是将要共享的值`connection`存入这些回调函数的上级作用域（A）：\n\n```javascript\nlet connection; \u002F\u002F (A)\ndb.open()\n.then(conn => {\n    connection = conn;\n    return connection.select({ name: 'Jane' });\n})\n.then(result => {\n    \u002F\u002F Process result\n    \u002F\u002F Use `connection` to make more queries (B)\n})\n···\n.catch(error => {\n    \u002F\u002F handle errors\n})\n.finally(() => {\n    connection.close(); \u002F\u002F (C)\n});\n```\n\n因为`connection`的定义在回调函数的外面，所以 B 和 C 都能访问它。\n\n## 3. 解决方法：嵌套作用域\n\n上面例子的同步版本，看起来是这样的：\n\n```javascript\ntry {\n    const connection = await db.open();\n    const result = await connection.select({ name: 'Jane' });\n    ···\n} catch (error) {\n    \u002F\u002F handle errors\n} finally {\n    connection.close();\n}\n```\n\n同步版本的代码中，使得`connection`在函数内部可用的方法是将声明提前到上级作用域中：\n\n```javascript\nconst connection = await db.open();\ntry {\n    const result = await connection.select({ name: 'Jane' });\n    ···\n} catch (error) {\n    \u002F\u002F handle errors\n} finally {\n    connection.close();\n}\n```\n\n> 译注：`try...catch`中，`catch`和`finally`可以共享`try`中的变量，所以此处将`connection`移到外部定义，对于同步代码来说，是非必需的。\n\n我们可以在 Promise 中做同样的事情——将 Promise 链起来：\n\n```javascript\ndb.open() \u002F\u002F (A)\n.then(connection => { \u002F\u002F (B)\n    return connection.select({ name: 'Jane' }) \u002F\u002F (C)\n    .then(result => {\n        \u002F\u002F Process result\n        \u002F\u002F Use `connection` to make more queries\n    })\n    ···\n    .catch(error => {\n        \u002F\u002F handle errors\n    })\n    .finally(() => {\n        connection.close();\n    });\n})\n```\n\n这段代码有两个 Promise 链：\n\n- 第一个开始于 A ，`connection`是`db.open()`的结果\n- 第二个被包裹在 B 处的`.then()`中，从 C 处开始，注意 C 处的`return`将两个 Promise 连接起来了\n\n你可能已经注意到了，不管是同步版本还是异步版本的代码，如果`db.open()`同步抛出一个错误，这个错误将不能被`catch`处理。有[一篇专门的关于 `Promise.try()`](http:\u002F\u002F2ality.com\u002F2017\u002F08\u002Fpromise-try.html)将演示在异步版本中如何修复这个问题。在同步版本的代码中，你可以将`db.open()`移入`try`中即可。\n\n## 4. 解决方法：返回多值\n\n下面将演示另一种在回调函数之间传值的方法。但是它不是任何时候都能工作，尤其是你不能将它用于前面演示的数据库操作中。我们来看一个它能工作的例子。\n\n我们面临一个相似的问题：在 Promise 链中，需要将`intermediate`的值从 A 处的回调传递到 B 处的回调。\n\n```javascript\nreturn asyncFunc1()\n.then(result1 => { \u002F\u002F (A)\n    const intermediate = ···;\n    return asyncFunc2();\n})\n.then(result2 => { \u002F\u002F (B)\n    console.log(intermediate);\n    ···\n});\n```\n\n我们使用`Promise.all()`从第一个回调函数中传递多个值给第二个回调函数，从而解决这个问题：\n\n```javascript\nreturn asyncFunc1()\n.then(result1 => {\n    const intermediate = ···;\n    return Promise.all([asyncFunc2(), intermediate]); \u002F\u002F (A)\n})\n.then(([result2, intermediate]) => {\n    console.log(intermediate);\n    ···\n});\n```\n\n注意，在 A 处返回一个数组是不行的，因为`.then()`会获得一个Promise，一个值。使用`Promise.all()`时，内部会使用`Promise.resolve()`来保证数组元素都是 Promise ，并且在它们全部被满足（fulfill）时，将它们的值组成一个数组传递给作为下一个回调的参数。\n\n这种方法的局限性在于你不能传值到`.catch()`或者`.finally()`回调中。\n\n最后，这种方法还可以在`Promise.all()`中传入一个对象（不仅仅可以是数组），这样每一个返回的值都有一个标签。\n\n> 译注：`Promise.all()`目前并不支持传入对象，作者应该是希望支持这样一种使用方式。\n\n## 5. 相关链接\n\n- 在 Exploring ES6 的\"[Promises for asynchronous programming](http:\u002F\u002Fexploringjs.com\u002Fes6\u002Fch_promises.html)\"章节，有关于 Promise 链的更多内容\n- [ES提案：`Promise.prototype.finally()`](http:\u002F\u002F2ality.com\u002F2017\u002F07\u002Fpromise-prototype-finally.html)\n- [ES提案：`Promise.try()`](http:\u002F\u002F2ality.com\u002F2017\u002F08\u002Fpromise-try.html)\n\n作者：Dr. Axel Rauschmayer\n\n原文链接：\u003Chttp:\u002F\u002F2ality.com\u002F2017\u002F08\u002Fpromise-callback-data-flow.html>\n","",{"id":61,"type":7,"slug":62,"title":63,"date":64,"category":11,"tags":65,"body_markdown":66,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":67},177,"the-boundary-of-mvvm","思考MVVM的边界","2017-07-11 13:27",[],"\n## 前提\n\nMVVM 的概念由来已久，一开始，伴随着非常多的争议，MVC 还是 MVP 还是 MVVM ？MVVM 中什么是 VM ？不过随着时间的推移，大家也不再纠结到底是 MV 什么鬼了，于是也有人叫它们为 MV* 。为行文方便，下文的 MVVM 并不特指 Angular.js 之类双向绑定的框架，更多是 MV* 的意义。\n\n不管它有什么争议，核心的数据绑定机制上，大家是没有太多争议的。Angular.js 带来的数据双向绑定至今让人印象深刻。而 React 带来的单向数据流\n\n![UI=f(state)](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg)\n\n在今天已经几乎成为主流，在其它框架中都能找到它的影子。在 Vue 中也能找到它的身影，尤其是在引入 Vuex 之后，几乎就是 Flux 的翻版，概念几乎完全一样。\n\n今天要讨论的主题就是上面这个公式的边界，为避免 MVVM 名称带来的争议，下文直接使用“框架”一词。\n\n\u003C!-- more -->\n\n## 数据与UI的映射\n\n![math function](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F02.jpg)\n\n我们在数学中都学过 y = f(x) ，表示对每一个确定的 x ，都有唯一的 y 值与它对应。例如 y = x + 1，不论 x 取什么值，都能找到确定的 y 与之对应。而反过来理解，y 值的不同正是因为 x 取值不同导致的。\n\nUI = F(state)\n\n这也是一个典型的函数。它表示每一种 state 都有唯一与之对应的 UI 表现。反过来，UI 表现的不同正是因为 state 的不同导致的。\n\n正是因为这种映射的确定性，带来了极易理解的 UI 开发方法：我们只需要编写好映射关系，然后注意力就可以全部放到 state 上面了，因为 state 的任何变动都会体现到 UI 上，而 UI 是不会在 state 不变的情况下发生变更的。而因为同样的 state 对应同样的 UI ，我们甚至可以完全将用户当前的 UI 复制到另一台设备上，只要保证 state 是完全相同的即可。\n\n## 初窥边界\n\n如果世界真的这么简单，那也许就不会有前端工程师一职了。\n\n回想一个真实的案例，如果我们要将一个字符串显示在界面上，那么可能是这么写：\n\n```html\n\u003Cspan>{{text}}\u003C\u002Fspan>\n```\n\n```javascript\nthis.setState({\n  text:'hello world'\n});\n```\n\n此时 UI 完全由 state 决定。但是，如果这是一个输入框呢？\n\n```html\n\u003Cinput value=\"{{text}}\" \u002F>\n```\n\n这样写吗？那输入框输入的时候会发生什么？熟悉 React 的朋友应该知道，在 React 中，有一种 input 叫作 Controlled Input ，它需要监听输入框的事件，当内容变更的时候，实时改变 state 的值，从而再次达到 state 和 UI 一致的目的。然而这种情况下与其说是 state 控制 UI ，倒不如说是 UI 在控制 state 了。\n\n![controlled input](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F03.jpg)\n\n如果说内容变更还可以勉强通过 Controlled Input 的方式来保持 state 到 UI 的映射，那光标的控制就只能说是无能为力了。\n\n理论上来说，当我们输入的时候，光标也在同步变更，此时如果我们继续应用 state 到 UI 的映射，是有可能导致输入框被重新渲染的，而此时 input 中的光标是无法保证位置的。这样会导致非常奇怪的输入体验，或者说其实是没法用的。框架在处理输入框上下了非常多的功夫，其实是避免了 state 变更的时候重新渲染 input 的，最多只是改变它的属性（值）。\n\n至此，我们其实已经看到了，所谓 state 控制 UI 也只是在我们的理想中存在，现实中有非常多的细节是无法通过 state 到 UI 的映射关系照顾到的。\n\n## 再探边界\n\n上面的例子，为什么在设计 state 的时候，不把光标也设计进去呢？这样不就可以进行精准的 state 到 UI 的控制了么？\n\n我们在脑海中尝试一下：首先，需要在 state 中为每个输入框的值增加一个光标位置。接下来，需要在每一次值变更的时候计算光标位置，并通过光标 API 维护界面上光标的位置。然后，需要监听 focus \u002F blur 事件，重建、移除光标。需要监听点击事件重算光标位置，需要监听键盘事件，使光标位置发生跳跃。除此之外，还需要处理选区，此时是否还需要在 state 中添加一个选区呢？\n\n我们只是想控制一下光标而已……\n\n而如果我们不控制光标（也就是主流框架的现状），你会发现事情要更容易得多，我们上述种种光标的行为浏览器都有对应的处理和反应。也就是说，浏览器把这些行为都已经封装到了 input 组件中，而我们在大部分情况下，并不需要处理这些。这个封装行为，实际上就划出了一条边界：框架应该只管理封装组件对外暴露的部分，未暴露的部分不应该介入。\n\n再举一例，当我们引入一个文本编辑器组件（例如 AceEditor ）时，这个编辑器除了内容之外，还会有非常多的行为，例如是否显示 查找\u002F替换 的界面，是否显示行号、参考线，使用不同的语法进行高亮等等。以高亮这一行为来说，本质上它是由一堆带样式的`\u003Cspan>`元素堆起来的，例如`var a=123`这样一句简单的代码，它实际的结构可能是这样的：\n\n```html\n\u003Cspan class=\"keyword\">var \u003C\u002Fspan>\u003Cspan class=\"variable\">a\u003C\u002Fspan>\u003Cspan class=\"operate\">=\u003C\u002Fspan> \u003Cspan class=\"const\">123\u003C\u002Fspan>\n```\n\n此时我们需要将这个高亮的结构通过 state 来映射吗？还是在 state 中直接保存 var a=123 这样一个字符串就好了？答案不言自明。\n\n![highlight](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F04.jpg)\n\n至此，我们已经能非常明白地对 MVVM 的边界有一个认知。这条边界，就是组件的封装，面对一个封装的组件，我们不应该跨过封装的边界。\n\n但，问题又来了，如果组件的行为并不能通过 MVVM 的 state 来决定，那么，由谁决定？\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg",{"id":69,"type":7,"slug":70,"title":71,"date":72,"category":11,"tags":73,"body_markdown":76,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},176,"secrets-of-wild-frontend","【闲聊】大前端揭秘","2017-06-23 11:47",[74,75],"前端","大前端","\n这是一篇标题党，真正的标题叫\n\n## GMTC 2017 一些感想和收获\n\nGMTC 2017 已经于 6 月 9 - 10 日在北京召开。这个会议的全称叫作“全球移动技术大会”，今年是第二届。第一届的内容以 App 为主，今年则在大前端的趋势下同时涵盖了 App 和 Web 的主题。\n\n我也非常荣幸地被工程化专场出品人裕波邀请作为演讲嘉宾，讲了富途在 web 前端组件化方面的一些实践经验。\n\n不过，本文的重点还是想以听会者的角度来讲一讲本次参会的一些感想和收获。\n\n\u003C!-- more -->\n\n## 大前端\n\n大前端这个概念很多年前就被提出来了，我自己的个人简介也多年来一直写的“关注大前端生态圈”。最近两年，这个概念已变得炙手可热。\n\n什么是大前端呢？这得从端这个概念讲起。端其实是来自英文单词“end”，终端的意思，通俗地理解成设备。而前端则是指用户手上的设备。具体到 web 前端而言，就是指用户访问 web 所使用的设备，那基本上就是 PC \u002F 手机等设备……上面安装的浏览器。而如果去掉 web 的概念，前端其实还可以指用户手机上的 App 。只不过在翻译的时候，我们有意识做了区别，web 的叫前端，native app 的叫终端，事实上，它们都是“end”，或者“front-end”。\n\n所以到这里大前端的概念就呼之欲出了，就是指 web 前端 + native app 这个范围内的东西。之所以这个概念会火，当然不是因为两个概念被相加这么简单，而是大家越来越发现在端上，不同的技术是有充分融合的可能性的。\n\n早些年，谈到 web 和 native 的融合，大家可能唯一能想到的方式就是 webview ，基于这个方案，也有很多成型的框架，最著名的就是Cordova。但是因为 webview 先天在体验和性能上的缺陷，这种方案一直没有成为主流。\n\n近年来，在 webview 的体验改进上，行业内的专家们都付出了相当多的努力，产生了很多具体的方案，比如使用 native UI + webview 结合，将 webivew 不好处理的像滑动、页面切换等交给 native 处理。\n\n在本次 GMTC 大会上，也有听到手Q对 webview 中加载性能的极致优化。他们通过 webview 池、接管 webview 网络、server + native + web 三者结合的增量更新机制等，非常好地提升了 webview 的性能，从现场的展示来看，优化效果非常明显。据说该解决方案将在近期全面开源，可以关注手Q团队的微信公众号“小时光茶社”。\n\n除了使用 webview 之外，还有两个方向，也是大前端的方向：native 动态化和 web native化。\n\n所谓 native 动态化，是指以 React Native 为代表的，使用 JS 驱动 Native UI 的技术。这种方案的好处是可以使用成熟和庞大的 web 生态圈资源，辅助 Native 的开发。比如 JS 的热加载、热更新，JS 框架 React \u002F Vue ，使用 CSS 来进行布局和样式编写等等。目前来看，这仍然是大前端领域一个非常大的热门主题。\n\nweb native 化则主要有几个方向：\n\n一个方向是以 AMP 为代表的轻量化 web 方案。在【重发】To A Dark Futuer 一文中，我提到了现行的 web 在移动端其实是有点格格不入的，它太繁重，不适合移动端轻量化的特征。而 AMP 则是要从 web 中挑出一个适合移动端使用的子集，然后加以修饰，让 web 更轻更快，变成移动设备上的一个更漂亮的公民。GMTC 上，来自百度的工程师也提到了他们家的 MIP 方案，和 AMP 如出一辙。特别值得关注的是现场有听众提问，以后我们是否需要准备一个 AMP 版本，再准备一个 MIP 版本，带来新的兼容性适配负担？百度的工程师表示他们会兼容 AMP 。但是现场使用百度搜索了一下一些 AMP 页面，并没有什么神奇的事情发生……\n\n另一个方向是为现有的 web 增加一些 App 才有的能力，这方面的代表技术是 PWA。Google 表示未来 PWA 将在 Android 系统上拥有和 App 同样的地位。目前 PWA 已经支持了将 web 应用放到桌面，有独立图标和独立进程管理，支持使用 Service Worker 进行离线，并具备了消息推送的能力（至于天朝……你懂的）。GMTC 的现场也有来自 Google 的工程师讲解了 PWA 的能力和基本使用方式。不过个人觉得介绍得还是比较浅。对于我更关注的 PWA 未来的计划基本上只字未提。\n\n第三个方向则是以小程序为代表的“再造一个 web 生态”。本次会议上来自小米的工程师分享小米正在研发的框架，基本上可以看作是“MIUI 上的小程序”，这是本次听会感受最强烈的一个主题，跟微信小程序有相似之处，下一篇专门来讲。\n\n## 其他\n\n1. Instagram 表示他们的 App 至今仍然是单 dex 的结构，也就是方法不超过 65535个，这个比较震惊，对一个用户量如此之大的程序仍然能如此克制，心怀敬意。\n2. 微信团队现场开源了他们的 SQLite 包装库  wcdb ，看起来还不错。\n3. 路遇图灵的同学也在现场，很贴心地送了两本书。不得不说图灵在作译者关系方面还是照顾得相当好的，赞。\n",{"id":78,"type":7,"slug":79,"title":80,"date":81,"category":11,"tags":82,"body_markdown":84,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":85},180,"webpack-3-coming","webpack 3来了","2017-06-20 09:42",[83],"webpack","\nwebpack 已经正式发布了 3.0.0 ，距离 webpack 2 （2.2.0）发布才仅仅过去了 5 个月。按理说，这是一件天大的好事，当一个构建工具在快速发布版本的时候，意味着它还有更多的可能性。但是翻一下这两天的微博、朋友圈，基本上大家的声音非常一致：\n\nwebpack 2 还没学呢，3就发布了。（这可如何是好？还好没学 2 ……）这是正式加入了刷版本号的军团吗？\n\n## webpack 干了啥\n\nwebpack之所以火起来，主要是做对了这么几件事情：\n\n1. AMD \u002F Common.js 一把抓，只是要模块化方案，全都支持\n2. 支持与 npm 无缝集成，更好地助力前端代码复用\n3. 将 CSS 和图片资源变成 JS 的附属资源，从源头上进行统一管理\n4. loader与plugin机制，带来无穷多的可能性\n\n当然，这些要具体去分析又可以写一篇长篇大论了。我个人觉得，webpack最牛的地方在于让全世界的前端都接受了一个事实：\n\n前端也是需要编译的，并且编译会带来更多可能性。\n\n正是因为编译的加入，我们看到了模块化更好地落地，看到了使用 npm 带来的更好的代码复用机制，看到了 vue 单文件组件化方案，看到了 JSX 在 React 中的平滑落地，看到了  TypeScript 正逐步被大家接受，看到了 CSS in JS 的方案正遍地开花……\n\n虽然大家都不想当配置工程师，但是上述种种还是会让人心动，所以仍然有无数人会拥抱它。在未来相当长一段时间内，这会成为前端开发工作流中一个必不可少的环节。\n\n\u003C!-- more -->\n\n## webpack 2 和 3 干了啥\n\nwebpack 2 最主要的变动就是支持了 ES Modules 。在 webpack 1 时代，如果用 ES Modules 写代码，需要先使用 babel 将代码转译成 CommonJS 模块，然后才可以使用 webpack 分析、打包、优化。而 webpack 2 直接支持了 ES Modules 的分析，不需要先依赖 babel 。\n\n看起来这是一个很小的改动，但背后意义重大。这意味着 JS 生态圈向着 ES Next 迈出了坚实的一步，为后续更多 ES 的支持打下了坚实的基础。同时，因为 ES Modules 的依赖声明是静态的，webpack 2 也支持了 tree sharking 。这意味着以后写代码的时候可以放心地将一个模块的所有功能一个点一个点进行导出，而打包的时候只会包含已经引入的那一些点，没有被使用到的代码将在打包时被抛弃。\n\n值得一提的是，webpack 2 和 webpack 1 配置文件不兼容。不过好在官方有完善的升级指引，并且现在如果配置文件不对的话，会非常详尽地告诉你，哪一个值是不对的，它的可能值是什么。\n\nwebpack 3 带来的最主要的变动……呃，好像没有。这其实意味着从 webpack 2 开始，对外暴露的 API 已经基本稳定。也意味着，从 webpack 2 升级到 webpack 3 只需要 npm i webpack@3 -D 即可。在本文行文之前，我在一个项目中进行了试验，的确如此，无痛升级。\n\n当然说一点变动都没有肯定是不对的，毕竟都跳大版本号了，最值得关注的一点是 webpack 3 带来了模块的 Scope Hoisting 特性。之前的版本中， webpack 会将模块用一个函数包裹起来，而在 webpack 3 中，如果你开启了 Scope Hoisting 的话，它会尝试将模块进行合并，减少包裹函数。\n\n![webpack 2 打包结果](\u002Fassets\u002Fweb\u002F2017\u002Fwebpack-3-coming\u002F01.jpg)\n\nwebpack 2 打包结果\n\n\n![webpack 3 打包结果](\u002Fassets\u002Fweb\u002F2017\u002Fwebpack-3-coming\u002F02.jpg)\n\nwebpack 3 打包结果\n\n## webpack 的未来\n\n在 webpack 3 的介绍文章中，还强调了一些值得关注的点，比如会更关注社区用户的需求，大家最需要哪个特性，就优先开发哪个特性。此外发布周期也会更短，不会再以年为单位进行 beta 或者 RC。\n\n在 What's Next 部分，则列出了一些可能会重点关注的点：\n\n- 构建过程中更好的缓存\n- 更快的冷启动构建和增量构建\n- 更好的 TypeScript 使用体验\n- 重构长缓存机制\n- WASM 模块支持\n- 提升用户体验\n\n整体来看，webpack 的团队还是非常积极的。大家之前所诟病的问题，像文档、配置、构建速度等等都已经在逐渐改善。按这个趋势的话，我对 webpack 的未来还是相当有信心的。\n\n怎么样，看完了还觉得惶恐吗？\n\n参考资料:\n- [webpack 3: Official Release!!](https:\u002F\u002Fmedium.com\u002Fwebpack\u002Fwebpack-3-official-release-15fd2dd8f07b)\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fwebpack-3-coming\u002F01.jpg",{"id":87,"type":7,"slug":88,"title":89,"date":90,"category":11,"tags":91,"body_markdown":94,"permalink":95,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},169,"frontend-recent-safety-tech","一些比较新的前端安全相关的技术点","2017-05-18 15:06",[92,55,93],"安全","HTTP","\n> 本文为[《Web前后端漏洞分析与防御》](https:\u002F\u002Fcoding.imooc.com\u002Fclass\u002F104.html)课程的配套文章。早年在慕课社区发布，现补发在博客上。\n\n一说到安全，大家总会特别敏感，尤其是有相当部分的前端开发者并不了解安全相关的知识，颇有谈虎色变的感觉。具体到前端安全这个话题呢，又有些说不清道不明，因为大部分的防御方案，总少不了后端的参与，也有开发者慢慢觉得好像安全都应该由后端来关注了。\n\n其实不然，起码 XSS CSRF 这一类的安全问题前端是一定要了解它们的原理和防御方法的。从防御方法上来说，XSS 和 CSRF 的防御在业界都有比较成熟的方案了。本文将记录一些比较新的防御方案，可能有一些比较老的书籍或者文章中不会提及这些方法。\n\n\u003C!-- more -->\n\n## XSS\n\nXSS 全称 Cross Site Scripting ，跨站脚本攻击，因为 CSS 这名字老早就被样式表拿走了，大家都在 web 这个领域，重名又不好看，所以只好起了个名字叫 XSS 了。说实话从 XSS 干的事来讲，其实并不太理解为什么有个“跨站”在里面，如果一定要强行解释的话，大概是因为有可能会运行一个来自别人网站的脚本，把这个脚本叫“跨站”了吧。\n\n好了，不重要。\n\n重要的是它是谁，它从哪里来，要到哪里去……这样的哲学问题有点难回答。我们换一个，重要的是它是什么鬼，能干什么，怎么防。\n\n### XSS 是什么鬼\n\n玩游戏的人都知道在游戏中经常出现一些奇葩的名字，比如“星辰并亲了他一口”，看上去一脸不知道什么鬼，但是当他有点事的时候，你就觉得好玩了，比如公会老大邀请他，就会有一条消息“公会老大邀请了星辰并亲了他一口”，然后一公会的人哄堂大笑。\n\n其实这种案例用专业的术语来说，就叫 XSS 了……\n\n本来“公会老大邀请了XX”这样一个句式，只希望XX是一个不会引起误会的名字而已，结果因为有一个奇葩名字，直接改变了整句话的意思，引介出了意外的含义。\n\nXSS 也是同样的东西，比如我只想在页面上显示一个名字：\n\n```html\n\u003Cspan class=\"name\">{{name}}\u003C\u002Fspan>\n```\n\n但是，如果我的名字是长这样的：\n\n```\n星辰\u003Cscript>alert('SB')\u003C\u002Fscript>\n```\n\n这时候就好玩了：\n\n```html\n\u003Cspan class=\"name\">星辰\u003Cscript>alert('SB')\u003C\u002Fscript>\u003C\u002Fspan>\n```\n\n你看，页面中凭空多了一段脚本。这个例子还算善良的，只是弹出来骂了你一句……\n\n“难道他还能弹出来打我？”\n\n呃……当然不是啦，但是人家可以偷偷干坏事啊。你说说，你都用JS干嘛？用户登录用的JS吧，读取资料用的JS吧，点击买东西、消费用的JS吧，查用户有多少钱用的JS吧，基于 Cookies 也可以读写吧。好的，你用JS能干的事情人家都能干。\n\n没事偷你个登录态，帮用户消费两块钱，查下你用户手机号是多少……接下来的事情我就不说了（无非就是前端程序员要背锅离职呗？）\n\n### XSS 怎么防御\n\n一个经典的防御方法就是对内容进行转义和过滤，比如\n\n```javascript\nvar escapeHtml = function(str) {\n\tif(!str) return '';\n\tstr = str.replace(\u002F&\u002Fg, '&amp;');\n\tstr = str.replace(\u002F\u003C\u002Fg, '&lt;');\n\tstr = str.replace(\u002F>\u002Fg, '&gt;');\n\tstr = str.replace(\u002F\"\u002Fg, '&quto;');\n\tstr = str.replace(\u002F'\u002Fg, '&#39;');\n\t\u002F\u002F str = str.replace(\u002F \u002Fg, '&#32;');\n\treturn str;\n};\n\nvar name = escapeHtml(`\u003Cscript>alert('SB')\u003C\u002Fscript>`);\n```\n\n此时 name 会变成\n\n```\n&lt;script&gt;alert(&#39;SB&#39;)&lt;\u002Fscript&gt;\n```\n\n这样就会原样显示出来，再也无法耍流氓啦。\n\n当然，富文本还要更麻烦一些，因为要保留一部分标签和属性，要不然全变纯文本了，就不富了。这种情况一般通过黑名单进行过滤，或者白名单放行。即只允许一部分指定的标签和属性，其它的全部转义掉。\n\n### CSP 大法\n\n前面转义的方法的出发点，是让用户的输入不要变成程序，输入的什么就让它输出成什么。\n\n事实上现代浏览器为我们带来了一个全新的安全策略，叫作内容安全策略，Content Security Policy，简称CSP。CSP的思路跟转义不一样，它的着手点是，如果一段代码变成了程序，我们是否应该运行它。或者更准确一点说，它实际上是定义页面上哪一些内容是可被信任的，哪一些内容是不被信任的。\n\n因为我们自己的脚本是预先就知道并放在页面上的，所以我们可以设置好信任关系，当有 XSS 脚本出现时，它并不在我们的信任列表中，因此可以阻止它运行。\n\n它的具体使用方式是在 HTTP 头中输出 CSP 策略：\n\n```\nContent-Security-Policy: \u003Cpolicy-directive>; \u003Cpolicy-directive>\n```\n\n从语法上可以看到，一个头可以输出多个策略，每一个策略由一个指令和指令对应的值组成。指令可以理解为指定内容类型的，比如`script-src`指令用于指定脚本，`img-src`用于指定图片。值则主要是来源，比如某个指定的URL，或者`self`表示同源，或者`unsafe-inline`表示在页面上直接出现的脚本等。\n\n详细的指令和值，可以查看[MDN相关页面](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FHeaders\u002FContent-Security-Policy)。\n\n具体到上面的 XSS 例子，可以使用\n\n```\nContent-Security-Policy: script-src 'self';\n```\n\n这样除了在同一个域名下的JS文件外，其它的脚本都不可以执行了，自然之前 XSS 的内容也就失效啦。简单粗暴有没有？\n\n当然，如果你说，我就是要在页面中放点内联的脚本，不可以么？当然可以啦，CSP 设计的时候也考虑了这些情况，还是相当灵活的。你只需要指定一个 nonce 属性，或者计算一下 hash 值，即可。详细的用法看 [MDN](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FHeaders\u002FContent-Security-Policy\u002Fscript-src) 哦。\n\n说实话，用 CSP 来处理 XSS 攻击还是不如转义来得优雅，因为转义可以不影响用户输入输出，不改变内容的本质。但是 CSP 提供了足够简单而又灵活的方式来防御 XSS ，可以很好地作为我们前端 XSS 防御的最后一道防线。\n\n## CSRF\n\nCSRF 也是个望文生不到义的词，它的全称是 Cross Site Request Foggy，即跨站请求攻击。虽然也有跨站，但我觉得这个跨站还是相当可以理解的，它真的是从别的网站发起一个请求到我们的网站的。\n\n当一个用户登录我们的网站后，在 Cookies 中会存放用户的身份凭证。在大部分时候，就是一个 SessionId 。当用户下次访问我们的网站的时候，我们用这个凭证识别出用户是谁，有没有登录态。\n\n如果第三方网站的代码请求了我们的网站，会发生什么呢？比如\n\n```html\n\u003Cimg src=\"http:\u002F\u002Fwww.example.com\u002Fhaha\" \u002F>\n```\n\n虽然它是一张图片，但它确实向`www.example.com`发了一个请求，如此用户有登录态的话，其实就相当于是用户自己发了一个请求。如果这个地址是一个发表文章、发布微博甚至转账之类的链接，那用户就在不知情的情况下进行了一些操作。这也是比较严重的安全问题。\n\n当然你可能会说，现在谁还这么弱智，把这么敏感的操作用 GET 啊？没错，你可以选择用 POST ，但是这丝毫不能阻止 CSRF 攻击的发生啊。\n\n```html\n\u003Ciframe name=\"test\">\u003C\u002Fiframe>\n\u003Cform target=\"test\" method=\"post\" action=\"http:\u002F\u002Fwww.example.com\u002Fhaha\">\n    ...\n\u003C\u002Fform>\n```\n\n当这个表单提交的时候，我们就发了一个 POST 请求。华丽丽的 CSRF 。\n\n### CSRF 的常规防御\n\nCSRF 比较常规的防御方式是通过判断来源和加 token。\n\n判断来源比较简单，主要是判断`referer`这个头，如果不是自己的网站，就返回错误。\n\n加 token 即同样的随机 token，在 cookies 中放一份，在表单中再放一份。这样第三方网站就无法获取到这个 token 是什么。\n\n但是这样做也有一个比较明显的问题，就是无法保证站内用户的体验。虽然你防了站外的攻击，但是也降低了站内用户的体验。具体表现在如果同时打开多个表单，只有最后一个表单能成功提交。\n\n### same-site 的 Cookie\n\n回想 CSRF 之所以能够攻击成功，核心原因就在于用户的身份是放在 Cookies 中的，而不管你通过什么方式访问网站，都会带上这个网站的 Cookies ，从第三方来的访问自然也不能例外。\n\n但是，Chrome 在这个问题上给了我们不同的答案，可以放第三方访问时不带 Cookies 。也就是说 Cookies 只有本站能用，来自第三方的访问都不能使用。\n\n具体的使用方式，是在打 Cookie 的时候，加上一个属性：`SameSite`，它的值有两：\n\n- `strict` 任何来自第三方的请求都不能使用 Cookies ，包括通过链接点进来的\n- `lex` 只有比较敏感的操作不带 Cookies ，比如表单提交\n\n针对 CSRF ，我们可以将 Cookies 设置成`SameSite: strict`的，这样就可以有效防御 CSRF 了。不过比较可惜的是，目前只有 Chrome 才支持这一属性。希望未来所有浏览器都能跟上脚步。\n\n> 使用 SameSite 还会面临一个问题，如果用户是点击链接进来的，那么是不能使用登录态的。一般可以考虑将用户不敏感的信息不设置这个属性，点进来仍然可以显示当前用户是谁，但是在请求的时候要求一个比较敏感的 SameSite 的 Cookies。这里需要更多的实践经验来探索。\n\n## 小结\n\n本文重点讲了 XSS 和 CSRF 这两种比较常见的前端安全问题的防御思路，尤其是如何使用一些新的规范、实现来帮助我们进行防御。希望后面浏览器对这些安全相关防御办法的普及率能再高一些，让前端工程师能花更少的时间写出更安全的代码。\n","\u002Farticle\u002Ffrontend-recent-safety-tech.html",{"id":97,"type":7,"slug":98,"title":99,"date":100,"category":11,"tags":101,"body_markdown":103,"permalink":104,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},170,"how_nginx_processes_a_request","【译】nginx是如何处理请求的","2017-05-05 12:55",[102],"nginx","\nNginx首先需要确定由哪个`server`来处理请求。我们看一个简单的配置文件，在`*:80`端口上包含了三个虚拟主机（`server`）：\n\n```\nserver {\n    listen      80;\n    server_name example.org www.example.org;\n    ...\n}\n\nserver {\n    listen      80;\n    server_name example.net www.example.net;\n    ...\n}\n\nserver {\n    listen      80;\n    server_name example.com www.example.com;\n    ...\n}\n```\n\n在这个配置下，Nginx只通过请求的`Host`头来决定路由到哪个`server`。如果`Host`头的值跟所有的`server`的`server_name`都不匹配，或者请求中没有包含这个阔大，Nginx会使用这个端口上的默认`server`。在这个配置文件中，默认`server`是指第一个，这正是Nginx标准的默认行为。默认`server`也可以通过配置显示指定，只需要在`listen`指令值中加上`default_server`参数即可。\n\n\u003C!-- more -->\n\n```\nserver {\n    listen      80 default_server;\n    server_name example.net www.example.net;\n    ...\n}\n```\n\n> `default_server`参数在 0.8.21 版本之后可用。在更早的版本中，应该使用`default`参数。\n\n值得注意的是，默认`server`是`listen`指令的参数，而不是`server_name`的。下文会详细介绍。\n\n## 如何阻止没有指明 Host 的请求\n\n如果要禁止没有包含`Host`头的请求，可以用下面的配置让一个`server`丢弃这样的请求：\n\n```\nserver {\n    listen      80;\n    server_name \"\";\n    return      444;\n}\n```\n\n如果请求没有带`Host`头，将与`server_name`为空字符串的`server`匹配，返回非标准的私有状态码444时，Nginx会关闭连接。\n\n> 从 0.8.48 版本开始，空字符串是`server_name`的默认值，因此`server_name \"\"`可以省略。在更早的版本中，默认值是机器的主机名。\n\n## 基于 IP 和基于主机名的虚拟主机\n\n我们来看一个更复杂的例子，这个例子中有一些监听在不同地址上的`server`：\n\n```\nserver {\n    listen      192.168.1.1:80;\n    server_name example.org www.example.org;\n    ...\n}\n\nserver {\n    listen      192.168.1.1:80;\n    server_name example.net www.example.net;\n    ...\n}\n\nserver {\n    listen      192.168.1.2:80;\n    server_name example.com www.example.com;\n    ...\n}\n```\n\n在这个配置中，Nginx 首先将请求的 IP 地址和端口与`server`进行匹配，得到一些匹配的`server`。然后根据请求的`Host`头与这些`server`的`server_name`进行匹配。如果`server_name`无法匹配，则请求将由默认`server`处理。例如，在`192.168.1.1:80`上收到一个`www.example.com`的请求，这个请求将由`192.168.1.1:80`上的默认`server`进行处理，也就是由第一个`server`进行处理，因为无法在这个（IP 地址和）端口上匹配到`www.example.com`。\n\n前面已经说过，默认`server`是`listen`指令的一个参数，那么监听不同的（IP 地址和）端口就可以指定不同的默认`server`：\n\n```\nserver {\n    listen      192.168.1.1:80;\n    server_name example.org www.example.org;\n    ...\n}\n\nserver {\n    listen      192.168.1.1:80 default_server;\n    server_name example.net www.example.net;\n    ...\n}\n\nserver {\n    listen      192.168.1.2:80 default_server;\n    server_name example.com www.example.com;\n    ...\n}\n```\n\n## 一个简单的 PHP 站点配置\n\n现在我们通过一个简单的 PHP 站点来看一下 Nginx 是如何处理`location`的：\n\n```\nserver {\n    listen      80;\n    server_name example.org www.example.org;\n    root        \u002Fdata\u002Fwww;\n\n    location \u002F {\n        index   index.html index.php;\n    }\n\n    location ~* \\.(gif|jpg|png)$ {\n        expires 30d;\n    }\n\n    location ~ \\.php$ {\n        fastcgi_pass  localhost:9000;\n        fastcgi_param SCRIPT_FILENAME\n                      $document_root$fastcgi_script_name;\n        include       fastcgi_params;\n    }\n}\n```\n\nNginx 首先会通过遍历字符串的方式查找指向最具体的`location`前缀，这与配置顺便无关。在上面的楝文件中，唯一的前缀是`\u002F`，它可以匹配任意请求，将被最后使用。然后 Nginx 会按照配置书写的顺序进行正则表达式匹配。一旦匹配到第一条规则，则会停止后续查找，Nginx 将使用这个`location`。如果正则表达式匹配全部失败了，则 Nginx 会使用前面找到的前缀指向最具体的`location`。\n\n值得注意的是，所有类型的`location`都只匹配请求的`URI`部分，不包括任何参数。这是因为请求的参数可能有很多种形式，例如：\n\n```\n\u002Findex.php?user=john&page=1\n\u002Findex.php?page=1&user=john\n```\n\n此外，用户可以在参数中随便添加任何东西：\n\n```\n\u002Findex.php?page=1&something+else&user=john\n```\n\n现在我们来看看，按上面的配置文件，一个请求将如何被处理：\n\n- 请求`\u002Flogo.gif`首先被前缀`location` `\u002F`匹配到，然后被正则表达式`\\.(gif|jpg|png)$`匹配到。这样的话，它将由后者进行处理。因为指定了`root \u002Fdata\u002Fwww`，因此这个请求将被映射到`\u002Fdata\u002Fwww\u002Flogo.gif`，这个文件将被发送到客户端。\n- 请求`\u002Findex.php`也被前缀`location ` `\u002F`匹配到，然后被正则表达式`\\.(php)$`匹配到。这样的话，它将由后者进行处理。请求被转交到监听在`localhost:9000`的 FastCGI 服务进行处理。`fastcgi_param`指令会将 FastCGI 的参数`SCRIPT_FILENAME`设置为`\u002Fdata\u002Fwww\u002Findex.php`，然后 FastCGI 服务会执行这个文件。`$document_root`变量的值等于`root`指令的值，变量`$fastcgi_script_name`的值等于请求的 URI ，也就是`\u002Findex.php`。\n- 请求`\u002Fabout.html`只被前缀`location` `\u002F`匹配到，这样它将被在这个`location`中进行处理。因为指定了`root \u002Fdata\u002Fwww`，因此这个请求将被映射到`\u002Fdata\u002Fwww\u002Fabout.html`，这个文件将被发送到客户端。\n- 请求`\u002F`的处理更复杂一些。它只被前缀`location` `\u002F`匹配到，这样它将被在这个`location`中进行处理。接下来`index`指令将根据`root \u002Fdata\u002Fwww`指令的路径探测`index`参数中指定的文件是否存在。如果`\u002Fdata\u002Fwww\u002Findex.html`不存在，而`\u002Fdata\u002Fwww\u002Findex.php`存在，则该指令将内部跳转到`\u002Findex.php`，然后 Nginx 会像对待新请求一样重新匹配这个请求。如前文所述，这个请求将被 FastCGI 服务进行处理。\n\n编写：Igor Sysoev 编辑：Brian Mercer\n\n原文：\u003Chttp:\u002F\u002Fnginx.org\u002Fen\u002Fdocs\u002Fhttp\u002Frequest_processing.html>\n","\u002Farticle\u002Fhow_nginx_processes_a_request.html",{"id":106,"type":7,"slug":107,"title":108,"date":109,"category":11,"tags":110,"body_markdown":113,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},181,"why-richtext-editor-on-web-is-terrible","为什么web富文本编辑器是天坑？","2017-03-15 10:17",[11,111,112],"富文本","编辑器","\n知乎看到一个问题，《为什么都说富文本编辑器是天坑》。里面的答案挺有意思的，各位做web前端的大牛们都有自己的切肤之痛。有兴趣的可以手工复制以下网址查看：\n\n\u003Chttps:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F38699645\u002Fanswer\u002F108376836>\n\n但是这些答案大部分还是停留在碰到的问题上面，而没有去深入总结和思考富文本编辑器背后的需求和实现思路。正好前两年在medium上看到一篇文章，叫《Why ContentEditable is Terrible》，推荐给大家。原文可点击左下角链接查看。\n\n\u003Chttps:\u002F\u002Fmedium.engineering\u002Fwhy-contenteditable-is-terrible-122d8a40e480>\n\n\u003C!-- more -->\n\n以下为简略意译版本：\n\n【一段引子，大意就是表示作者在富文本编辑器的问题上已经思考了一年了。】\n\n从数学证明的角度来说，ContentEditable在富文本的编辑这件事上是有问题的（broken，也许应该翻为烯烂？）。\n\n首先，什么叫作“所见即所得”（WYSIWYG）？当你选中一段文本按下回车的时候会发生什么？这些问题的定义并不是很清楚，而从数学证明的角度可以首先弄明白我们要研究的问题是什么。\n\n什么叫所见即所得？一个好的所见即所得编辑器应该遵守以下三条公理：\n\n- DOM内容和可见的内容的映射应该是工作良好的（well-behaved）\n- DOM选区和可见的选区的映射应该是工作良好的（well-behaved）\n- 所有可见的编辑操作应该映射到对可见内容的一个代数闭域和完整集合\n\n我将会解释这三条分别是什么意思，以及富文本编辑器为什么应该遵守这三条规则。但是首先我们要记住，公理是不需要证明的，所以它们的正确性先不作置疑。\n\n接下来，我们会看到ContentEditable无法遵守上面的任意一条公理。\n\n最后我们来看一看一些解决方案，以及Medium的富文本编辑器如何解决这个问题。\n\n。。。敷衍的分隔线。。。\n\nDOM空间是指页面的内在结构，而可视空间（WYSIWYG）是页面的渲染结果。当我们说两个页面一样时，是指他们在可视空间看起来一样。\n\n浏览器的渲染引擎做的工作就是将DOM空间映射到可视空间。即对DOM x来说，可视空间 = 渲染函数Render(x)。\n\n当我们说一个映射工作良好（well-behaved）时，是指所有的编辑操作都能在映射中反映出来。如果用E表示编辑操作，x和y表示DOM，则有\n\n- 如果Render(x) = Render(y)\n- 那么Render(E(x)) = Render(E(y))\n\n这其实是在处理“所见”之后的“所得”部分。即如果两个页面看起来一样，我们对它们做相同的编辑操作，那么操作之后的结果应该仍然看起来一样。\n\n事实上无数的富文本编辑器无法满足这个假设。看例子\n\nThe [hobbit](http:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FThe_Hobbit) was a very well-to-do hobbit, and his name was *_Baggins_*.\n\n对应的DOM大概如下\n\n```html\nThe \u003Ca href=”http:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FThe_Hobbit\">hobbit\u003C\u002Fa> was a very well-to-do hobbit, and his name was \u003Cstrong>\u003Cem>Baggins\u003C\u002Fem>\u003C\u002Fstrong>.\n```\n\n对于粗体和任何的部分，有非常多的编码方法\n\n```html\n\u003Cstrong>\u003Cem>Baggins\u003C\u002Fem>\u003C\u002Fstrong>\n\u003Cem>\u003Cstrong>Baggins\u003C\u002Fstrong>\u003C\u002Fem>\n\u003Cem>\u003Cstrong>Bagg\u003C\u002Fstrong>\u003Cstrong>ins\u003C\u002Fstrong>\u003C\u002Fem>\n\u003Cem>\u003Cstrong>Bagg\u003C\u002Fstrong>\u003C\u002Fem>\u003Cstrong>\u003Cem>ins\u003C\u002Fem>\u003C\u002Fstrong>\n```\n\n从编辑器的角度来说，它们应该是一样的，但是从DOM的角度很难识别它们是同样的东西。\n\n很多编辑器在这方面会有问题，比如有一个空的`span`可能会导致两个ContentEditable元素看起来一样，但编辑行为完全不一样，让用户觉得很难编辑，工程师也很难调试。\n\n理想情况下，我们应该可以对DOM使用可视化编辑的指令，这些指令会保证对可视空间相同的页面进行相同编辑操作后可视空间仍然是完全相同的。\n\n。。。分隔线。。。\n\nDOM空间和可视空间的映射很抓狂，但至少是多对一的，一个DOM只会有一种可视表现。\n\n选区就更糟糕了，它是多对多映射的。\n\n```html\nhis name was \u003Cstrong>\u003Cem>Baggins\u003C\u002Fem>\u003C\u002Fstrong>\n```\n\n假设这样一段内容，光标在Baggins前面，那么光标到底是在`\u003Cstrong>`前后还是`\u003Cem>`前后？你打字的时候希望是粗体的还是斜体的还是没有任何样式的？\n\n更麻烦的是，一个DOM选区会有多种可视表示。比如“well-to-do”在“to-”后面折行了，那么在光标在第一行最后和在第二行开始对应着相同的DOM位置，但可视位置却不一样。\n\n我们设计编辑器时希望选区看起来和表现起来都是一样的，但是这个映射完全乱七八糟。\n\n。。。分隔线。。。\n\n【这一段举了webkit的例子，省略，只说结论】\n\n富文本编辑器可以产出非常简洁易懂的代码，但是一旦碰到来自外部的内容就蒙逼了，比如粘贴自网页或者word的内容。\n\n一个好的富文本编辑器的内容应该是对自己的编辑行为闭域的，即内容应该只来自于正常的输入和编辑，而不应该介入来自外部的内容。\n\n。。。分隔线。。。\n\n好的富文本要怎么做？Medium的思路：\n\n1. 为文档生成数据模型，可以根据模型判断是否在可视空间相同\n2. 建立从DOM到模型的映射关系\n3. 在模型上建立编辑操作的定义\n4. 将所有的键盘和鼠标操作映射到上述编辑操作\n\n1 模型\n\nMedium编辑器的模型包含两部分：一个段落列表，一个区块列表。\n\n每个段落包括：纯文本，标记（如第1个字到第5个字是粗体），图片等数据，布局数据（位置等）\n\n区域则是包含段落的一片区域。\n\n选区使用两个点来表示，每个点由段落和索引和文本的偏移量来表示。\n\n这个模型的好处是当且仅当两个模型相同的时候，可视空间才会相同，模型的任何变更都会很好地映射到可视空间（well-defined）。\n\n2 DOM到模型的映射\n\n分为内部映射和外部映射。内部映射是DOM和模型的映射，我们希望是一对一的。外部映射是当我们拿到粘贴的内容时，需要将内容映射到我们的段落-区块模型。我们希望外部映射是有损的，首先处理纯文本，然后是粗体\u002F任何\u002F链接等标记，接下来是图片和其它的格式 。\n\n将模型映射到DOM看起来类似这样：\n\n```html\n\u003Cdiv> \u003C!-- root -->\n  \u003Csection> \u003C!-- section -->\n    \u003C!-- section-inner -->\n    \u003Cdiv class=\"section-inner layout-column\">\n      \u003Cp>  \u003C!-- paragraph -->\n        \u003Cstrong>\u003Cem>Baggins\u003C\u002Fem>\u003C\u002Fstrong> \u003C!-- text -->\n```\n\n【省略一段解释对应关系的，注释中都有写了。】\n\n映射样式的时候，我们会按顺序进行：首先是A，然后STRONG，然后EM，绝不会出现A在STRONG中的情况。\n\n3 编辑操作\n\n定义了6种操作：添加段落、移除段落、更新段落、添加选区、移除选区、更新选区。\n\n所有的编辑操作都可以由这6种操作进行组合而来。在这些操作下，所有的内容都是结构良好的。这些操作直接对应我们模型的变更，而不是DOM的变更 。\n\n4 捕获编辑动作\n\n键盘和鼠标操作被映射到这6种操作中，具体而言是从ContentEditable操作中映射而来：换行（enter\u002Fctrl-m）、删除（delete\u002Fbackspace）、输入、粘贴等。我们捕获这些事件，然后阻止它们的默认动作，然后将这些动作转换成内部的编辑操作。\n\n对其它的键盘事件，我们不阻止默认动作，而是让ContentEditor进行操作，然后在事件结束后将段落和DOM再映射回模型。\n\n如果每次模型变更都全量更新DOM，编辑体验会很差，因为内容在不停闪烁。我们监听了模型的变更，尽量让DOM的变化最小化。\n\n。。。分隔线。。。\n\n【展望未来的富文本编辑器，略，都是没影的东西。】\n",{"id":115,"type":7,"slug":116,"title":117,"date":118,"category":11,"tags":119,"body_markdown":121,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":59},171,"ios-disable-hotfix-and-the-future-of-web","【蹭热点】从iOS禁用热更新扯到web的未来","2017-03-09 09:24",[120,11],"热更新","\n昨天发生了一件不大不小的事情：有相当多的iOS开发者收到了来自苹果的警告邮件，大意是“你的app有动态修改应用逻辑的行为”。说得更通俗一些，就是能绕过app store的发版审核机制，直接进行更新的app基本都收到了警告。\n\n按理说，这其实是一件于情于理都没什么好争辩的事情，毕竟发版本要经过审核是白纸黑字定下的规则。但是这件事情却掀起了轩然大波，甚至连React-Native和Weex都一瞬间有种人人自危的感觉。这到底是为什么呢？\n\n## 审核之痛\n\n首先，为什么有这么多人在明知冒险的情况下仍然会选择使用热更新的方式呢？主要的原因还是苹果的版本发布审核太慢，据说7天以上是家常便饭。试想，一个app发出去，发现有一个bug，用户居然需要一周以上的时候才能更新到修复版本，这个在当前中国互联网环境下确实是不太适用的，所以热更新才能大行其道。\n\n当然，也有一部分人完全是动了歪心思，先合法地上架app，然后再修修修改已上架的app行为，偷偷摸摸做些见不得人的事情。\n\n## web缺席\n\n既然热更新是如此迫切的需求，那难道就没有更好的方案吗？比如……web？天生就热更新，不好么？\n\n当然好。可是web的性能和体验这么差，大部分情况下属于不得已的情况下才会考虑的方案。所以在移动互联网的崛起中，web基本是缺席的。而且这个现象并没有任何好转的迹象。\n\n\u003C!-- more -->\n\n## web在互联网的地位\n\n这一段标题党了，既然都缺席了，还谈何地位？曾经有一段时间，web在移动互联网的应用是很被看好的，也就是HTML5红遍大江南北那几年，随着各种native API（定位、加速计、电池等）的到位和各种hybrid方案的成熟，web一度有要将Android和iOS直接干下马的气势。\n\n然而，时间终将会说明一切。事实证明，一个大而全的web体系并不适合在移动互联网使用。\n\n在《To a Dark Future》一文（见公众号菜单）中，我曾经说过，web标准体系是一个非常繁杂的体系，即使是想在页面上显示一个文本，也会面临一堆的事务需要处理。 同时，由于web标准关注点的迷失，一些长久以来无法很好在web中完成的东西现在仍然缺位。这导致web在移动互联网时代有着天然的性能和体验缺陷，而且短期内看不到有利好的趋势。\n\n## A New Web?\n\n既然大而全的web体系并不适合在移动互联网使用，那是否有替代方案呢？\n\nGoogle给了一个方向，就是PWA。但是思路仍然是在web上做加法。如果这个东西推广成功了，那么至少web可以解决一个老大难的资源加载问题。然而，由于标准制订牵涉到各方利益，目前来看，这个标准基本上不可能推广到iOS中。从昨天的苹果警告热更新应用中更可以明显的感觉到这一信号：苹果不希望自己的应用生态被侵入，而如入PWA能落地，无疑是一个巨大的威胁。\n\n微信也给了一个方向，就是小程序。很多人看完小程序都会有一个感觉：这和web不是同一个东西吗？从技术的角度上来说，小程序之所以和web撇清关系，就是希望抛开繁杂的web体系，重建一个微信自己的新web，而这个新web中什么特性保留什么特性抛弃完全是微信自己掌控。控件不满足需求时新建一种，性能有问题时改一下实现，甚至改一下API也不是什么大事。关于小程序和web的技术考量，我在知乎《微信小程序为什么不用HTML5、CSS，自己搞了个WXML、WXSS，很多框架用不了，好处一点不知道？》这个问题下有详细回答，可[点击查看](\u002Farticle\u002Ftech\u002F2017\u002Fwhy-mini-program-use-self-developed-tech-stack.html)。\n\n## Future\n\n未来会怎样？我在[Dark Future](\u002Farticle\u002Fweb\u002F2017\u002Fto-a-dark-future.html)一文中表示对web前端持悲观态度。\n\n但是……（对，按套路，肯定有但是的……）热更新的需求仍然无比旺盛。如果苹果继续保持审核时的傲骄姿势的话，新web还一定会出现，但既然叫新web，也就跟现有的web没有太大关系了，但这也许是web唯一的出路。\n",46]