[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002F5":3},{"items":4,"total":128},[5,21,31,42,51,59,70,80,89,99,108,118],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":15,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},126,"article","should-involve-design-as-a-developer","【问答】如何看待“代码没有写到10万行不要碰设计”这样的观点？","2018-07-29 10:45","tech",[13,14],"开发","设计","\n本文来自知乎问题[如何看待“代码没有写到10万行不要碰设计”这样的观点？](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F287006602\u002Fanswer\u002F454886392)\n\n有些答主可能误解了题主说的“设计”的意思。这里不是指代码设计或者工程设计，指的是交互和视觉设计。\n\n## 1 前端需要对交互和视觉设计有一些认识，这是完全没有问题的\n\n比较硬的关系是，你会接设计同学出的设计稿，如果一点不了解设计领域的知识，拿到稿件会手足无措。适当地了解设计理论、方法、软件使用以及设计稿的常用处理（蒙版、切图、切片、变换、拼接等），是前端必备的工作知识。\n\n比较软的关系是，前端是离用户最近的工程师，需要对用户体验负责。很多时候设计稿输出来的是静态的，但是用户交互的是一个动态的页面，如何把这些交互做好，设计和前端基本上各有一半的能力和责任。\n\n\u003C!-- more -->\n\n## 2 分清楚输入和输出\n\n很多新人弄不清楚自己能力的输入和输出是什么关系。所谓输入就是学习，你的代码能力、设计能力、审美的学习培养，这些都是输入。把你的代码能力、设计能力和审美应用到工作中，输出好的产品，这是输出。\n\n我个人认为，公司的工作是没有义务为输入负责的，更多的是需要输出。换句话说，你的学习、培养，不应该由公司的工作来承担，公司为你付工资，是希望你能输出自己的能力，为公司的产品添砖加瓦、锦上添花。\n\n因此这就很容易理解了，你作为一个不会设计的前端，要去参与交互设计的过程，这对你来说是一个输入的过程，是你学习怎么做交互的过程。对于公司的工作来说，这很可能是一个负效率的事情。因此，公司\u002Fleader不予答应是天经地义的，不要有什么抱怨。\n\n如果真的觉得自己需要学习锻炼，可以放到业余时间去学习。你总会有上下班的时间，有休息的时间，有项目间空档的时间，用这些时间去学习和锻炼，甚至与你们项目的产品、设计同学去探讨设计、实现上的细节，探讨用户体验、用户价值等。这是一个更合理的方式。\n\n## 3 借力公司资源\n\n如上一段所说，让不专业的人参与专业的工作，对公司来说不一定是好事。但是公司总还是有专业的资源，包括设计的流程、设计的资源库、方法论积累、规范以及专业的设计师同学。这些都是学习过程中非常难得的资源，可以先静下心了解、学习。甚至在自己的工作时间允许的时候，参与设计评审等环节，但只能以旁听、学习者的身份参与。如果真的可以的话，经过一段时间积累，会有比较大的提升。\n\n## 4 注意专业深度和广度的关系\n\n上一段说的借力公司资源，理论上是可行的。但是实际操作中，可能引发一些问题，比如Leader会担心你是否不想在开发上做深入学习和实践了，是否想转设计岗，甚至是否是想离职。\n\n所以更为重要的是，要认清自己的身份，你首先是一个工程师，需要对代码负责。接下来才是一个前端工程师，大部分对代码负责，少部分对用户、交互、体验负责。（如果公司\u002F项目的要求没那么高，那只对代码负责也可以的。）因此，所谓“代码没有写到10万行不要碰设计”，本质上是希望你首先打好作为工程师的基础，把手上的代码写好。先学会好好写代码，再去学设计。\n\n这其实也就是常说的专业深度和广度的关系。你说前端的基础不包括设计知识吗？当然也包括，第一段就说了。但是在一个人入门还不深入，工作刚能上手的阶段，更好的学习路径必然是先把更为基础的代码学好。设计作为前端进阶甚至外延的知识，在打好第一阶段的基础之后再去了解学习更好。这个道理其实也适用于其它领域的知识，比如对前端来说，后端、客户端、测试等，都重要。但是没有这些知识，你同样能干活，但是没有前端基础，你干不了活。\n\n从公司的角度来说，招你过来，也是为了让你输出前端方面的能力，而不是培养你的设计知识，否则为什么不招设计师呢？可能还更便宜一些。也就是说，你的价值首先是100%的前端，然后才可以叠加10% 20%的设计，这属于你在前端之外为公司创造的额外的价值。如果你是50%的前端 + 10%的设计，还有一堆的前端问题都搞不定\u002F搞不好，那公司可能更希望招一个80%但不会设计的前端。不管是在自身学习还是在参与公司工作、解决产品问题，都要非常清醒地认识到这一点。\n\n所以个人建议的学习通道是，好好学前端知识，好好码前端代码。先把知识学深，觉得技术上基本上没有太大的难题，什么问题都有解决方案了，再去追求更广的领域，比如产品、设计、用户体验等等。\n",null,0,"2026-08-28 04:37:17","published","",{"id":22,"type":7,"slug":23,"title":24,"date":25,"category":26,"tags":27,"body_markdown":30,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},185,"work-with-npm-package-lock","如何与NPM package-lock.json愉快地玩耍","2018-07-26 19:40","web",[28,29],"npm","package-lock","\n## 背景\n\n对的，最近写文章都会交代一下背景。因为按标题的套路，这本应该是一篇教程类的文章，但这种文章其实挺无趣的。之所以想写这篇，是因为确实碰到了一些很麻烦的事情。闲言少叙，我们进入正题。\n\n最近我们前端代码打包正在接入Gitlab CI，使用Docker来作为Executor，也就是在Docker中进行前端代码打包，然后收集打包结果，以备发布时使用。打包时Docker镜像很自然地就选择了官方Node镜像，最新版本（Node 10）。\n\n一开始我们尝试性地接入了几个项目，有使用NPM scripts进行打包的，也有使用Gulp进行打包的，一切都很正常。但是昨天在接入一个新项目，使用Gulp打包的时候，却突然碰到了报错：\n\n```sh\n$ gulp gitlab-ci\ngulp[85]: ..\u002Fsrc\u002Fnode_contextify.cc:631:static void node::contextify::ContextifyScript::New(const v8::FunctionCallbackInfo\u003Cv8::Value>&): Assertion `args[1]->IsString()' failed.\nAborted\nERROR: Job failed: exit code 134\n```\n\n看了一眼这个错误信息，一下子就发现，这并不是来自JS层的错误，而是来自Node原生层，这就超出了我的理解范围了。\n\n\u003C!-- more -->\n\n## 模块版本兼容问题\n\n通过搜索，首先找到一篇中文的解决方案：[升级node10后gulp报错的解决办法](https:\u002F\u002Fliyang.pro\u002Fafter-upgrading-node10-gulp-error-solution\u002F)。文中给出了如下解决办法：\n\n```sh\nrm -fr node_modules\nrm -fr package-lock.json\nnpm cache clean --force\nnpm install\n```\n\n所以这是什么原理呢？没看到文章有解释，另外这文章本身也写得不是很清楚。抱着半信半疑的态度，在CI的脚本中加上了`npm cache clean --force`，结果问题依旧。\n\n接着搜索了一下，很快发现一些和这个问题有关的issue：\n\n- nodejs\u002Fnode: [node 10.0.0安装模块时core dump](https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fnode\u002Fissues\u002F20281)\n- nodejs\u002Fnode: [新发布的版本与Gulp ^3.9.0不兼容](https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fnode\u002Fissues\u002F20285)\n- gulpjs\u002Fgulp: [将graceful-fs的升级合并回Gulp 3](https:\u002F\u002Fgithub.com\u002Fgulpjs\u002Fgulp\u002Fissues\u002F2146)\n- gulpjs\u002Fgulp: [node 10中崩溃](https:\u002F\u002Fgithub.com\u002Fgulpjs\u002Fgulp\u002Fissues\u002F2162)\n- isaacs\u002Fnode-graceful-fs [v3.x分支与Node.js master不兼容](https:\u002F\u002Fgithub.com\u002Fisaacs\u002Fnode-graceful-fs\u002Fissues\u002F120)\n- isaacs\u002Fnode-graceful-fs [v3.0.11依赖natives 1.1.1，后者与node 10不兼容](https:\u002F\u002Fgithub.com\u002Fisaacs\u002Fnode-graceful-fs\u002Fissues\u002F130)\n\n通过`npm ls`，一下子就看到了`natives`模块的版本是1.1.1，于是定位到真正的问题：`gulp`依赖`vinyl-fs`，`vinyl-fs`依赖`graceful-fs`，`graceful-fs`依赖`natives`，而`natives`在1.1.3之前都不兼容Node 10。\n\n那么解决方案就简单了，想办法升级一下模块不就好了？\n\n## 如何升级间接依赖\n\n首先想到的是Gulp是否有解决这个问题，`npm info gulp`看了一下，发现最新版本是`3.9.1`，然后`npm ls`一下，发现本机装的已经是`3.9.1`了，也就是说，Gulp根本没有升级的可能。那要如何升级这个间接的依赖呢？\n\n这里我绕了个弯子，`package.json`中直接依赖的模块无法升级，`npm update`也不能搞定。于是想到，如果显式安装一下这个模块，是不是能解决问题？\n\n```sh\nnpm install natives@1.1.3\n```\n\n安装完之后还要在`package.json`中将直接依赖`natives`删除（因为我并不直接依赖它，留着没用）。然后再次查看依赖：\n\n```sh\n▶ npm ls natives\nseed@1.0.0 \u002FUsers\u002FTooBug\u002Fwork\u002Foa\u002Flearn\u002Ffrontend\n└─┬ gulp@3.9.1\n  └─┬ vinyl-fs@0.3.14\n    └─┬ graceful-fs@3.0.11\n      └── natives@1.1.3\n```\n\n这次对了。\n\n但是，这个操作我是在本机进行的，这个操作到底改了什么呢？如果去CI上再次执行`npm install`，会不会依然安装到旧版本呢？毕竟`package.json`并没有任何改动。\n\n于是翻看了一下`git diff`，发现了本文的主角`package-lock.json`被修改了，其中的`natives`由1.1.1变成了1.1.3。\n\n```json\n\"natives\": {\n    \"version\": \"1.1.3\",\n    \"resolved\": \"http:\u002F\u002Fregistry.npm.oa.com\u002Fnatives\u002Fdownload\u002Fnatives-1.1.3.tgz\",\n    \"integrity\": \"sha1-RKV5vmRQfqLW7RygSpQVkVz3VVg=\"\n},\n```\n\n于是怦然大悟：本来npm在安装模块是是按语义化版本安装最新版的，但是在CI上却没有安装`natives` 1.1.3版本，而是安装了`natives` 1.1.1版本，这正是因为`package-lock.json`装版本锁定了，从而导致了与Node 10的不兼容问题。\n\n这也能很好地解释，为什么其它项目没有这样的问题，因为其它的项目在代码仓库中没有包含`package-lock.json`，在安装时自然就安装了`natives` 1.1.3版本。\n\n于是再回头看一开始搜到的文章提出的解决办法：\n\n```sh\nrm -fr node_modules\nrm -fr package-lock.json\nnpm cache clean --force\nnpm install\n```\n\n其实是非常有效的，因为它将`package-lock.json`直接删除了，然后重新安装一遍最新版本，生成新的`package-lock.json`，从而解决问题。\n\n## 延伸：如何正确地在项目中使用`package-lock.json`\n\n最后，也补充说明一下`package-lock.json`的正确用法。（虽然我知道，但还是不小心踩坑了。如果不清楚它的用法的话，可能会在坑里待更长时间爬不出来。）\n\n首先，需要确保`npm`的版本在5.6以上，因为在5.0 - 5.6中间，对`package-lock.json`的处理逻辑更新过几个版本。5.6以上的才是符合认知的逻辑。\n\n然后，在项目中如果没有锁版本的必要，可以不使用`package-lock.json`，在安装模块时指定`--no-lock`即可。\n\n如果项目中有`package-lock.json`，则安装模块时会以这个文件中指定的版本和地址为准，直接下载安装。（除非它和`package.json`中指定的版本不相符。）\n\n最后，如果已经锁定了版本的情况下，需要更新直接依赖，则直接安装指定版本，`package.json`和`package-lock.json`都会同步更新。如果需要更新间接依赖的话，则需要像本文这样，手工安装一下，保证`package-lock.json`被更新到。或者如果其它模块的锁定并不是那么重要时，也可以直接删除`package-lock.json`，然后重新安装一遍，相当于把所有（直接依赖+间接依赖）模块全部更新一遍。\n",{"id":32,"type":7,"slug":33,"title":34,"date":35,"category":26,"tags":36,"body_markdown":40,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":41},182,"a-bug-in-wechat-work","记一次企业微信webview bug排查","2018-07-24 10:40",[37,38,39],"bug","debug","企业微信","\n## 背景\n\n咱们在企业微信上开发了一个简单的选课系统，用于让公司内的同学在企业微信上进行选课。昨天下午HR的同学很开心地推送了8月份的课程列表，让大家报名。然后就炸锅了：企业微信中点击课程没反应，报不了名！\n\n收到消息第一时间感觉确认了一下问题，发现Mac下使用完全没有任何问题。于是打听了一下，出问题的都是Windows电脑的企业微信。直接使用Chrome浏览器访问或者在手机企业微信上访问都是没有问题的。\n\n那就简单啦，赶紧引导大家在浏览器或者手机企业微信上访问。问题解决！顺便感慨一下：狡兔三窟真重要啊，还好我们及早打通了OA登录，在各种环境下都可以登录使用。\n\n当然，夜深人静的时候，作为一名靠谱的前端工程师，还是要老老实实查问题的。于是诞生此文，流水账，记录bug排查的全程并揭秘原因。\n\n\u003C!-- more -->\n\n## TLDR\n\n> Too Long, Didn't Read\n>\n> 太长了，不想读\n\n好的，说重点：\n\n1. Windows企业微信某个版本的webview有bug，导致`'ontouchstart' in windows`为true，事实上它并不支持触控事件。\n2. 这会导致第三方库`iscroll`产生误判，认为这是一个触控设备，从而错误地将`disableMouse`设为`true`，进而忽略`mousedown\u002Fmouseup`事件，导致内部不向外发出`click`事件。\n3. 最终导致外层绑定的`click`事件处理函数不触发，点击无效。\n\n## 排查过程\n\n### 重现\n\n首先，想办法重现。\n\n打开虚拟机，下载企业微信，登录、开应用，一顿操作猛如虎，然后……没发现问题。\n\n重现失败。\n\n接下来找反馈方。您的系统是什么版本？您的企业微信是什么版本？您是什么网络？得知是旧版企业微信。于是赶紧让组里小伙伴确认一下，是不是有问题的升级一下就没有问题了。\n\n是。\n\n锁定问题范围：旧版本Windows企业微信下页面出现点击无效。\n\n虚拟机下载了一个旧版本企业微信，覆盖安装，再次登录、开应用，一顿操作猛如虎，然后果然看到了问题所在。\n\n接下来，想办法在开发环境重现问题。开启本机环境，配置hosts访问本机，问题依旧，重现成功。\n\n### 确认问题\n\n再接下来，确认问题来源。\n\n因为企业微信没有调试工具或者调试接口，只能先引入`vconsole`模块，确保日志能被看到。\n\n```sh\nnpm install vconsole\n```\n\n```javascript\nvar VConsole = require('vconsole');\nnew VConsole();\n```\n\n![VConsole界面](\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F1.png)\n\n展示一下项目的Vue代码：\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"content\">\n        \u003Cdiv class=\"listWrapper\" ref=\"wrapper\">\n            \u003Cul>\n                \u003Cli v-for=\"course in courseList\" @click=\"chooseCourse\">{{course.name}}\u003C\u002Fli>\n            \u003C\u002Ful>\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport IScroll from 'iscroll';\n\nexport default {\n    created(){\n        this.scroll = new IScroll(this.$refs.wrapper, {\n            click: true,\n            tap: true,\n            wheel: true,\n            mouseWheel: true,\n        })\n    },\n    methods:{\n        chooseCourse(){\n\n        }\n    }\n}\n\u003C\u002Fscript>\n```\n\n核心逻辑是，`.listWrapper`部分会被`iscroll`模块处理，以便能够方便地使用无限滚动、滚动加载等功能。\n\n于是将`@click`分别绑定到`.listWrapper`内部和外部，发现外部的点击可以响应，内部的点击无法响应。\n\n问题被进一步缩小范围：`iscroll`导致的元素点击事件不触发。\n\n接下来就想到将代码中初始化过的`iscroll`实例对象的`options`属性打印出来进行对比，看看是否初始化的时候环境检测结果有些不一样。果然发现了一点区别：\n\n没有问题的浏览器上：\n\n```\ndisableMouse: true\ndisablePointer: false\ndisableTouch: true\n```\n\n有问题的浏览器上：\n\n```\ndisableMouse: true\ndisablePointer: true\ndisableTouch: false\n```\n\n可以看到`disablePointer`和`disableTouch`的值是不一样的。开始翻看`iscroll`的源码\u003Chttps:\u002F\u002Fgithub.com\u002Fcubiq\u002Fiscroll\u002Fblob\u002Fmaster\u002Fbuild\u002Fiscroll.js>，发现它的基本逻辑：\n\n构造函数`IScroll()`从第317行开始，首先处理了选项的合并。\n\n第331行有三个关键的选项：\n\n```javascript\ndisablePointer : !utils.hasPointer,\ndisableTouch : utils.hasPointer || !utils.hasTouch,\ndisableMouse : utils.hasPointer || utils.hasTouch,\n```\n\n于是，往回翻，找到`utils`中这几个东东的定义：\n\n```javascript\nhasTouch: 'ontouchstart' in window,\nhasPointer: !!(window.PointerEvent || window.MSPointerEvent), \u002F\u002F IE10 is prefixed\n```\n\n因此`hasTouch`和`hasPointer`的值不同会导致上述选项`disableTouch`和`disableMouse`的不同。再接下来就简单了，将这两个值分别打印出来，很快就能发现，正是旧版企业微信的`hasTouch`判断失误，导致了后续`disableMouse`为`true`，导致鼠标`mousedown`\u002F`mouseup`事件相关处理函数没有被调用。\n\n值得注意的是，没有问题的浏览器，`disableMouse`也为`true`。这里经过调试跟踪，发现这部分浏览器也没有走鼠标事件`mousedown`\u002F`mouseup`，而是走了`pointerdown`\u002F`pointerup`事件。于是caniuse了一下，发现`Pointer`事件从Chrome 55开始支持的，而出问题的企业微信的webview使用的是Chrome 49。因此这个最后的保险，`Pointer`事件也失效了。\n\n![Pointer事件兼容性](\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F2.png)\n\n### 修复\n\n超级简单，只需要在调用的时候传入`disableMouse:false`即可。\n\n```javascript\nthis.scroll = new IScroll(this.$refs.wrapper, {\n    disableMouse: false,\n    click: true,\n    tap: true,\n    wheel: true,\n    mouseWheel: true,\n})\n```\n\n### 复盘\n\n问题解决了，自然要回想一下，是什么导致了这个问题，我还能做点什么。\n\n导致问题的直接原因是：企业微信的webview有一个bug，明明不支持触控，但是在`'ontouchstart' in window`时却返回了`true`。又因为新版本的企业微信已经没有了这个问题，所以要反馈给企业微信也是一件无意义的事情了。\n\n另一个有一些关系的原因是：引入了一些没有把握的第三方库。事实上我们对于第三方库的引入一直非常谨慎，但这个系统因为早期并不是一个正式的项目，因此有些随意了。\n\n再接下来就是记录一下这整个过程，整理成本文，虽然并不知道对其他人是否有一些思路上的借鉴意义。\n\n值得一提的是，在修复的过程中，发现`iscroll`的源码早已不是我很多年前看的时候的样子了，整个模块划分非常清晰，所以代码读起来很容易。接下来有空的时候我应该会再仔细读一下它的代码，关于事件的检测、分发还是做得挺有意思的。\n\n修复的过程让我想到了知乎上看过的一个问题：我去修电脑，别人只飞了一根线，却要了我100元，合理吗？下面有个回答：线值0.5元，知道怎么飞这根线值99.5元。\n\n对工程师来说也一样，修复的过程只有一行代码，但找到这个方案的过程却并不容易。比如我，因为这个问题的出现，半夜11点半才下班。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fa-bug-in-wechat-work\u002F1.png",{"id":43,"type":7,"slug":44,"title":45,"date":46,"category":26,"tags":47,"body_markdown":50,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},184,"what-does-frontend-architect-do","【问答】2018年的前端是否有『架构』可言?","2018-05-28 21:52",[48,49],"前端","架构","\n本文来自知乎问题[2018年的前端是否有『架构』可言?](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F278925914\u002Fanswer\u002F403803226)\n\n## 牛角尖\n\n一，明确下架构的定义，按题主的说法，“整个后端的架构是非常复杂和庞大的，一个好的架构师需要在数不清的方案组合中进行架构选择”。\n\n所以，回答前端的方案组合很多，是否就可以回应这个问题了。那么，前端架构的方案多吗？多如牛毛吧，比后端多很多很多吧。\n\n-------------------以上是牛角尖，以下是正经回答------------------\n\n## 一，关注点\n\n后端架构的目标，题主也说到了，高性能、高可用、可扩展、安全。\n\n至于后面说的那么多知识点，虽然都是有用的，但是不免有些掉书袋了。你说高可用有各种难点，有各个方面，但事实上这些问题的解决方案也都非常明确了。如果不明确的也是业界公认暂时无解的问题，例如分布式CAP等。至于其中的具体策略，如果还需要自己去想，那这个后端工程师（还不叫架构师）应该是不合格的。做后端的架构其实基本上是选择。\n\n那么前端呢？目前是高性能、高可用、可扩展、安全？所以数据库不用选，语言不用选，框架三选一，架构就简单了？我认为这是对前端的关注点理解不到位的体现。\n\n## 二，前端的核心关注点——用户体验\n\n作为前端工程师，关注点是什么呢？首先就是用户体验。\n\n用户访问我的网站快不快，用起来爽不爽，会不会觉得卡，会不会觉得low。这才是前端最该关注的东西。\n\n所以我做的架构一点都不高大上。如何让网站加载更快，如何不引入不必要的代码，把不必要的代码干掉，如何让用户加载最少的内容，如果让用户最快看到效果，前端渲染还是后端渲染，同步输出还是异步输出。\n\n是不是觉得一点都不是架构师干的事？但前端构架师确实就是在干这些事。\n\n诸如此类的事情非常多，比如报错是红色文字还是输入框变红？Loading放顶上还是页面中间？转菊花还是骨架屏？先显示占位图还是先白屏？\n\n所以，你要说前端技术架构师，还不如说是用户体验架构师。跟后端不在一个维度上。\n\n## 三，工程难度\n\n题主说，“最多算上错误监控、埋点方案、缓存策略等偏运维的决策”，仿佛这些是一句话可以搞定的很简单的事情。\n\n是，后端错误监控不难，无非try..catch嘛，再不挤进程挂了还有运维工具可以救人于水火。但是前端呢？一个ajax加载失败了怎么监控，一个css加载失败了怎么监控，再极端一点，浏览器卡死了怎么监控，页面崩溃了怎么监控？\n\n至于埋点，能做得好的我都非常佩服。连用户关闭了你的页面都监控不到，还想通过埋点来获取业务和技术数据？\n\n至于缓存就更奇葩了，到底有没有缓存，到底是200还是304还是没有请求了，到底版本对不对，到底有没有被代理瞎搞，到底这个神奇的浏览器是怎么处理缓存的……别以为说一句“加个缓存”，就这么轻轻松松地搞定了。每一个把缓存玩好的前端工程师都值得尊敬。就不说更多的什么localStorage\u002FserviceWorker之类的了。\n\n说这一大段，并不是要说前端架构师搞这些很苦很累，你们要尊重我们。而是想说，这些不起眼的事情，不如想象的那么容易，很多时候架构师是在帮助工程师探路或者踩坑，把这些东西的技术方案搞定。\n\n## 四，面向团队\n\n前端工程化还不成熟，这是个切实的情况。架构师在项目选型的同时，有相当部分的精力会放到工程化方案的选型上。是否用webpack，是否用npm，是否用typescript，是否用ES6\u002F7\u002F8，是否要babel，是否用postcss。是否要CI，是否要自动打包发布。是否要分包加载，是否要抽取CSS文件……\n\n这些事，在后端可能不存在，在客户端可能也不存在，或者即使存在，也被IDE悄悄地代劳了。作为项目的选型者，写几个依赖，install一下就开工了。但是前端不行，前端这里有大量的工程化选型或者配置\u002F开发的任务。\n\n作为架构师，如何选择一套好用的、没有坑的语言\u002F构建工具，也不容易。\n\n我举个例子吧，大家可能觉得typescript很好，直接用不就好了吗？殊不知，typescript需要配置tsc或者webpack的ts-loader（现在babel也可以了），还有tsconfig文件，以及各种.d.ts文件。当然，即使这样，也有可能在写代码的时候报一堆无法解决的错误，需要你花大量的时间去研究为什么有这个做，怎样可以不报错（也可能根本就不能不报错）。当你在Vue中使用typescript时，你需要研究怎样让它识别Vue Component的类型，怎样做自动提示和类型检查。当你在ajax使用时会发现你要研究怎样将服务端返回的数据与某个类型映射起来（然后发现在runtime时毛用都没有）。\n\n前端架构师，也有相当多的精力在这上面。\n\n这是一个好的现状吗？未必，但现状确实如此，这个锅架构师不背谁来背？\n\n## 五，小结\n\n所谓架构，我理解是综合考虑目标、业界和团队，作为合理的方案选择，既能支撑业务的发展，又能令团队满意。如果能达到这个目标，自然就是一个好的架构。目前来看，前端要做到一个好的架构不容易，做的事情并不比后端少。\n\n至于说前端架构师做的所谓的这些架构的事情是否高大上，那是另一个没有答案的问题。\n",{"id":52,"type":7,"slug":53,"title":54,"date":55,"category":11,"tags":56,"body_markdown":58,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},124,"comments-on-tnt-workstation","【问答】如何评价锤子科技推出的TNT工作站？","2018-05-16 09:46",[57],"tnt","\n本文来自知乎问题[如何评价锤子科技推出的TNT工作站？](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F277297661\u002Fanswer\u002F392829702)\n\n先说三个革命性的交互。\n\n## 一，TNT\n\n> TNT是指touch and talk，先指点再说话，通过这两个动作的组合完成一个输入动作。\n\n我个人认为这个交互方向还是有点想法的，先指点再用语音确实能极大提高机器理解的准确率。\n\n但是具体的交互和场景是不是这个工作站这么玩，值得打问号。\n\n先说交互，从现场演示和演示视频来看，它需要人频繁地“点按屏幕-说话-放开”，这个过程其实很容易失误，现场演示也有这样的失误（希望没看错）。另外长时间办公的场景下，反复高频进行这个操作，手是否会累，我个人觉得是很有可能的事情。\n\n再说场景，很多答案都提到了，这玩意是放办公室？一堆人靠吼来办公？靠语音这么不靠谱的方式来做excel表格真的好？还是放老板那？让老板自己记账、做PPT？\n\n如果有个财务是这么做账：“5月 工资 10000 报销 1000 老板出去玩 500 啊 识别错了 五百 不是五十” 会不会被打死？发布会完了之后回想起这个表格的演示，只能说，锤子的产品经理有认真地研究过靠excel为生的人是怎么玩键盘和excel的吗？\n\n## 二，水晶球\n\n> 水晶球是指AI能力的应用，发布会演示最多的场景是PPT自动排版、寻找素材之类的场景。\n\n这一块个人觉得是整个发布会最大的亮点了，确实能解决一些痛点。\n\n人工智能、AI这么火，但是却真的没有见到在PC上有什么用武之地，一堆厂商都钻到了语音助手的牛角尖里出来不来，包括Windows的小娜、Mac的Siri，反正我是没见有人对着电脑说话的。\n\n所以这个水晶球实际上算一个初级AI在PC上的落地实践，而且确实是能在一定程度上提升工作效率的。\n\n乐观一点想，就算这是锤子给大家做了个示范吧，AI不一定要是语音助手。也希望大家去尽情地抄一下这个特性，把AI真的带到PC电脑上来。\n\n如果只是讨论水晶球在这个工作站的事情的话，其实还任重而道远。如果功能仅限于PPT排排版，搜搜图什么的，那么用途会非常有限。如果能扩展到整个系统层面，就会比较好玩了。但是在系统层面去做这些事情，就意味着要打通各个APP，要有统一的标准和接口，凭锤子一己之力去推动这个事情，不看好。\n\n## 三，发牌？卡牌？\n\n不记得名字了，反正就这么个东西。个人打负分。这玩意看起来很炫，但是没有人真的会这么用。\n\n先看现场演示的结果，操作失误率非常高。这背后并不一定都是老罗的问题，而很可能是这套交互本身的问题。\n\n以IM为例，将10+个长得一毛一样的IM对话一起摆在人眼面前，信息处理的压力会非常大。人要非常小心地去辨别和记忆每个窗口的对象、会话上下文，稍不留神就会出错。即使你很小心不出错，也会处于一种“我一不留神就会出错”的焦虑中。\n\n如果你说，我不用记忆啊，我一个一个去看不就好了。对，那你为什么不一个一个展示给我呢？这两者在信息的传达上完全不一回事的。\n\n至于后半程演示的建群，批量单聊等，其实是交互成本很高的操作。得承认它很炫，但是因为交互成本高，实用性其实并不会太好。完全属于只是看着爽的东西。\n\n## 定位\n\n再说这个工作站的定位。\n\n第一个问题，我用什么写代码？\n\n同样的问题，可以换成“我用什么做音频\u002F做视频\u002F做实验\u002F做科学计划\u002F做机器学习\u002F......”，本质上是一个生态的问题。既然是作为桌面工作站，那么它的功能完备性就显得非常重要了。如果只能Office三件套，再七拼八凑地用一些不知名的软件去勉强把工作应付出来（是否能应付还不一定），那怎么能叫工作站？这样的状态只有在“我在地铁\u002F公交\u002F飞机等条件非常受限的地方，需要临时完成一点微小的工作”的场景下，才能接受的。\n\n所以，拿一个Android来做工作站，是一件相当不被认可的事情。\n\n第二个问题，我凭什么不买iMac\u002FSurface Studio\u002F笔记本？\n\n至少这一些是真的电脑，想做的事情都有成熟的解决方案来做，不用担心如果我买了它有没有能做事的软件，做得爽不爽。\n\n如果你是一个价格2000块的工作站，那我还有理由说服自己，人家比你们这些货便宜好几倍啊，试试又不会死。\n\n但是一台手机 + 一台电脑，一万多？给我一个不买iMac的理由？哪怕就一个。\n",{"id":60,"type":7,"slug":61,"title":62,"date":63,"category":26,"tags":64,"body_markdown":68,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":69},183,"nginx-koa-use-http2-server-push","Nginx + Koa 开启http\u002F2 server push","2018-05-15 09:25",[65,66,67],"node.js","nginx","http2","\n一看这标题就是不准备好好写的。对的，最近特别忙，只能简单记录一下折腾的东西。\n\n## Nginx开启server push\n\n1. 升级nginx到1.13.9或以上版本（注意1.13.6修改http\u002F2的实现，与一些旧版本客户端不兼容，比如旧版Android okhttp）\n2. nginx配置中加上`http2_push_preload on`，表示使用preload header来作为server push标识\n\n## Node开启server push\n\nNode处理文件内容时加上preload header即可，例如：\n\n```\nlink: \u003C\u002Fmain.37d69167.css>; as=style; rel=preload, \u003C\u002Fmain.f06ad8b3.css>; as=style; rel=preload\n```\n\n此处比较科学的做法应该是使用一个中间件，在返回内容之前，根据要返回的HTML内容来处理preload header。\n\n因为我主要处理静态html文件，又用的koa，所以将主要逻辑放在了koa-static的`setHeaders`函数中。`setHeaders`主要用于在返回静态文件前设置自定义的header，刚好和server push的场景相符。\n\n\u003C!-- more -->\n\n主要逻辑：\n\n1. 读取html文件，使用正则表达式匹配出css和js文件的路径（如果有图片也可以一起）\n2. 将这些资源拼接成preload header的值\n\n```javascript\nconst htmlContent = fs.readFileSync(path, 'utf8');\nconst styleRegExp = \u002F\u003Clink(?:.*?)href=['\"]?([\\w\\.\u002F]+\\.css)['\"]?\u002Fg\nlet currentStyle;\nconst styleList = [];\nwhile(currentStyle = styleRegExp.exec(htmlContent)){\n    styleList.push(currentStyle[1]);\n\n}\nconst scriptRegExp = \u002F\u003Cscript(?:.*?)src=['\"]?([\\w\\.\u002F]+\\.js)['\"]?\u002Fg\nlet currentScript;\nconst scriptList = [];\nwhile(currentScript = scriptRegExp.exec(htmlContent)){\n    scriptList.push(currentScript[1]);\n}\nlet link = styleList.map((styleFile) => {\n    return `\u003C${styleFile}>; as=style; rel=preload`;\n}).join(', ');\nlink += ', ' + scriptList.map((scriptFile) => {\n    return `\u003C${scriptFile}>; as=script; rel=preload`;\n}).join(', ');\ncache[path] = link;\nres.setHeader('Link', link);\n```\n\n这样就可以实现http\u002F2 server push了。\n\n## 有缓存不再推送\n\n上面两步都超简单，网上的教程满天飞。这一个标题“有缓存不再推送”内容才是促成本文的原因。\n\n回到http\u002F2 server push的原理，浏览器访问`index.html`时，server除了返回html，还将css \u002F js \u002F image也一并推送回来，这样浏览器接受完之后，就不用再单独请求一次，从而加快页面的加载。\n\n但是这里有一个矛盾，如果我们的静态资源是有长缓存的，下一次请求的时候该推送还是不该推送呢？如果推送，则相当于是忽略了缓存，白白浪费带宽。\n\n到目前为止，这些仍然是网上文章中的主要结论。于是我就验证了一下，有缓存时是否真的会浪费带宽。于是我打开了`chrome:\u002F\u002Fnet-internals\u002F#http2`，然后找到了活跃的http\u002F2连接。在`Source Type`为`HTTP2 SESSION`那一栏中，可以看到详细的HTTP\u002F2通信过程。我截了一些图：\n\n首先是没有缓存的情况下，server push开启：\n\n1. 浏览器请求完html之后，发现了`PUSH_PROMISE`，按字面意思理解，也就是server承诺推荐这些资源\n    ![push promise](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg)\n2. 接下来浏览器接受了这些推送的stream，把资源弄下来了\n    ![accept push](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F02.jpg)\n\n到这里一切正常。但是当资源有缓存时，再次请求，server push仍然开启的情况下：\n\n1. 浏览器收到PUSH_PROMISE，注意最后一行，`main.6e607578.js`的`stream_id`为`12`\n    ![push prmise stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F03.jpg)\n2. 接下来搜索这个`stream_id=12`，找到一串看不懂的东西，看起来跟TCP窗口调整的逻辑很类似？\n    ![stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F04.jpg)\n3. 再接着，罢工了……清楚地写着`Abandoned.`已放弃，应该是浏览器拒绝了这次推送，注意`stream_id`仍然是`12`\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n\n也就是说，在有缓存的情况下，浏览器并不会傻乎乎地再接受推送。试验到这里后有点不可思议，于是又从两个方面做了验证。\n\n1. 网络带宽\n    这是`chrome:\u002F\u002Fnet-internals\u002F#timeline`的时间线，比较明显的有15个峰，分别是我发起的15次请求，前5次缓存有效，中间5次没有缓存，后5次缓存有效。可以明显看到，有缓存时带宽是比没缓存时低的，证实有缓存时push并不会真的发生。\n\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n2. 服务端网络IO\n    在有缓存的情况下，服务端网络IO大约每个请求0.2-0.4M，在无缓存的情况下，服务端网络IO大约每个请求2.2M-2.4M。同样证实有缓存的情况下，push并不会发生。\n\n## 有缓存不再推送\n\n其实有上面的结论后，不需要再做什么了。但是仍然怀疑这是不是哪一端实现的bug，要不然为什么大家都把这当作一个不可解决的缺陷呢？\n\n那么就假装我不知道上面这一段吧，还是需要自己根据是否有缓存控制是否开启server push。\n\n最容易想到的办法就是cookies了，将已推送过的资源url放入cookies中，下次再请求时进行对比，已有的资源就不再推送。\n\n但是这样的话，cookies会非常庞大。于是有人提出了使用BloomFilter来存放推送信息。\n\nBloomFilter是一个空间和时间复杂度都比较小的算法，主要用于快速进行“有损”存在性判定。所谓存在性判定就是给定一个key，确定它是否存在。而“有损”的意思则是指它并不100%精准。\n\n它的原理可以简单这么理解：首先放一个数组，接下来每一个需要检查的key都做一个hash，映射到这个数组中的某几个位置，如果这几个位置全部为`1`，则认为这个key存在，否则认为这个key不存在。\n\n考虑到server push的场景，即使不精准也不影响页面打开使用，因此它是适用的。HTTP server软件H2O就是使用了类似的算法。\n\n本来我也打算这么实现一版，但是后来转念一想，我就一个页面，3个资源，好像没必要这么麻烦。不如直接全部hash一把，hash匹配就认为有缓存，hash不匹配就认为没缓存，全部重新推送一遍。\n\n于是就有了类似这样的代码：\n\n```javascript\nlet pushHash = md5(cache[path]);\nif(!cookie || cookie !== pushHash){\n    res.setHeader('Link', cache[path]);\n    res.setHeader('Set-Cookie', 'push=' + pushHash);\n}\n```\n\n简单粗暴有效。\n\n参考：\n\n- https:\u002F\u002Fimququ.com\u002Fpost\u002Fcache-aware-server-push-in-h2o.html\n- https:\u002F\u002Fwww.keakon.net\u002F2018\u002F03\u002F07\u002FNGINX%E6%94%AF%E6%8C%81HTTP\u002F2serverpush%E4%BA%86\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg",{"id":71,"type":7,"slug":72,"title":73,"date":74,"category":11,"tags":75,"body_markdown":78,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":79},123,"2017-tech-hot-points","2017年科技新闻热点分析","2018-03-05 13:25",[76,77],"科技","热点","\n2017年开始我做了一款资讯App，叫土葱日报，主要用来抓取科技新闻。目前只有Android版本，在Google Play和应用宝市场有下载。\n\n经过一年的运行，积累了上十万条新闻数据，并从中分析出了近百万关键词数据。在此对结论做一个展示。\n\n\u003C!-- more -->\n\n![五大热门公司](\u002Fassets\u002Ftech\u002F2018\u002F2017-tech-hot-points\u002F01.jpg)\n\n五大热门公司：苹果、腾讯、亚马逊、乐视、特斯拉\n\n![五大热门技术](\u002Fassets\u002Ftech\u002F2018\u002F2017-tech-hot-points\u002F02.jpg)\n\n五大热门技术：人工智能、VR AR、小程序、自动驾驶、区块链\n\n![几个结论](\u002Fassets\u002Ftech\u002F2018\u002F2017-tech-hot-points\u002F03.jpg)\n\n注：以上皆由土葱日报采集的科技新闻分析而来，由于分析方法不一定严谨，数据不一定全面，不对结论负责任。\n\n注：由于文字不足300字，无法声明原创，但本文结论均由本人原创。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2018\u002F2017-tech-hot-points\u002F01.jpg",{"id":81,"type":7,"slug":82,"title":83,"date":84,"category":85,"tags":86,"body_markdown":88,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},86,"2017-summary","2017小结","2018-02-21 14:30","life",[87],"年度总结","\n2017于个人来说，是非常重要的一年，这一年经历了很多此前从来没有接触过的事情。甚至某一些事情可能会成为未来一些事情开始的基础。\n\n## 工作\n\n这一年工作状态总体不算太好。一年的安全域工作着实无趣，中间也就少不了自己打打酱油。\n\n离开了产品团队，也觉得并不是很踏实。花了半年时间介入新股认购重构的需求，最后结果也并不是太好。\n\n年底回到产品团队，却发现物是人非。产品换了一大波人来和开发对接，一切都不太对劲。[*原来对接的产品也都鸡犬升天，看着和自己同一年毕业进腾讯，同一年进富途的人现在成了三个中心的总监，心里多少会有些不平衡。*]\n\n前端方面，可以说是覆灭。业务小组分开后，前端组的概念更加虚无缥缈，虽然要有预期，但是事情比想象更坏。[*leader们全部是后端出身，不懂前端价值和方法，缺少对前端的尊重，也不希望外人介入管理，*]各组间隔阂进一步加深，导致前端间交流也较少，价值进一步削弱。\n\n客观说，我也没有竭尽全力去推动前端方面的事情。但是话说回来，在一个氛围诡异的环境里，把自己做成一个歇斯底里的形象，这个团队也未必能忍得下。\n\n年底接到了IT方面的工作。在产品和前端都不如意的情况下，反而希望更早介入这一块的工作。前端方面仍然有一些计划继续去推进，但是早晚需要放手。\n\n\u003C!-- more -->\n\n## 课程\n\n制作了两门慕课课程，花的精力选比想象中要大，几乎耗尽了整年的休息时间。课程上线后也有几万块的收入，但是还是觉得性价比真的太低。而且周末连续待在家，身体素质也觉得下降了。以后不再接这样太耗精力的事情了。\n\n## 投资\n\n在老温的介绍下，跟着小虫开始做一些港美股投资。整年收益还是不错的，但是主要收入还是来自自己的判断，主要靠腾讯。美股斯凯奇确实赚了一些钱，但是因为投得不多，收益也就不算太多。2018年按理是行情很好的一年，但是收益如何不敢奢望。\n\n9月份在好奇心驱使下花了2000买了一些VEN，算是正式踏足币市。后来政策落地，跌到300，也就没卖，不当回事了。结果到年底居然神奇地挣回来了，还翻了几十倍。只能说是瞎猫撞死老鼠了。币市目前投机氛围非常严重，2018年是否值得再投入有待观察。\n\n## 画画\n\n本着两个人要制造更多共同爱好的原则，去上了几节素描课。其实投入进去还真的有点意思，但是一直画不好也还是会觉得很失落。后来因为录课程的原因，耽误了。2018年希望继续捡起来，好好学一学画画。\n\n## 台湾行\n\n10月份去了一趟台湾，持续一周的时间。这次行程有些喜出望外，台湾给人的感觉非常好。语言文字都相通，没有沟通障碍，台湾人也热情友好，如沐春风。饮食习惯也非常一致，吃的很好。最重要的是发现台湾真的是一个高素质的地方，人们普遍自律而又有人情味，人会不由自主地放下防御心态，真的非常舒服。\n\n## 其他\n\n书读太少了。\n\n前端行业可能也就这样了，再出风头也意义有限，刻意回避也不是很必要。\n\n技术方面，客户端开发没有深入，前端方面投入也不如之前。开始找不到技术的兴奋点，更想独立完成一些产品。\n\n土葱日报是真心想好好做的产品，但是精力有限，加之客户端开发不熟悉，仍然离想要的结果太远。\n\n小兔笔记已经持续投入了几年。下半年开始了底层重构，为未来一段时间的持续进化提供基础。希望在不久之后推出一个基本满意的版本。\n\n生孩子的事情几经犹豫，最后还是先搁下，眼前要忙的事情还多。\n\n买车的事情提上日程，但是买什么犹豫不决。\n\n全年税后收入[*60*]万，够我得瑟好久了。\n",{"id":90,"type":7,"slug":91,"title":92,"date":93,"category":11,"tags":94,"body_markdown":98,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},125,"hello-xiaoai","小爱同学 你好吗","2018-01-09 14:07",[95,96,97],"智能音箱","小爱同学","小米","## 秀优越感\n\n小米AI音箱299元，但是现在还买不到。要不选择等半年，要不选择淘宝加价一两百。然而令人惊喜的是，参加前端体验大会后，主办方直接送了一个作为礼品。于是我能抢先体验到这一款产品。秀优越感完毕。\n\n## 功能\n\n外形和基本功能就不详细介绍了，网上的评测文章挺多了。\n\n我用得比较多的功能主要还是点歌，叫一声小爱同学，然后就可以点歌。而且还可以沿同一歌手的歌单一直放下去。至于音效嘛，作为一款身型小巧的音箱，还是有点超出预期的。\n\n此外，家里有一个智能插座是连接饮水机的，还有小米盒子、小米扫地机器人、小米台灯等设备，都可以比较好地联动：\n\n“小爱同学，我要看琅玡榜”\n\n“小爱同学，电视声音大一点”\n\n“小爱同学，扫地”\n\n“小爱同学，扫地机器人还有多少电”\n\n“小爱同学，打开台灯”\n\n“小爱同学，台灯亮度高一点”\n\n“小爱同学，开始烧水”\n\n……\n\n总体来说，小爱同学对声音的响应灵敏度还不错，在房间里也能很自然地交互，对智能家居的打通也做得非常好，用起来真的非常方便。\n\n\u003C!-- more -->\n\n前几天传出来小米估值2000亿美元在香港上市，个人认为，这其中应该至少有800亿美元的估值来自小米智能家居生态。而AI音箱作为智能家居重要的一环，作用不可小觑。\n\n## 一些不爽的地方\n\n前面说了，爽的地方很多评测都说过了，因此不详细说。这里重点说一说不爽的地方。\n\n### 连续自然语言交互\n\n首先会感觉不爽的地方是没有连续自然语言交互的能力。\n\n“小爱同学，放一首歌”\n\n“好的，即将为你播放XXX的XXX”\n\n“下一首”\n\n“......”\n\n“小爱同学，下一首”\n\n“...”（开始播放下一首）\n\n在连续的语音指令后，很容易遗忘“小爱同学”这个称呼。而这个称呼的长度多达4个字，我要说的指令“下一首”本身都只有3个字。多次出现这种情况会让人比较抓狂。\n\n### 音源鉴别\n\n另一个不爽的地方在于小米AI音箱没有鉴别音源的能力。例如我把AI音箱放在电视旁边，我在看电视，然后我叫了一声小爱同学，然后音箱就开始听电视讲台本了，听完一段后装个傻，大家都很愉快。\n\n说它完全没有音源鉴别能力也并不准确。因为我做了一些实验：\n\n- 音箱在播放声音的时候是可以同时监听语言的，比如放歌的时候（不论是蓝牙音源还是音箱自己放的歌）可以被唤醒\n- 音箱在播放声音时，来自音箱自己的声音无法唤醒自己（例如连上蓝牙后，用手机播放“小爱同学”）\n\n因此，我猜测这里应该有用一些类似双麦降噪之类的技术，来区分声音是否是自己发出的。如果不是的话才进行唤醒响应。当然也不排除这只是个巧合……\n\n如果已经有这样一些音源定位的技术的话，我觉得定位唤醒人的音源是非常重要的，谁叫的我，我就只听谁的，这样应该可以解决上面说的电视声音干扰的问题，提升使用体验。\n\n### 语音交互的问题\n\n自古以来，人跟机器的交互都是采用确定性的方式，不管是机械手柄还是实体键盘还是触控屏，机器展示出来的界面是确定的，人的操作也是确定的。一个操作会有一个确定的反馈。这实际上是人适应机器的过程。\n\n但是语音交互打破了这种确定性，变成了机器适应人。那么，一句话说出去，机器会有怎样的反应，这个过程就变得不确定了。\n\n“小爱同学，台灯亮度调高一点”\n\n“小爱同学，台灯色温调高一点”\n\n这两句在人看来都是非常容易理解的，然而，小爱同学只能处理前一句。\n\n表面上来看，这个问题主要在于工程师的适配库不够强大，没有把色温调节这个功能点适配进去。但是深层次来看，这其实就是语音交互自身的问题。没有了确定性，那么对任何一句话的回应都是不敢做期望的。\n\n于是，一开始拿到AI音箱的时候，想什么都通过它来完成，但是在使用时间长了之后，会发生一些心理变化。\n\n例如，让AI音箱调大电视音量，如果漏掉“电视”两个字，就变成了调大AI音箱的音量，然后你需要重新再喊一次。而如果此时电视声音略大，就可能识别失败。于是两三次之后我重新拿回了遥控器调电视音量。\n\n所以语音交互的确是带来了巨大的便利性，但是同时也带来了巨大的不确定性。如果反复碰到这个不确定性带来的不好的结果，我会怀念确定性的控制方式。\n\n再比如，用AI音箱控制台灯，每一次控制完之后都会担心，我的控制是不是真的生效了，然后会跑去台灯那里看一下，控制是否生效了。（虽然这个场景不太真实，但是智能家居多了之后一定会有控制不在身边的电器的需求。）这本质上是一个对智能家居智能控制结果不太任何的体现。回想一下，我用手机控制的时候这种担心会少很多，这是因为手机上可以看到设备的当前状态，而AI音箱并没有直观反馈当前状态的地方。\n\n这些问题其实都是交互设计的研究范围。以UI的交互设计为例，经过几十年的发展，目前UI设计理论可以说非常成熟了。但是语音的交互设计体系才刚开始，如何给用户做合理的反馈，如何管理用户的心理预期可能是接下来AI音箱们需要解决的一个非常重要的课题。\n\n## 安全问题\n\n一个音箱会有安全问题？？嗯。一开始我也没有想到这个问题，可能第一次被问的话，我也应该会回答没有吧。\n\n但是，当这个音箱是小米AI音箱，背后接着那么多智能家居的时候，这个问题就变得严肃了。\n\n举个例子吧。小米生态链最近有一款智能门锁，可以接入米家，支持蓝牙、指纹、密码、钥匙开锁。虽然没有确认，但我觉得可以接入米家的应该也可以接入小米AI音箱吧。\n\n那么，如果我站在家门口，喊一句“小爱同学，请开门”……原来芝麻开门的故事真的不是童话……\n\n同样的，就算你不用门锁，你总有插座、电视、空调、扫地机器人、台灯什么的吧。如果一个陌生人在楼道里就能控制你的家电，这应该可以算是非常严重的安全问题了吧？\n\n因此，小米AI音箱在全面接入智能家居之前，声纹锁应该是个必备功课，否则爆出问题是早晚的事情。\n\n这背后其实是一个控制权的问题。当我拿起一个遥控器，我就有控制权，这个控制权靠遥控器这种实物来保证。当我拿起米家APP，我也有控制权，这个控制权靠手机这种实物（以及手机的安全机制）来保证。而当我放一个音箱在家里，这个控制权要靠什么来保证？\n\n所以，在使用小米AI音箱的这几周中，我跟它接触得相当多，而每次接触都只做一件事情——关闭麦克风。至少这样能在心理上有一种我掌握了控制权的安慰。至于是不是真的掌握，作为一个做技术的人，是不敢相信的。\n\n因此，小米AI音箱要解决的第二个问题，是确保麦克风真的可以被关闭，并且有独立的工作状态指示灯，且这个指示灯跟麦克风必须是硬件级别的关联，不可以通过软件进行修改。（可参考macbook摄像头的指示灯）\n\n最后一个跟安全相关的问题是隐私。事实上在之前有很多获得小米AI音箱F码的机会，但是我一直没有去买，因为我还是不太能接受24小时被一个机器监听。\n\n当我使用其它的数据设备的时候，我可以在使用时开机，不使用时关机或者锁屏或者待机，甚至在使用时我也可以选择暂停、放在桌上或者把摄像头对准窗外。即使是在使用手机语音助手的时候，我也可以选择在我需要的时候再点开它。但是使用小米AI音箱的时候，我并没有这种选择权，不管我在哪个房间，跟谁、朝哪个方向说话，都只能选择由它来监听我。这是由语音的交互形式决定的。\n\n因此，当我确定我在一段时间内不再使用的时候（例如睡觉前），我一定会选择关闭麦克风。否则，所有的主动权全部在机器手上。这是交互形式由人适应机器变成机器适应人所带来的改变，目前我也想不到有什么好的解决方案。\n\n## 小结\n\n鉴于上面想到的诸多问题，AI音箱到底是不是一个真的风口呢？个人依然持有一定的怀疑态度。\n\n交互的进一步完善一边要依赖交互理论的发展，另一边还要重试依赖自然语音交互技术的不断进步。而安全问题仍然会是一个在几年内都需要持续去完善的问题。\n\n再重申一下，小米AI音箱是个非常榜的产品，使用体验很不错，相关的文章网上已经非常多，所以本文重点讲述的是使用过程中感受不太好的方面。\n\n不管怎么说，一种新的可能性正在到来。为小米点赞。\n",{"id":100,"type":7,"slug":101,"title":102,"date":103,"category":26,"tags":104,"body_markdown":106,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":107},174,"notes-for-2nd-frontend-exp-conf","第二届前端体验大会杂记","2017-12-26 09:37",[105],"会议","\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","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fnotes-for-2nd-exp-conf\u002F01.jpg",{"id":109,"type":7,"slug":110,"title":111,"date":112,"category":26,"tags":113,"body_markdown":115,"permalink":116,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":117},172,"migrate-blog-to-amp","轻松迁移博客到AMP","2017-08-29 20:18",[114],"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":119,"type":7,"slug":120,"title":121,"date":122,"category":26,"tags":123,"body_markdown":126,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":127},173,"mini-app-of-xiaomi","小米也有小程序？热闹了","2017-08-29 09:17",[124,125,97],"小程序","微信","上上周五（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",190]